@bash-app/bash-common 30.293.0 → 30.297.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 (61) 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 +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -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/service/__tests__/serviceListingMediaUtils.test.d.ts +2 -0
  36. package/dist/utils/service/__tests__/serviceListingMediaUtils.test.d.ts.map +1 -0
  37. package/dist/utils/service/__tests__/serviceListingMediaUtils.test.js +33 -0
  38. package/dist/utils/service/__tests__/serviceListingMediaUtils.test.js.map +1 -0
  39. package/dist/utils/service/serviceListingMediaUtils.d.ts +22 -0
  40. package/dist/utils/service/serviceListingMediaUtils.d.ts.map +1 -0
  41. package/dist/utils/service/serviceListingMediaUtils.js +40 -0
  42. package/dist/utils/service/serviceListingMediaUtils.js.map +1 -0
  43. package/dist/utils/trustScoreUtils.d.ts +25 -0
  44. package/dist/utils/trustScoreUtils.d.ts.map +1 -0
  45. package/dist/utils/trustScoreUtils.js +99 -0
  46. package/dist/utils/trustScoreUtils.js.map +1 -0
  47. package/package.json +1 -1
  48. package/prisma/schema.prisma +133 -15
  49. package/src/__tests__/bashSectionTitleForBashType.test.ts +4 -0
  50. package/src/definitions.ts +85 -1
  51. package/src/extendedSchemas.ts +7 -0
  52. package/src/index.ts +3 -0
  53. package/src/membershipDefinitions.ts +12 -1
  54. package/src/utils/__tests__/reputationUtils.serviceListing.test.ts +24 -0
  55. package/src/utils/__tests__/reviewDimensionUtils.test.ts +65 -0
  56. package/src/utils/__tests__/trustScoreUtils.test.ts +105 -0
  57. package/src/utils/reputationUtils.ts +7 -1
  58. package/src/utils/reviewDimensionUtils.ts +70 -0
  59. package/src/utils/service/__tests__/serviceListingMediaUtils.test.ts +44 -0
  60. package/src/utils/service/serviceListingMediaUtils.ts +62 -0
  61. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=serviceListingMediaUtils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceListingMediaUtils.test.d.ts","sourceRoot":"","sources":["../../../../src/utils/service/__tests__/serviceListingMediaUtils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { MediaType } from "@prisma/client";
3
+ import { filterServiceListingMedia, resolveServiceListingImage, } from "../serviceListingMediaUtils.js";
4
+ describe("serviceListingMediaUtils", () => {
5
+ test("resolveServiceListingImage prefers coverPhoto when gallery has videos", () => {
6
+ expect(resolveServiceListingImage({
7
+ coverPhoto: "https://cdn.example.com/cover.jpg",
8
+ media: [
9
+ { type: MediaType.Video, url: "https://cdn.example.com/live.mp4" },
10
+ ],
11
+ })).toBe("https://cdn.example.com/cover.jpg");
12
+ });
13
+ test("filterServiceListingMedia excludes supporting videos", () => {
14
+ const filtered = filterServiceListingMedia({
15
+ media: [
16
+ { type: MediaType.Video, url: "https://cdn.example.com/live.mp4" },
17
+ { type: MediaType.Image, url: "https://cdn.example.com/still.jpg" },
18
+ ],
19
+ });
20
+ expect(filtered.map((item) => item.url)).toEqual([
21
+ "https://cdn.example.com/still.jpg",
22
+ ]);
23
+ });
24
+ test("resolveServiceListingImage returns undefined when only videos exist", () => {
25
+ expect(resolveServiceListingImage({
26
+ coverPhoto: null,
27
+ media: [
28
+ { type: MediaType.Video, url: "https://cdn.example.com/live.mp4" },
29
+ ],
30
+ })).toBeUndefined();
31
+ });
32
+ });
33
+ //# sourceMappingURL=serviceListingMediaUtils.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceListingMediaUtils.test.js","sourceRoot":"","sources":["../../../../src/utils/service/__tests__/serviceListingMediaUtils.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EACL,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AAExC,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,IAAI,CAAC,uEAAuE,EAAE,GAAG,EAAE;QACjF,MAAM,CACJ,0BAA0B,CAAC;YACzB,UAAU,EAAE,mCAAmC;YAC/C,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,kCAAkC,EAAE;aACnE;SAC0C,CAAC,CAC/C,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAChE,MAAM,QAAQ,GAAG,yBAAyB,CAAC;YACzC,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,kCAAkC,EAAE;gBAClE,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,mCAAmC,EAAE;aACpE;SAC2B,CAAC,CAAC;QAEhC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;YAC/C,mCAAmC;SACpC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC/E,MAAM,CACJ,0BAA0B,CAAC;YACzB,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,kCAAkC,EAAE;aACnE;SAC0C,CAAC,CAC/C,CAAC,aAAa,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type Media } from "@prisma/client";
2
+ import type { ServiceExt } from "../../extendedSchemas.js";
3
+ export type ServiceListingOwner = {
4
+ uploadedImage?: string | null;
5
+ image?: string | null;
6
+ } | null | undefined;
7
+ /** True when a URL is the service owner's personal profile photo, not listing media. */
8
+ export declare function isOwnerProfileListingImageUrl(url: string, owner?: ServiceListingOwner): boolean;
9
+ /** Gallery images suitable for public service cards — excludes videos and profile avatars. */
10
+ export declare function filterServiceListingMedia(service: Pick<ServiceExt, "media"> & {
11
+ owner?: ServiceListingOwner;
12
+ }): Media[];
13
+ /**
14
+ * Resolve the still image for browse cards and list heroes.
15
+ *
16
+ * Prefers `coverPhoto`, then the first gallery image. Supporting videos in
17
+ * `media` are never used — cards must not autoplay performance clips.
18
+ */
19
+ export declare function resolveServiceListingImage(service: Pick<ServiceExt, "coverPhoto" | "media"> & {
20
+ owner?: ServiceListingOwner;
21
+ }): string | undefined;
22
+ //# sourceMappingURL=serviceListingMediaUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceListingMediaUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/service/serviceListingMediaUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GAAG,IAAI,GAAG,SAAS,CAAC;AASrB,wFAAwF;AACxF,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,mBAAmB,GAC1B,OAAO,CAOT;AAED,8FAA8F;AAC9F,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAE,GACnE,KAAK,EAAE,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,YAAY,GAAG,OAAO,CAAC,GAAG;IAClD,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,GACA,MAAM,GAAG,SAAS,CAQpB"}
@@ -0,0 +1,40 @@
1
+ import { MediaType } from "@prisma/client";
2
+ function normalizeListingImageUrl(url) {
3
+ if (!url?.trim())
4
+ return null;
5
+ return url.trim();
6
+ }
7
+ /** True when a URL is the service owner's personal profile photo, not listing media. */
8
+ export function isOwnerProfileListingImageUrl(url, owner) {
9
+ if (!owner)
10
+ return false;
11
+ const normalized = url.trim();
12
+ const profileUrls = [owner.uploadedImage, owner.image]
13
+ .map(normalizeListingImageUrl)
14
+ .filter((value) => Boolean(value));
15
+ return profileUrls.some((profileUrl) => profileUrl === normalized);
16
+ }
17
+ /** Gallery images suitable for public service cards — excludes videos and profile avatars. */
18
+ export function filterServiceListingMedia(service) {
19
+ const media = Array.isArray(service.media) ? service.media : [];
20
+ return media.filter((item) => item?.type === MediaType.Image &&
21
+ typeof item.url === "string" &&
22
+ item.url.trim().length > 0 &&
23
+ !item.url.startsWith("https://maps.googleapis.com/") &&
24
+ !isOwnerProfileListingImageUrl(item.url, service.owner));
25
+ }
26
+ /**
27
+ * Resolve the still image for browse cards and list heroes.
28
+ *
29
+ * Prefers `coverPhoto`, then the first gallery image. Supporting videos in
30
+ * `media` are never used — cards must not autoplay performance clips.
31
+ */
32
+ export function resolveServiceListingImage(service) {
33
+ const cover = normalizeListingImageUrl(service.coverPhoto);
34
+ if (cover && !isOwnerProfileListingImageUrl(cover, service.owner)) {
35
+ return cover;
36
+ }
37
+ const firstImage = filterServiceListingMedia(service)[0];
38
+ return firstImage?.url?.trim() || undefined;
39
+ }
40
+ //# sourceMappingURL=serviceListingMediaUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceListingMediaUtils.js","sourceRoot":"","sources":["../../../src/utils/service/serviceListingMediaUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAc,MAAM,gBAAgB,CAAC;AAQvD,SAAS,wBAAwB,CAC/B,GAA8B;IAE9B,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,6BAA6B,CAC3C,GAAW,EACX,KAA2B;IAE3B,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC;SACnD,GAAG,CAAC,wBAAwB,CAAC;SAC7B,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACtD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;AACrE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,yBAAyB,CACvC,OAAoE;IAEpE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,OAAO,KAAK,CAAC,MAAM,CACjB,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,KAAK;QAC9B,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ;QAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAC1B,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,8BAA8B,CAAC;QACpD,CAAC,6BAA6B,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAC1D,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAEC;IAED,MAAM,KAAK,GAAG,wBAAwB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3D,IAAI,KAAK,IAAI,CAAC,6BAA6B,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,UAAU,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,OAAO,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;AAC9C,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.297.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", () => {