@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.
- package/dist/__tests__/bashSectionTitleForBashType.test.js +3 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
- package/dist/definitions.d.ts +77 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +1 -0
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +7 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/membershipDefinitions.d.ts +9 -0
- package/dist/membershipDefinitions.d.ts.map +1 -1
- package/dist/membershipDefinitions.js +10 -1
- package/dist/membershipDefinitions.js.map +1 -1
- package/dist/utils/__tests__/reputationUtils.serviceListing.test.js +18 -0
- package/dist/utils/__tests__/reputationUtils.serviceListing.test.js.map +1 -1
- package/dist/utils/__tests__/reviewDimensionUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/reviewDimensionUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/reviewDimensionUtils.test.js +48 -0
- package/dist/utils/__tests__/reviewDimensionUtils.test.js.map +1 -0
- package/dist/utils/__tests__/trustScoreUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/trustScoreUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/trustScoreUtils.test.js +101 -0
- package/dist/utils/__tests__/trustScoreUtils.test.js.map +1 -0
- package/dist/utils/reputationUtils.d.ts.map +1 -1
- package/dist/utils/reputationUtils.js +7 -1
- package/dist/utils/reputationUtils.js.map +1 -1
- package/dist/utils/reviewDimensionUtils.d.ts +33 -0
- package/dist/utils/reviewDimensionUtils.d.ts.map +1 -0
- package/dist/utils/reviewDimensionUtils.js +35 -0
- package/dist/utils/reviewDimensionUtils.js.map +1 -0
- package/dist/utils/service/__tests__/serviceListingMediaUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/serviceListingMediaUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/serviceListingMediaUtils.test.js +33 -0
- package/dist/utils/service/__tests__/serviceListingMediaUtils.test.js.map +1 -0
- package/dist/utils/service/serviceListingMediaUtils.d.ts +22 -0
- package/dist/utils/service/serviceListingMediaUtils.d.ts.map +1 -0
- package/dist/utils/service/serviceListingMediaUtils.js +40 -0
- package/dist/utils/service/serviceListingMediaUtils.js.map +1 -0
- package/dist/utils/trustScoreUtils.d.ts +25 -0
- package/dist/utils/trustScoreUtils.d.ts.map +1 -0
- package/dist/utils/trustScoreUtils.js +99 -0
- package/dist/utils/trustScoreUtils.js.map +1 -0
- package/package.json +1 -1
- package/prisma/schema.prisma +133 -15
- package/src/__tests__/bashSectionTitleForBashType.test.ts +4 -0
- package/src/definitions.ts +85 -1
- package/src/extendedSchemas.ts +7 -0
- package/src/index.ts +3 -0
- package/src/membershipDefinitions.ts +12 -1
- package/src/utils/__tests__/reputationUtils.serviceListing.test.ts +24 -0
- package/src/utils/__tests__/reviewDimensionUtils.test.ts +65 -0
- package/src/utils/__tests__/trustScoreUtils.test.ts +105 -0
- package/src/utils/reputationUtils.ts +7 -1
- package/src/utils/reviewDimensionUtils.ts +70 -0
- package/src/utils/service/__tests__/serviceListingMediaUtils.test.ts +44 -0
- package/src/utils/service/serviceListingMediaUtils.ts +62 -0
- 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 @@
|
|
|
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
package/prisma/schema.prisma
CHANGED
|
@@ -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
|
|
1979
|
+
id String @id @default(cuid())
|
|
1968
1980
|
bashEventId String
|
|
1969
1981
|
authorId String
|
|
1970
|
-
body String
|
|
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
|
-
|
|
1974
|
-
|
|
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
|
|
2977
|
-
userId
|
|
2978
|
-
reviewId
|
|
2979
|
-
feedPostId
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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
|
|
2985
|
-
review
|
|
2986
|
-
feedPost
|
|
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", () => {
|