@bash-app/bash-common 30.293.0 → 30.295.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/__tests__/bashSectionTitleForBashType.test.js +3 -0
  2. package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
  3. package/dist/definitions.d.ts +77 -0
  4. package/dist/definitions.d.ts.map +1 -1
  5. package/dist/definitions.js +1 -0
  6. package/dist/definitions.js.map +1 -1
  7. package/dist/extendedSchemas.d.ts +7 -0
  8. package/dist/extendedSchemas.d.ts.map +1 -1
  9. package/dist/extendedSchemas.js.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/membershipDefinitions.d.ts +9 -0
  15. package/dist/membershipDefinitions.d.ts.map +1 -1
  16. package/dist/membershipDefinitions.js +10 -1
  17. package/dist/membershipDefinitions.js.map +1 -1
  18. package/dist/utils/__tests__/reputationUtils.serviceListing.test.js +18 -0
  19. package/dist/utils/__tests__/reputationUtils.serviceListing.test.js.map +1 -1
  20. package/dist/utils/__tests__/reviewDimensionUtils.test.d.ts +2 -0
  21. package/dist/utils/__tests__/reviewDimensionUtils.test.d.ts.map +1 -0
  22. package/dist/utils/__tests__/reviewDimensionUtils.test.js +48 -0
  23. package/dist/utils/__tests__/reviewDimensionUtils.test.js.map +1 -0
  24. package/dist/utils/__tests__/trustScoreUtils.test.d.ts +2 -0
  25. package/dist/utils/__tests__/trustScoreUtils.test.d.ts.map +1 -0
  26. package/dist/utils/__tests__/trustScoreUtils.test.js +101 -0
  27. package/dist/utils/__tests__/trustScoreUtils.test.js.map +1 -0
  28. package/dist/utils/reputationUtils.d.ts.map +1 -1
  29. package/dist/utils/reputationUtils.js +7 -1
  30. package/dist/utils/reputationUtils.js.map +1 -1
  31. package/dist/utils/reviewDimensionUtils.d.ts +33 -0
  32. package/dist/utils/reviewDimensionUtils.d.ts.map +1 -0
  33. package/dist/utils/reviewDimensionUtils.js +35 -0
  34. package/dist/utils/reviewDimensionUtils.js.map +1 -0
  35. package/dist/utils/trustScoreUtils.d.ts +25 -0
  36. package/dist/utils/trustScoreUtils.d.ts.map +1 -0
  37. package/dist/utils/trustScoreUtils.js +99 -0
  38. package/dist/utils/trustScoreUtils.js.map +1 -0
  39. package/package.json +1 -1
  40. package/prisma/schema.prisma +133 -15
  41. package/src/__tests__/bashSectionTitleForBashType.test.ts +4 -0
  42. package/src/definitions.ts +85 -1
  43. package/src/extendedSchemas.ts +7 -0
  44. package/src/index.ts +2 -0
  45. package/src/membershipDefinitions.ts +12 -1
  46. package/src/utils/__tests__/reputationUtils.serviceListing.test.ts +24 -0
  47. package/src/utils/__tests__/reviewDimensionUtils.test.ts +65 -0
  48. package/src/utils/__tests__/trustScoreUtils.test.ts +105 -0
  49. package/src/utils/reputationUtils.ts +7 -1
  50. package/src/utils/reviewDimensionUtils.ts +70 -0
  51. package/src/utils/trustScoreUtils.ts +136 -0
@@ -0,0 +1,35 @@
1
+ const MIN_DIM = 1;
2
+ const MAX_DIM = 5;
3
+ /** Validate optional 1–5 dimension; returns null when omitted. */
4
+ export function parseOptionalDimensionRating(value, fieldName) {
5
+ if (value === null || value === undefined) {
6
+ return null;
7
+ }
8
+ const n = Number(value);
9
+ if (!Number.isInteger(n) || n < MIN_DIM || n > MAX_DIM) {
10
+ throw new Error(`${fieldName} must be an integer from ${MIN_DIM} to ${MAX_DIM}`);
11
+ }
12
+ return n;
13
+ }
14
+ export function parseRequiredOverallRating(value) {
15
+ const n = Number(value);
16
+ if (!Number.isInteger(n) || n < MIN_DIM || n > MAX_DIM) {
17
+ throw new Error(`Overall rating must be an integer from ${MIN_DIM} to ${MAX_DIM}`);
18
+ }
19
+ return n;
20
+ }
21
+ export function averageNullable(values) {
22
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
23
+ if (nums.length === 0)
24
+ return null;
25
+ const sum = nums.reduce((a, b) => a + b, 0);
26
+ return Math.round((sum / nums.length) * 100) / 100;
27
+ }
28
+ export function percentTrue(values) {
29
+ const defined = values.filter((v) => typeof v === "boolean");
30
+ if (defined.length === 0)
31
+ return null;
32
+ const yes = defined.filter(Boolean).length;
33
+ return Math.round((yes / defined.length) * 100);
34
+ }
35
+ //# sourceMappingURL=reviewDimensionUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewDimensionUtils.js","sourceRoot":"","sources":["../../src/utils/reviewDimensionUtils.ts"],"names":[],"mappings":"AA+BA,MAAM,OAAO,GAAG,CAAC,CAAC;AAClB,MAAM,OAAO,GAAG,CAAC,CAAC;AAElB,kEAAkE;AAClE,MAAM,UAAU,4BAA4B,CAC1C,KAA8B,EAC9B,SAAiB;IAEjB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,OAAO,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,4BAA4B,OAAO,OAAO,OAAO,EAAE,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAc;IACvD,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,OAAO,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,0CAA0C,OAAO,OAAO,OAAO,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAqC;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAsC;IAChE,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC;IAC3E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;AAClD,CAAC"}
@@ -0,0 +1,25 @@
1
+ export type TrustScoreActorType = "host" | "service";
2
+ export interface TrustScoreInputs {
3
+ bayesianReviewScore: number | null;
4
+ completedVolume: number;
5
+ cancellationRate: number;
6
+ responseRate: number;
7
+ identityVerified: boolean;
8
+ verifiedAttendanceRate?: number;
9
+ verifiedCompletionRate?: number;
10
+ accountAgeDays: number;
11
+ disputeRate: number;
12
+ repeatCustomerRate: number;
13
+ }
14
+ export interface TrustScoreBreakdown {
15
+ score: number | null;
16
+ components: Record<string, number>;
17
+ weights: Record<string, number>;
18
+ }
19
+ /**
20
+ * Compute a 0–100 trust score from normalized component signals (0–1 each).
21
+ */
22
+ export declare function computeTrustScoreFromInputs(actorType: TrustScoreActorType, inputs: TrustScoreInputs): TrustScoreBreakdown;
23
+ /** Convenience when only review stats are available (early bootstrap). */
24
+ export declare function trustScoreFromReviewAverage(actorType: TrustScoreActorType, averageRating: number | null | undefined, totalRatings: number | null | undefined): number | null;
25
+ //# sourceMappingURL=trustScoreUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustScoreUtils.d.ts","sourceRoot":"","sources":["../../src/utils/trustScoreUtils.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,gBAAgB;IAC/B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AA6CD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,mBAAmB,EAC9B,MAAM,EAAE,gBAAgB,GACvB,mBAAmB,CAyCrB;AAED,0EAA0E;AAC1E,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,mBAAmB,EAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACtC,MAAM,GAAG,IAAI,CAef"}
@@ -0,0 +1,99 @@
1
+ import { bayesianAverage } from "./reputationUtils.js";
2
+ const HOST_WEIGHTS = {
3
+ reviews: 0.35,
4
+ volume: 0.15,
5
+ cancellation: 0.1,
6
+ response: 0.1,
7
+ identity: 0.1,
8
+ attendance: 0.05,
9
+ accountAge: 0.05,
10
+ disputes: 0.05,
11
+ repeat: 0.05,
12
+ };
13
+ const SERVICE_WEIGHTS = {
14
+ reviews: 0.4,
15
+ volume: 0.15,
16
+ cancellation: 0.1,
17
+ response: 0.1,
18
+ identity: 0.1,
19
+ completion: 0.1,
20
+ accountAge: 0.05,
21
+ disputes: 0.05,
22
+ repeat: 0.05,
23
+ };
24
+ function clamp01(n) {
25
+ if (n < 0)
26
+ return 0;
27
+ if (n > 1)
28
+ return 1;
29
+ return n;
30
+ }
31
+ function logScaledVolume(count, cap = 100) {
32
+ if (count <= 0)
33
+ return 0;
34
+ return clamp01(Math.log10(count + 1) / Math.log10(cap + 1));
35
+ }
36
+ function accountAgeScore(days) {
37
+ return clamp01(Math.log10(Math.max(days, 1) + 1) / Math.log10(365 * 3 + 1));
38
+ }
39
+ function inverseRate(rate) {
40
+ return clamp01(1 - clamp01(rate));
41
+ }
42
+ /**
43
+ * Compute a 0–100 trust score from normalized component signals (0–1 each).
44
+ */
45
+ export function computeTrustScoreFromInputs(actorType, inputs) {
46
+ const weights = actorType === "host" ? HOST_WEIGHTS : SERVICE_WEIGHTS;
47
+ const reviewComponent = inputs.bayesianReviewScore != null
48
+ ? clamp01(inputs.bayesianReviewScore / 5)
49
+ : 0.5;
50
+ const components = {
51
+ reviews: reviewComponent,
52
+ volume: logScaledVolume(inputs.completedVolume),
53
+ cancellation: inverseRate(inputs.cancellationRate),
54
+ response: clamp01(inputs.responseRate),
55
+ identity: inputs.identityVerified ? 1 : 0,
56
+ accountAge: accountAgeScore(inputs.accountAgeDays),
57
+ disputes: inverseRate(inputs.disputeRate),
58
+ repeat: clamp01(inputs.repeatCustomerRate),
59
+ };
60
+ if (actorType === "host") {
61
+ components.attendance = clamp01(inputs.verifiedAttendanceRate ?? 0);
62
+ }
63
+ else {
64
+ components.completion = clamp01(inputs.verifiedCompletionRate ?? 0);
65
+ }
66
+ let weighted = 0;
67
+ let weightSum = 0;
68
+ for (const [key, weight] of Object.entries(weights)) {
69
+ const val = components[key];
70
+ if (val == null)
71
+ continue;
72
+ weighted += val * weight;
73
+ weightSum += weight;
74
+ }
75
+ if (weightSum === 0) {
76
+ return { score: null, components, weights };
77
+ }
78
+ const normalized = weighted / weightSum;
79
+ const score = Math.round(normalized * 100);
80
+ return { score, components, weights };
81
+ }
82
+ /** Convenience when only review stats are available (early bootstrap). */
83
+ export function trustScoreFromReviewAverage(actorType, averageRating, totalRatings) {
84
+ const bayesian = bayesianAverage(averageRating, totalRatings);
85
+ const result = computeTrustScoreFromInputs(actorType, {
86
+ bayesianReviewScore: bayesian,
87
+ completedVolume: totalRatings ?? 0,
88
+ cancellationRate: 0,
89
+ responseRate: 1,
90
+ identityVerified: false,
91
+ accountAgeDays: 30,
92
+ disputeRate: 0,
93
+ repeatCustomerRate: 0,
94
+ verifiedAttendanceRate: 0,
95
+ verifiedCompletionRate: 0,
96
+ });
97
+ return result.score;
98
+ }
99
+ //# sourceMappingURL=trustScoreUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustScoreUtils.js","sourceRoot":"","sources":["../../src/utils/trustScoreUtils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "30.293.0",
3
+ "version": "30.295.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -91,6 +91,7 @@ model BashFeedPost {
91
91
  saves BashFeedSave[]
92
92
  reposts BashFeedRepost[]
93
93
  helpfulVotes ReviewHelpfulVote[]
94
+ serviceReviews ServiceReview[]
94
95
 
95
96
  @@unique([userId, repostOfPostId])
96
97
  @@index([bashId])
@@ -345,6 +346,8 @@ model CompetitionPrizeOffer {
345
346
  sponsorLabel String?
346
347
  sourceServiceId String?
347
348
  sourceSku String?
349
+ /// Existing prize slot the offerer wants to sponsor (optional)
350
+ targetPrizeId String?
348
351
  hostNote String?
349
352
  createdAt DateTime @default(now())
350
353
  updatedAt DateTime @updatedAt
@@ -352,10 +355,12 @@ model CompetitionPrizeOffer {
352
355
  competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
353
356
  offeredBy User @relation("CompetitionPrizeOffersSubmitted", fields: [offeredByUserId], references: [id], onDelete: Cascade)
354
357
  sourceService Service? @relation("CompetitionPrizeOfferSourceService", fields: [sourceServiceId], references: [id], onDelete: SetNull)
358
+ targetPrize Prize? @relation("CompetitionPrizeOfferTarget", fields: [targetPrizeId], references: [id], onDelete: SetNull)
355
359
 
356
360
  @@index([competitionId])
357
361
  @@index([offeredByUserId])
358
362
  @@index([status])
363
+ @@index([targetPrizeId])
359
364
  }
360
365
 
361
366
  model CompetitionParticipant {
@@ -969,6 +974,8 @@ model BashEvent {
969
974
  sentReminders SentReminder[]
970
975
  recurrence Recurrence?
971
976
  reviews Review[]
977
+ serviceReviews ServiceReview[]
978
+ providerHostReviews ProviderHostReview[]
972
979
  serviceBookings ServiceBooking[]
973
980
  sponsorBookingRequests SponsorBookingRequest[] @relation("SponsorBookingEvent")
974
981
  sponsorships SponsoredEvent[]
@@ -1963,15 +1970,23 @@ enum BackgroundEffect {
1963
1970
  Starfield
1964
1971
  }
1965
1972
 
1973
+ enum EventDiscussionTopic {
1974
+ General
1975
+ Icebreaker
1976
+ }
1977
+
1966
1978
  model EventDiscussionComment {
1967
- id String @id @default(cuid())
1979
+ id String @id @default(cuid())
1968
1980
  bashEventId String
1969
1981
  authorId String
1970
- body String @db.Text
1982
+ body String @db.Text
1971
1983
  mentionedUserIds String[] // User IDs mentioned in the comment
1972
1984
  parentId String? // For nested replies (mirrors BashFeedComment)
1973
- createdAt DateTime @default(now())
1974
- updatedAt DateTime @updatedAt
1985
+ topic EventDiscussionTopic @default(General)
1986
+ /// Snapshot of the icebreaker prompt this answer belongs to (Icebreaker topic only).
1987
+ icebreakerPrompt String? @db.Text
1988
+ createdAt DateTime @default(now())
1989
+ updatedAt DateTime @updatedAt
1975
1990
 
1976
1991
  bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
1977
1992
  author User @relation("EventDiscussionComments", fields: [authorId], references: [id], onDelete: Cascade)
@@ -1982,6 +1997,7 @@ model EventDiscussionComment {
1982
1997
  @@index([bashEventId])
1983
1998
  @@index([authorId])
1984
1999
  @@index([parentId])
2000
+ @@index([bashEventId, topic, icebreakerPrompt])
1985
2001
  }
1986
2002
 
1987
2003
  model EventDiscussionCommentLike {
@@ -2716,7 +2732,7 @@ model Recurrence {
2716
2732
  repeatYearlyDate DateTime?
2717
2733
  // Per-occurrence exceptions for the computed series (no occurrence rows are materialized):
2718
2734
  // dates the host removed from the series (matched against the generated occurrence start instants).
2719
- excludedDates DateTime[]
2735
+ excludedDates DateTime[] @default([])
2720
2736
  // Per-occurrence date/time moves: JSON map of original-occurrence ISO start -> { startDateTime, endDateTime? }.
2721
2737
  occurrenceOverrides Json?
2722
2738
  createdAt DateTime @default(now())
@@ -2912,6 +2928,8 @@ model Prize {
2912
2928
  winnerUserId String?
2913
2929
  wonAt DateTime? // When the prize was awarded
2914
2930
  claimedAt DateTime? // When the prize was claimed
2931
+ /// Host-set amount sponsors pay to fund this prize slot (cents). Shown when seeking prize contributions.
2932
+ sponsorCostCents Int?
2915
2933
  /// Platform user who donated this prize (optional)
2916
2934
  contributorUserId String?
2917
2935
  /// Sponsor marketplace service the prize was picked from (optional)
@@ -2944,6 +2962,7 @@ model Prize {
2944
2962
  winner User? @relation(fields: [winnerUserId], references: [id], onDelete: Cascade)
2945
2963
  contributor User? @relation("PrizeContributor", fields: [contributorUserId], references: [id], onDelete: SetNull)
2946
2964
  sourceService Service? @relation("PrizeSourceService", fields: [sourceServiceId], references: [id], onDelete: SetNull)
2965
+ sponsorshipOffers CompetitionPrizeOffer[] @relation("CompetitionPrizeOfferTarget")
2947
2966
  bashPointsTransaction BashCreditTransaction? @relation(fields: [bashPointsTransactionId], references: [id], onDelete: SetNull)
2948
2967
 
2949
2968
  @@index([competitionId])
@@ -2955,11 +2974,22 @@ model Prize {
2955
2974
 
2956
2975
  model Review {
2957
2976
  id String @id @default(cuid())
2977
+ /// Overall rating (1–5). Headline aggregate uses this field.
2958
2978
  rating Int
2959
2979
  creatorId String
2960
2980
  bashEventId String
2961
2981
  createdAt DateTime @default(now())
2962
2982
  updatedAt DateTime @updatedAt
2983
+ /// Optional multi-dimensional event ratings (Overall required via rating).
2984
+ organizationRating Int?
2985
+ atmosphereRating Int?
2986
+ valueRating Int?
2987
+ safetyRating Int?
2988
+ wouldAttendAgain Boolean?
2989
+ /// Trust badges stamped at write time from platform verification checks.
2990
+ verifiedBooking Boolean @default(false)
2991
+ verifiedAttendance Boolean @default(false)
2992
+ verifiedServiceCompletion Boolean @default(false)
2963
2993
  /// Public reply from the event host (service provider) to the reviewer.
2964
2994
  hostReplyText String? @db.Text
2965
2995
  hostReplyAt DateTime?
@@ -2971,22 +3001,93 @@ model Review {
2971
3001
  @@unique([creatorId, bashEventId])
2972
3002
  }
2973
3003
 
3004
+ /// Bash-native review of a service listing at a specific event (attendee or host).
3005
+ model ServiceReview {
3006
+ id String @id @default(cuid())
3007
+ creatorId String
3008
+ serviceId String
3009
+ bashEventId String
3010
+ reviewerRole ServiceReviewerRole
3011
+ overallRating Int
3012
+ communicationRating Int?
3013
+ professionalismRating Int?
3014
+ qualityRating Int?
3015
+ wouldHireAgain Boolean?
3016
+ verifiedBooking Boolean @default(false)
3017
+ verifiedAttendance Boolean @default(false)
3018
+ verifiedServiceCompletion Boolean @default(false)
3019
+ comment String? @db.Text
3020
+ providerReplyText String? @db.Text
3021
+ providerReplyAt DateTime?
3022
+ /// Null until blind host↔provider window reveals (attendee reviews publish immediately).
3023
+ publishedAt DateTime?
3024
+ feedPostId String?
3025
+ createdAt DateTime @default(now())
3026
+ updatedAt DateTime @updatedAt
3027
+
3028
+ creator User @relation("ServiceReviewsCreated", fields: [creatorId], references: [id], onDelete: Cascade)
3029
+ service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
3030
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
3031
+ feedPost BashFeedPost? @relation(fields: [feedPostId], references: [id], onDelete: SetNull)
3032
+ helpfulVotes ReviewHelpfulVote[]
3033
+
3034
+ @@unique([creatorId, serviceId, bashEventId])
3035
+ @@index([serviceId, publishedAt])
3036
+ @@index([bashEventId])
3037
+ }
3038
+
3039
+ /// Provider reviews the host after a confirmed booking (blind mutual window with host ServiceReview).
3040
+ model ProviderHostReview {
3041
+ id String @id @default(cuid())
3042
+ creatorId String
3043
+ hostUserId String
3044
+ serviceId String
3045
+ bashEventId String
3046
+ overallRating Int
3047
+ communicationRating Int?
3048
+ professionalismRating Int?
3049
+ qualityRating Int?
3050
+ wouldWorkAgain Boolean?
3051
+ verifiedBooking Boolean @default(false)
3052
+ verifiedServiceCompletion Boolean @default(false)
3053
+ comment String? @db.Text
3054
+ publishedAt DateTime?
3055
+ createdAt DateTime @default(now())
3056
+ updatedAt DateTime @updatedAt
3057
+
3058
+ creator User @relation("ProviderHostReviewsCreated", fields: [creatorId], references: [id], onDelete: Cascade)
3059
+ host User @relation("ProviderHostReviewsReceived", fields: [hostUserId], references: [id], onDelete: Cascade)
3060
+ service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
3061
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
3062
+
3063
+ @@unique([creatorId, bashEventId, serviceId])
3064
+ @@index([hostUserId, publishedAt])
3065
+ }
3066
+
3067
+ enum ServiceReviewerRole {
3068
+ Attendee
3069
+ Host
3070
+ }
3071
+
2974
3072
  /// Profile-only helpfulness votes on host reviews (Review rows or BashFeed posts shown as reviews).
2975
3073
  model ReviewHelpfulVote {
2976
- id String @id @default(cuid())
2977
- userId String
2978
- reviewId String?
2979
- feedPostId String?
2980
- isHelpful Boolean
2981
- createdAt DateTime @default(now())
2982
- updatedAt DateTime @updatedAt
3074
+ id String @id @default(cuid())
3075
+ userId String
3076
+ reviewId String?
3077
+ feedPostId String?
3078
+ serviceReviewId String?
3079
+ isHelpful Boolean
3080
+ createdAt DateTime @default(now())
3081
+ updatedAt DateTime @updatedAt
2983
3082
 
2984
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2985
- review Review? @relation(fields: [reviewId], references: [id], onDelete: Cascade)
2986
- feedPost BashFeedPost? @relation(fields: [feedPostId], references: [id], onDelete: Cascade)
3083
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
3084
+ review Review? @relation(fields: [reviewId], references: [id], onDelete: Cascade)
3085
+ feedPost BashFeedPost? @relation(fields: [feedPostId], references: [id], onDelete: Cascade)
3086
+ serviceReview ServiceReview? @relation(fields: [serviceReviewId], references: [id], onDelete: Cascade)
2987
3087
 
2988
3088
  @@index([reviewId])
2989
3089
  @@index([feedPostId])
3090
+ @@index([serviceReviewId])
2990
3091
  @@index([userId])
2991
3092
  }
2992
3093
 
@@ -2997,6 +3098,9 @@ model GoogleReview {
2997
3098
  text String?
2998
3099
  createdAt DateTime @default(now())
2999
3100
  serviceId String
3101
+ service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
3102
+
3103
+ @@index([serviceId])
3000
3104
  }
3001
3105
 
3002
3106
  model Session {
@@ -3122,6 +3226,10 @@ model User {
3122
3226
  status UserStatus
3123
3227
  hostRating Float?
3124
3228
  totalRatings Int?
3229
+ /// Secondary composite trust signal (0–100) for host profile detail.
3230
+ trustScore Int?
3231
+ trustScoreUpdatedAt DateTime?
3232
+ verifiedBookingsCount Int @default(0)
3125
3233
  totalEventsHosted Int? @default(0) // Total published/completed events hosted
3126
3234
  totalAttendeesHosted Int? @default(0) // Total checked-in attendees across all events
3127
3235
  totalBookingsAccepted Int? @default(0) // Service provider stat
@@ -3315,6 +3423,9 @@ model User {
3315
3423
  remindersAssignedToMe Reminder[] @relation("RemindersAssignedToMe")
3316
3424
  sentReminders SentReminder[]
3317
3425
  reviews Review[]
3426
+ serviceReviewsCreated ServiceReview[] @relation("ServiceReviewsCreated")
3427
+ providerHostReviewsCreated ProviderHostReview[] @relation("ProviderHostReviewsCreated")
3428
+ providerHostReviewsReceived ProviderHostReview[] @relation("ProviderHostReviewsReceived")
3318
3429
  firstFreeListingId String?
3319
3430
  createdServices Service[] @relation("CreatedService")
3320
3431
  ownedServices Service[] @relation("OwnedService")
@@ -3996,6 +4107,10 @@ model Service {
3996
4107
  externalReviewSource String?
3997
4108
  externalRating Float?
3998
4109
  externalReviewCount Int?
4110
+ /// Secondary composite trust signal (0–100) for detail pages and ranking.
4111
+ trustScore Int?
4112
+ trustScoreUpdatedAt DateTime?
4113
+ verifiedCompletionsCount Int @default(0)
3999
4114
  stripeAccountId String?
4000
4115
  paymentAccountId String?
4001
4116
  targetAudienceId String? @unique
@@ -4081,6 +4196,9 @@ model Service {
4081
4196
  competitionPrizeOffersSourced CompetitionPrizeOffer[] @relation("CompetitionPrizeOfferSourceService")
4082
4197
  venueMatchCandidates VenueMatchCandidate[]
4083
4198
  supplyLeadConversions VenueSupplyLead[] @relation("SupplyLeadConvertedService")
4199
+ googleReviews GoogleReview[]
4200
+ serviceReviews ServiceReview[]
4201
+ providerHostReviews ProviderHostReview[]
4084
4202
 
4085
4203
  @@index([serviceListingStripeSubscriptionId])
4086
4204
  @@index([paymentAccountId])
@@ -44,6 +44,10 @@ describe("bashSectionTitleForBashType", () => {
44
44
  it("'Trending' → 'Trending'", () => {
45
45
  expect(bashSectionTitleForBashType("Trending")).toBe("Trending");
46
46
  });
47
+
48
+ it("'Ideas' → 'Ideas'", () => {
49
+ expect(bashSectionTitleForBashType("Ideas")).toBe("Ideas");
50
+ });
47
51
  });
48
52
 
49
53
  describe("BashEventType enum values get pluralized", () => {
@@ -1471,6 +1471,7 @@ export const INCOME_RANGE_VALUES: ReadonlyArray<IncomeRange> = [
1471
1471
 
1472
1472
  /** Homepage / discovery section labels for synthetic rows (not `BashEventType` enum values). */
1473
1473
  const SYNTHETIC_BASH_SECTION_TITLE: Record<string, string> = {
1474
+ Ideas: "Ideas",
1474
1475
  Featured: "Featured",
1475
1476
  Trending: "Trending",
1476
1477
  };
@@ -1817,11 +1818,19 @@ export interface UserTemporaryBadge {
1817
1818
 
1818
1819
  export interface Review {
1819
1820
  id: string;
1820
- rating: number; // 1-5 stars
1821
+ rating: number; // 1-5 stars (Overall)
1821
1822
  creatorId: string;
1822
1823
  bashEventId: string;
1823
1824
  createdAt: string;
1824
1825
  updatedAt: string;
1826
+ organizationRating?: number | null;
1827
+ atmosphereRating?: number | null;
1828
+ valueRating?: number | null;
1829
+ safetyRating?: number | null;
1830
+ wouldAttendAgain?: boolean | null;
1831
+ verifiedBooking?: boolean;
1832
+ verifiedAttendance?: boolean;
1833
+ verifiedServiceCompletion?: boolean;
1825
1834
  comments?: ReviewComment[];
1826
1835
  }
1827
1836
 
@@ -1853,10 +1862,83 @@ export interface ReviewWithUser extends Review {
1853
1862
 
1854
1863
  export interface ReviewWithUserAndEvent extends ReviewWithUser, ReviewWithEvent {}
1855
1864
 
1865
+ /** Wizard / API shape for BashEvent.itinerary JSON (see prisma BashEvent.itinerary). */
1866
+ export interface ItineraryItem {
1867
+ id: string;
1868
+ title: string;
1869
+ startTime: string;
1870
+ endTime: string;
1871
+ description?: string;
1872
+ }
1873
+
1856
1874
  export interface NewReview {
1857
1875
  rating: number;
1858
1876
  bashEventId: string;
1859
1877
  commentContent?: string;
1878
+ organizationRating?: number;
1879
+ atmosphereRating?: number;
1880
+ valueRating?: number;
1881
+ safetyRating?: number;
1882
+ wouldAttendAgain?: boolean;
1883
+ }
1884
+
1885
+ export interface ServiceReviewDimensions {
1886
+ communicationRating?: number | null;
1887
+ professionalismRating?: number | null;
1888
+ qualityRating?: number | null;
1889
+ wouldHireAgain?: boolean | null;
1890
+ }
1891
+
1892
+ export interface NewServiceReview extends ServiceReviewDimensions {
1893
+ overallRating: number;
1894
+ bashEventId: string;
1895
+ comment?: string;
1896
+ createPost?: boolean;
1897
+ postContent?: string;
1898
+ }
1899
+
1900
+ export interface ServiceReviewPublic {
1901
+ id: string;
1902
+ overallRating: number;
1903
+ reviewerRole: "Attendee" | "Host";
1904
+ comment?: string | null;
1905
+ createdAt: string;
1906
+ publishedAt?: string | null;
1907
+ verifiedBooking: boolean;
1908
+ verifiedAttendance: boolean;
1909
+ verifiedServiceCompletion: boolean;
1910
+ communicationRating?: number | null;
1911
+ professionalismRating?: number | null;
1912
+ qualityRating?: number | null;
1913
+ wouldHireAgain?: boolean | null;
1914
+ providerReplyText?: string | null;
1915
+ providerReplyAt?: string | null;
1916
+ feedPostId?: string | null;
1917
+ creator?: {
1918
+ id: string;
1919
+ givenName?: string | null;
1920
+ familyName?: string | null;
1921
+ image?: string | null;
1922
+ };
1923
+ bashEvent?: {
1924
+ id: string;
1925
+ title: string;
1926
+ startDateTime: string;
1927
+ };
1928
+ }
1929
+
1930
+ export interface ServiceReviewStats {
1931
+ overallAverage: number | null;
1932
+ totalReviews: number;
1933
+ dimensionAverages: import("./utils/reviewDimensionUtils.js").ReviewDimensionAverages;
1934
+ googleAverage?: number | null;
1935
+ googleCount?: number;
1936
+ trustScore?: number | null;
1937
+ }
1938
+
1939
+ export interface TrustScoreBreakdownPublic {
1940
+ score: number | null;
1941
+ components: Record<string, number>;
1860
1942
  }
1861
1943
 
1862
1944
  export interface ReviewStats {
@@ -1869,6 +1951,8 @@ export interface ReviewStats {
1869
1951
  4: number;
1870
1952
  5: number;
1871
1953
  };
1954
+ dimensionAverages?: import("./utils/reviewDimensionUtils.js").ReviewDimensionAverages;
1955
+ trustScore?: number | null;
1872
1956
  }
1873
1957
 
1874
1958
  /** Helpful / Not helpful tallies on profile review cards (API: host-reviews). */
@@ -287,6 +287,7 @@ export interface CompetitionPrizeOfferExt {
287
287
  sponsorLabel?: string | null;
288
288
  sourceServiceId?: string | null;
289
289
  sourceSku?: string | null;
290
+ targetPrizeId?: string | null;
290
291
  hostNote?: string | null;
291
292
  createdAt: Date | string;
292
293
  respondedAt?: Date | string | null;
@@ -296,6 +297,12 @@ export interface CompetitionPrizeOfferExt {
296
297
  serviceName: string | null;
297
298
  coverPhoto: string | null;
298
299
  } | null;
300
+ targetPrize?: {
301
+ id: string;
302
+ name: string;
303
+ placeName: string | null;
304
+ sponsorCostCents: number | null;
305
+ } | null;
299
306
  }
300
307
 
301
308
  /** Competition row + relations; Override keeps new columns visible before consumers refresh Prisma. */
package/src/index.ts CHANGED
@@ -407,6 +407,8 @@ export * from "./utils/service/serviceRateUtils.js";
407
407
  export * from "./utils/serviceAvailabilityDisplay.js";
408
408
  export * from "./utils/entertainmentServiceTypeOptions.js";
409
409
  export * from "./utils/reputationUtils.js";
410
+ export * from "./utils/reviewDimensionUtils.js";
411
+ export * from "./utils/trustScoreUtils.js";
410
412
  export * from "./utils/stripeAccountUtils.js";
411
413
  export * from "./utils/userUtils.js";
412
414
  export * from "./utils/venueTypeFormatter.js";
@@ -219,6 +219,17 @@ export const BASHPOINTS_REWARDS: Record<MembershipTier, {
219
219
  },
220
220
  } as const;
221
221
 
222
+ /** Referral tier qualification thresholds — single source of truth for grant + stats UI. */
223
+ export const AMBASHADOR_QUALIFICATION = {
224
+ freeSignups: 1,
225
+ paidSignups: 1,
226
+ } as const;
227
+
228
+ export const INSIDER_QUALIFICATION = {
229
+ freeSignups: 100,
230
+ paidSignups: 10,
231
+ } as const;
232
+
222
233
  /**
223
234
  * BashPoints system rules and limits
224
235
  *
@@ -233,7 +244,7 @@ export const BASHPOINTS_CONFIG = {
233
244
  paymentVerificationBonus: 500,
234
245
 
235
246
  // Tier achievement bonuses (one-time when user reaches each tier)
236
- ambashadorAchievementBonus: 500, // Ambashador: 3 free or 1 paid referral
247
+ ambashadorAchievementBonus: 500, // Ambashador: 1 free or 1 paid referral
237
248
  scoutAchievementBonus: 250, // Scout: first service profile referral
238
249
  insiderAchievementBonus: 1000, // Bash Insider: 10 paid or 100 free referrals
239
250
  agentAchievementBonus: 500, // Agent: 5+ service profile referrals