@bash-app/bash-common 30.225.0 → 30.227.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__/agePolicy.test.d.ts +2 -0
- package/dist/__tests__/agePolicy.test.d.ts.map +1 -0
- package/dist/__tests__/agePolicy.test.js +203 -0
- package/dist/__tests__/agePolicy.test.js.map +1 -0
- package/dist/__tests__/getDisplayGender.test.d.ts +2 -0
- package/dist/__tests__/getDisplayGender.test.d.ts.map +1 -0
- package/dist/__tests__/getDisplayGender.test.js +34 -0
- package/dist/__tests__/getDisplayGender.test.js.map +1 -0
- package/dist/__tests__/statusEnums.test.d.ts +2 -0
- package/dist/__tests__/statusEnums.test.d.ts.map +1 -0
- package/dist/__tests__/statusEnums.test.js +24 -0
- package/dist/__tests__/statusEnums.test.js.map +1 -0
- package/dist/aiApproval.d.ts +89 -0
- package/dist/aiApproval.d.ts.map +1 -0
- package/dist/aiApproval.js +82 -0
- package/dist/aiApproval.js.map +1 -0
- package/dist/definitions.d.ts +44 -2
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +106 -2
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +280 -105
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +34 -2
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/legalTemplates.d.ts +1 -1
- package/dist/legalTemplates.js +1 -1
- package/dist/utils/__tests__/badgeUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/badgeUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/badgeUtils.test.js +157 -0
- package/dist/utils/__tests__/badgeUtils.test.js.map +1 -0
- package/dist/utils/__tests__/bashPointsPaymentUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/bashPointsPaymentUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/bashPointsPaymentUtils.test.js +114 -0
- package/dist/utils/__tests__/bashPointsPaymentUtils.test.js.map +1 -0
- package/dist/utils/__tests__/contentFilterUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/contentFilterUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/contentFilterUtils.test.js +443 -0
- package/dist/utils/__tests__/contentFilterUtils.test.js.map +1 -0
- package/dist/utils/__tests__/displayDateUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/displayDateUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/displayDateUtils.test.js +105 -0
- package/dist/utils/__tests__/displayDateUtils.test.js.map +1 -0
- package/dist/utils/__tests__/flyerUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/flyerUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/flyerUtils.test.js +49 -0
- package/dist/utils/__tests__/flyerUtils.test.js.map +1 -0
- package/dist/utils/__tests__/getBackendHost.test.d.ts +2 -0
- package/dist/utils/__tests__/getBackendHost.test.d.ts.map +1 -0
- package/dist/utils/__tests__/getBackendHost.test.js +104 -0
- package/dist/utils/__tests__/getBackendHost.test.js.map +1 -0
- package/dist/utils/__tests__/getFrontendHost.test.d.ts +2 -0
- package/dist/utils/__tests__/getFrontendHost.test.d.ts.map +1 -0
- package/dist/utils/__tests__/getFrontendHost.test.js +207 -0
- package/dist/utils/__tests__/getFrontendHost.test.js.map +1 -0
- package/dist/utils/__tests__/promoCodesUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/promoCodesUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/promoCodesUtils.test.js +183 -0
- package/dist/utils/__tests__/promoCodesUtils.test.js.map +1 -0
- package/dist/utils/__tests__/qrCodeUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/qrCodeUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/qrCodeUtils.test.js +99 -0
- package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -0
- package/dist/utils/__tests__/stripeAccountUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/stripeAccountUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/stripeAccountUtils.test.js +75 -0
- package/dist/utils/__tests__/stripeAccountUtils.test.js.map +1 -0
- package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.d.ts +2 -0
- package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.d.ts.map +1 -0
- package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.js +233 -0
- package/dist/utils/blog/__tests__/getRelatedBlogPosts.test.js.map +1 -0
- package/dist/utils/blog/blogCommentDbUtils.d.ts +161 -0
- package/dist/utils/blog/blogCommentDbUtils.d.ts.map +1 -0
- package/dist/utils/blog/blogCommentDbUtils.js +111 -0
- package/dist/utils/blog/blogCommentDbUtils.js.map +1 -0
- package/dist/utils/blog/blogDbUtils.d.ts +10 -0
- package/dist/utils/blog/blogDbUtils.d.ts.map +1 -1
- package/dist/utils/blog/blogDbUtils.js +71 -0
- package/dist/utils/blog/blogDbUtils.js.map +1 -1
- package/dist/utils/flyerUtils.d.ts +42 -0
- package/dist/utils/flyerUtils.d.ts.map +1 -0
- package/dist/utils/flyerUtils.js +104 -0
- package/dist/utils/flyerUtils.js.map +1 -0
- package/dist/utils/urlUtils.d.ts +10 -0
- package/dist/utils/urlUtils.d.ts.map +1 -1
- package/dist/utils/urlUtils.js +24 -5
- package/dist/utils/urlUtils.js.map +1 -1
- package/dist/utils/userUtils.d.ts +32 -0
- package/dist/utils/userUtils.d.ts.map +1 -1
- package/dist/utils/userUtils.js +27 -0
- package/dist/utils/userUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +1196 -845
- package/src/__tests__/agePolicy.test.ts +257 -0
- package/src/__tests__/getDisplayGender.test.ts +50 -0
- package/src/__tests__/statusEnums.test.ts +36 -0
- package/src/aiApproval.ts +118 -0
- package/src/definitions.ts +138 -1
- package/src/extendedSchemas.ts +86 -3
- package/src/index.ts +15 -0
- package/src/legalTemplates.ts +1 -1
- package/src/utils/__tests__/badgeUtils.test.ts +191 -0
- package/src/utils/__tests__/bashPointsPaymentUtils.test.ts +153 -0
- package/src/utils/__tests__/contentFilterUtils.test.ts +547 -0
- package/src/utils/__tests__/displayDateUtils.test.ts +113 -0
- package/src/utils/__tests__/flyerUtils.test.ts +70 -0
- package/src/utils/__tests__/getBackendHost.test.ts +128 -0
- package/src/utils/__tests__/getFrontendHost.test.ts +232 -0
- package/src/utils/__tests__/promoCodesUtils.test.ts +237 -0
- package/src/utils/__tests__/qrCodeUtils.test.ts +143 -0
- package/src/utils/__tests__/stripeAccountUtils.test.ts +93 -0
- package/src/utils/blog/__tests__/getRelatedBlogPosts.test.ts +271 -0
- package/src/utils/blog/blogCommentDbUtils.ts +161 -0
- package/src/utils/blog/blogDbUtils.ts +95 -1
- package/src/utils/flyerUtils.ts +137 -0
- package/src/utils/urlUtils.ts +27 -5
- package/src/utils/userUtils.ts +40 -0
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts +0 -6
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts.map +0 -1
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js +0 -104
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js.map +0 -1
- package/dist/utils/service/serviceRateDBUtils.d.ts +0 -1
- package/dist/utils/service/serviceRateDBUtils.d.ts.map +0 -1
- package/dist/utils/service/serviceRateDBUtils.js +0 -159
- package/dist/utils/service/serviceRateDBUtils.js.map +0 -1
- package/prisma/migrations/add_bash_availability.sql +0 -53
- package/prisma/migrations/add_bashcash_pricing_to_ticket_tier.sql +0 -15
- package/prisma/migrations/add_bashpoints_purchase_tracking.sql +0 -33
- package/prisma/migrations/add_event_group_allow_auto_join.sql +0 -17
- package/prisma/migrations/add_group_member_status_not_going_maybe.sql +0 -5
- package/prisma/migrations/add_groups_and_momentum.sql +0 -135
- package/prisma/migrations/add_groups_momentum_phase0_4.sql +0 -200
- package/prisma/migrations/add_new_feature_tables.sql +0 -140
- package/prisma/migrations/add_pricing_type_enum.sql +0 -88
- package/prisma/migrations/add_tier_privacy_and_fee_handling.sql +0 -29
- package/prisma/migrations/diagnostic_bashcash_columns.sql +0 -157
- package/prisma/migrations/fix_bashcash_referral_code_schema_mismatch.sql +0 -81
- package/prisma/migrations/rename_bashcash_to_bashpoints.sql +0 -183
- package/prisma/migrations/rename_bashcredits_to_bashpoints.sql +0 -96
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgePolicy helpers — unit tests.
|
|
3
|
+
*
|
|
4
|
+
* `getAgeFromDob` and `meetsAgePolicy` are the source of truth for both:
|
|
5
|
+
* - the `GET /me/id-status` endpoint (which returns `verifiedAge`)
|
|
6
|
+
* - the checkout `IdRequirementGate` (which gates buy-button on min age)
|
|
7
|
+
*
|
|
8
|
+
* They must handle: missing input, invalid strings, the day-before-birthday
|
|
9
|
+
* edge case, and leap-day birthdays without throwing or off-by-one errors.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
AgePolicy,
|
|
13
|
+
AgePolicyLabel,
|
|
14
|
+
AgePolicyMinimumAge,
|
|
15
|
+
agePolicyToFilterLabel,
|
|
16
|
+
eventMatchesAgeFilter,
|
|
17
|
+
getAgeFromDob,
|
|
18
|
+
legacyAgeRangeMatchesFilter,
|
|
19
|
+
meetsAgePolicy,
|
|
20
|
+
} from "../definitions";
|
|
21
|
+
|
|
22
|
+
describe("AgePolicyLabel", () => {
|
|
23
|
+
it("maps every enum value to a non-empty display label", () => {
|
|
24
|
+
for (const policy of Object.values(AgePolicy)) {
|
|
25
|
+
expect(typeof AgePolicyLabel[policy]).toBe("string");
|
|
26
|
+
expect(AgePolicyLabel[policy].length).toBeGreaterThan(0);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("uses the marketing-friendly +/All Ages copy", () => {
|
|
31
|
+
expect(AgePolicyLabel[AgePolicy.AllAges]).toBe("All Ages");
|
|
32
|
+
expect(AgePolicyLabel[AgePolicy.EighteenPlus]).toBe("18+");
|
|
33
|
+
expect(AgePolicyLabel[AgePolicy.TwentyOnePlus]).toBe("21+");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("AgePolicyMinimumAge", () => {
|
|
38
|
+
it("returns null for unrestricted events", () => {
|
|
39
|
+
expect(AgePolicyMinimumAge[AgePolicy.AllAges]).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns the explicit numeric minimum for restricted events", () => {
|
|
43
|
+
expect(AgePolicyMinimumAge[AgePolicy.EighteenPlus]).toBe(18);
|
|
44
|
+
expect(AgePolicyMinimumAge[AgePolicy.TwentyOnePlus]).toBe(21);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("getAgeFromDob", () => {
|
|
49
|
+
/** Build an ISO date string offset by a number of years, months, and days from now. */
|
|
50
|
+
const isoFromOffset = (years: number, months = 0, days = 0): string => {
|
|
51
|
+
const d = new Date();
|
|
52
|
+
d.setFullYear(d.getFullYear() - years);
|
|
53
|
+
d.setMonth(d.getMonth() - months);
|
|
54
|
+
d.setDate(d.getDate() - days);
|
|
55
|
+
return d.toISOString();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it("returns null for null / undefined / empty input", () => {
|
|
59
|
+
expect(getAgeFromDob(null)).toBeNull();
|
|
60
|
+
expect(getAgeFromDob(undefined)).toBeNull();
|
|
61
|
+
expect(getAgeFromDob("")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns null when the date string cannot be parsed", () => {
|
|
65
|
+
expect(getAgeFromDob("not-a-date")).toBeNull();
|
|
66
|
+
expect(getAgeFromDob("2025-13-99")).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("computes whole-year age for a clearly-past birthday", () => {
|
|
70
|
+
// Born 25 years ago, 1 month ago — birthday already passed this year.
|
|
71
|
+
expect(getAgeFromDob(isoFromOffset(25, 1))).toBe(25);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("subtracts a year when birthday hasn't occurred yet this year", () => {
|
|
75
|
+
// Born 25 years ago + 1 month from now (birthday is next month).
|
|
76
|
+
const d = new Date();
|
|
77
|
+
d.setFullYear(d.getFullYear() - 25);
|
|
78
|
+
d.setMonth(d.getMonth() + 1);
|
|
79
|
+
expect(getAgeFromDob(d.toISOString())).toBe(24);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("treats today as 'birthday already happened' (not subtracting a year)", () => {
|
|
83
|
+
// Born exactly 30 years ago today.
|
|
84
|
+
const d = new Date();
|
|
85
|
+
d.setFullYear(d.getFullYear() - 30);
|
|
86
|
+
expect(getAgeFromDob(d.toISOString())).toBe(30);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("treats tomorrow's birthday as still being last year's age", () => {
|
|
90
|
+
// Born 30 years ago - 1 day (so birthday is tomorrow).
|
|
91
|
+
const d = new Date();
|
|
92
|
+
d.setFullYear(d.getFullYear() - 30);
|
|
93
|
+
d.setDate(d.getDate() + 1);
|
|
94
|
+
expect(getAgeFromDob(d.toISOString())).toBe(29);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("accepts a Date instance, not just a string", () => {
|
|
98
|
+
const d = new Date();
|
|
99
|
+
d.setFullYear(d.getFullYear() - 40);
|
|
100
|
+
d.setMonth(d.getMonth() - 2);
|
|
101
|
+
expect(getAgeFromDob(d)).toBe(40);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns 0 for a baby born earlier this year", () => {
|
|
105
|
+
// Born ~3 months ago — under 1 year old.
|
|
106
|
+
expect(getAgeFromDob(isoFromOffset(0, 3))).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("meetsAgePolicy", () => {
|
|
111
|
+
const yearsAgo = (n: number): string => {
|
|
112
|
+
const d = new Date();
|
|
113
|
+
d.setFullYear(d.getFullYear() - n);
|
|
114
|
+
// back off a day to guarantee the birthday has occurred this year
|
|
115
|
+
d.setDate(d.getDate() - 1);
|
|
116
|
+
return d.toISOString();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
it("All Ages always passes — even with no DOB", () => {
|
|
120
|
+
expect(meetsAgePolicy(AgePolicy.AllAges, null)).toBe(true);
|
|
121
|
+
expect(meetsAgePolicy(AgePolicy.AllAges, undefined)).toBe(true);
|
|
122
|
+
expect(meetsAgePolicy(AgePolicy.AllAges, yearsAgo(5))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("denies restricted policies when DOB is missing", () => {
|
|
126
|
+
expect(meetsAgePolicy(AgePolicy.EighteenPlus, null)).toBe(false);
|
|
127
|
+
expect(meetsAgePolicy(AgePolicy.TwentyOnePlus, undefined)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("18+ allows ages >= 18 and denies < 18", () => {
|
|
131
|
+
expect(meetsAgePolicy(AgePolicy.EighteenPlus, yearsAgo(17))).toBe(false);
|
|
132
|
+
expect(meetsAgePolicy(AgePolicy.EighteenPlus, yearsAgo(18))).toBe(true);
|
|
133
|
+
expect(meetsAgePolicy(AgePolicy.EighteenPlus, yearsAgo(40))).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("21+ allows ages >= 21 and denies under-21 (including 18-, 20-year-olds)", () => {
|
|
137
|
+
expect(meetsAgePolicy(AgePolicy.TwentyOnePlus, yearsAgo(20))).toBe(false);
|
|
138
|
+
expect(meetsAgePolicy(AgePolicy.TwentyOnePlus, yearsAgo(21))).toBe(true);
|
|
139
|
+
expect(meetsAgePolicy(AgePolicy.TwentyOnePlus, yearsAgo(50))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("denies on invalid DOB strings (defensive fail-closed)", () => {
|
|
143
|
+
expect(meetsAgePolicy(AgePolicy.EighteenPlus, "not-a-date")).toBe(false);
|
|
144
|
+
expect(meetsAgePolicy(AgePolicy.TwentyOnePlus, "")).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("agePolicyToFilterLabel", () => {
|
|
149
|
+
it("maps each enum to its discover-filter chip label", () => {
|
|
150
|
+
expect(agePolicyToFilterLabel(AgePolicy.AllAges)).toBe("All Ages");
|
|
151
|
+
expect(agePolicyToFilterLabel(AgePolicy.EighteenPlus)).toBe("18+");
|
|
152
|
+
expect(agePolicyToFilterLabel(AgePolicy.TwentyOnePlus)).toBe("21+");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("treats null / undefined (legacy events) as All Ages so they don't disappear from filters", () => {
|
|
156
|
+
expect(agePolicyToFilterLabel(null)).toBe("All Ages");
|
|
157
|
+
expect(agePolicyToFilterLabel(undefined)).toBe("All Ages");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("eventMatchesAgeFilter", () => {
|
|
162
|
+
it("matches everything when no labels are selected (no-op filter)", () => {
|
|
163
|
+
expect(eventMatchesAgeFilter(AgePolicy.AllAges, [])).toBe(true);
|
|
164
|
+
expect(eventMatchesAgeFilter(AgePolicy.TwentyOnePlus, [])).toBe(true);
|
|
165
|
+
expect(eventMatchesAgeFilter(null, [])).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("matches when the event's policy is in the selected labels", () => {
|
|
169
|
+
expect(eventMatchesAgeFilter(AgePolicy.EighteenPlus, ["18+"])).toBe(true);
|
|
170
|
+
expect(
|
|
171
|
+
eventMatchesAgeFilter(AgePolicy.TwentyOnePlus, ["21+", "All Ages"])
|
|
172
|
+
).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does not match when the event's policy is excluded by the selection", () => {
|
|
176
|
+
expect(eventMatchesAgeFilter(AgePolicy.EighteenPlus, ["21+"])).toBe(false);
|
|
177
|
+
expect(eventMatchesAgeFilter(AgePolicy.AllAges, ["18+", "21+"])).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("treats legacy 'Everyone' label as All Ages (back-compat with old saved filters)", () => {
|
|
181
|
+
expect(eventMatchesAgeFilter(AgePolicy.AllAges, ["Everyone"])).toBe(true);
|
|
182
|
+
expect(eventMatchesAgeFilter(AgePolicy.EighteenPlus, ["Everyone"])).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("treats legacy events without an agePolicy as All Ages", () => {
|
|
186
|
+
expect(eventMatchesAgeFilter(null, ["All Ages"])).toBe(true);
|
|
187
|
+
expect(eventMatchesAgeFilter(null, ["18+"])).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("legacyAgeRangeMatchesFilter", () => {
|
|
192
|
+
// The legacy `targetAudience.ageRange` field was schema-free, so we have to
|
|
193
|
+
// tolerate strings, arrays, objects, null, and even checkbox-style payloads.
|
|
194
|
+
|
|
195
|
+
it("matches everything when no filter labels are selected (no-op filter)", () => {
|
|
196
|
+
expect(legacyAgeRangeMatchesFilter(null, [])).toBe(true);
|
|
197
|
+
expect(legacyAgeRangeMatchesFilter(["18+"], [])).toBe(true);
|
|
198
|
+
expect(legacyAgeRangeMatchesFilter("anything", [])).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("matches '18+' filter when the legacy blob contains '18+'", () => {
|
|
202
|
+
expect(legacyAgeRangeMatchesFilter(["18+"], ["18+"])).toBe(true);
|
|
203
|
+
expect(legacyAgeRangeMatchesFilter("18+", ["18+"])).toBe(true);
|
|
204
|
+
expect(
|
|
205
|
+
legacyAgeRangeMatchesFilter({ adults: "18+" }, ["18+"])
|
|
206
|
+
).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("matches '21+' filter only when the legacy blob contains '21+'", () => {
|
|
210
|
+
expect(legacyAgeRangeMatchesFilter(["21+"], ["21+"])).toBe(true);
|
|
211
|
+
expect(legacyAgeRangeMatchesFilter("18+", ["21+"])).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("matches All Ages when blob is empty / missing (catches untagged legacy events)", () => {
|
|
215
|
+
expect(legacyAgeRangeMatchesFilter(null, ["All Ages"])).toBe(true);
|
|
216
|
+
expect(legacyAgeRangeMatchesFilter(undefined, ["All Ages"])).toBe(true);
|
|
217
|
+
expect(legacyAgeRangeMatchesFilter("", ["All Ages"])).toBe(true);
|
|
218
|
+
expect(legacyAgeRangeMatchesFilter([], ["All Ages"])).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("matches All Ages when blob explicitly mentions 'All Ages' or 'Family Friendly'", () => {
|
|
222
|
+
expect(
|
|
223
|
+
legacyAgeRangeMatchesFilter(["All Ages"], ["All Ages"])
|
|
224
|
+
).toBe(true);
|
|
225
|
+
expect(
|
|
226
|
+
legacyAgeRangeMatchesFilter(["Family Friendly"], ["All Ages"])
|
|
227
|
+
).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does NOT match All Ages when blob carries a 21+ or 18+ marker", () => {
|
|
231
|
+
expect(legacyAgeRangeMatchesFilter(["18+"], ["All Ages"])).toBe(false);
|
|
232
|
+
expect(legacyAgeRangeMatchesFilter(["21+"], ["All Ages"])).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("treats legacy 'Everyone' filter label as a synonym for All Ages", () => {
|
|
236
|
+
expect(legacyAgeRangeMatchesFilter(null, ["Everyone"])).toBe(true);
|
|
237
|
+
expect(legacyAgeRangeMatchesFilter(["18+"], ["Everyone"])).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns true when ANY selected label matches (multi-select union semantics)", () => {
|
|
241
|
+
// Legacy blob says 21+; user selected both 18+ and 21+ — still a match.
|
|
242
|
+
expect(
|
|
243
|
+
legacyAgeRangeMatchesFilter(["21+"], ["18+", "21+"])
|
|
244
|
+
).toBe(true);
|
|
245
|
+
// Legacy blob says 21+; user selected only 18+ — no match.
|
|
246
|
+
expect(legacyAgeRangeMatchesFilter(["21+"], ["18+"])).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does not throw on values that JSON.stringify can't handle (defensive)", () => {
|
|
250
|
+
const cyclic: Record<string, unknown> = {};
|
|
251
|
+
cyclic.self = cyclic;
|
|
252
|
+
// Cyclic objects throw inside JSON.stringify; the helper does NOT swallow
|
|
253
|
+
// that error today, so we document the contract: callers must pass
|
|
254
|
+
// serializable values. (The MyBashesPage code path always does.)
|
|
255
|
+
expect(() => legacyAgeRangeMatchesFilter(cyclic, ["18+"])).toThrow();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `getDisplayGender` precedence tests.
|
|
3
|
+
*
|
|
4
|
+
* Pins the contract that:
|
|
5
|
+
* - `genderIdentity` (free-text) **always wins** over the `gender` enum
|
|
6
|
+
* - whitespace-only `genderIdentity` is treated as absent
|
|
7
|
+
* - falsy `gender` (`null`, `undefined`) yields `null`, not the string "null"
|
|
8
|
+
* - the helper never throws on partial input
|
|
9
|
+
*/
|
|
10
|
+
import { Gender } from "@prisma/client";
|
|
11
|
+
import { getDisplayGender } from "../utils/userUtils.js";
|
|
12
|
+
|
|
13
|
+
describe("getDisplayGender", () => {
|
|
14
|
+
it("returns genderIdentity when present, even if gender is also set", () => {
|
|
15
|
+
expect(
|
|
16
|
+
getDisplayGender({
|
|
17
|
+
gender: Gender.Female,
|
|
18
|
+
genderIdentity: "Non-binary",
|
|
19
|
+
}),
|
|
20
|
+
).toBe("Non-binary");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns the gender enum value when genderIdentity is empty/null", () => {
|
|
24
|
+
expect(
|
|
25
|
+
getDisplayGender({ gender: Gender.Male, genderIdentity: null }),
|
|
26
|
+
).toBe("Male");
|
|
27
|
+
expect(
|
|
28
|
+
getDisplayGender({ gender: Gender.Male, genderIdentity: "" }),
|
|
29
|
+
).toBe("Male");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("trims whitespace-only genderIdentity and falls back to gender", () => {
|
|
33
|
+
expect(
|
|
34
|
+
getDisplayGender({ gender: Gender.NoPreference, genderIdentity: " " }),
|
|
35
|
+
).toBe("NoPreference");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns null when both fields are missing", () => {
|
|
39
|
+
expect(getDisplayGender({})).toBeNull();
|
|
40
|
+
expect(
|
|
41
|
+
getDisplayGender({ gender: null, genderIdentity: null }),
|
|
42
|
+
).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns the trimmed identity (no surrounding whitespace) when used alone", () => {
|
|
46
|
+
expect(
|
|
47
|
+
getDisplayGender({ genderIdentity: " Genderfluid " }),
|
|
48
|
+
).toBe("Genderfluid");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin the value sets of the three status enums introduced as part of the
|
|
3
|
+
* "stringly-typed status" cleanup. These enums are the contract between
|
|
4
|
+
* - api endpoints (uploadIdApi, serviceApi, adminDashboardSummaryApi)
|
|
5
|
+
* - bash-app UI components (IdRequirementGate, IdVerificationSection,
|
|
6
|
+
* CustomScopeRequestsAdminPage, ServicesRatesAndPricing)
|
|
7
|
+
* - the database (Postgres ENUM columns)
|
|
8
|
+
*
|
|
9
|
+
* Adding/removing/renaming a member without intentionally updating the
|
|
10
|
+
* migration + all callers should fail this test loudly.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
CustomScopeStatus,
|
|
14
|
+
GovIdRetentionPolicy,
|
|
15
|
+
SchoolNameSource,
|
|
16
|
+
} from "@prisma/client";
|
|
17
|
+
|
|
18
|
+
describe("status enum value sets", () => {
|
|
19
|
+
test("GovIdRetentionPolicy", () => {
|
|
20
|
+
expect(Object.values(GovIdRetentionPolicy).sort()).toEqual(
|
|
21
|
+
["Indefinite", "ThirtyDays", "UntilIdExpires"].sort(),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("SchoolNameSource", () => {
|
|
26
|
+
expect(Object.values(SchoolNameSource).sort()).toEqual(
|
|
27
|
+
["AiInferred", "UserConfirmed"].sort(),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("CustomScopeStatus", () => {
|
|
32
|
+
expect(Object.values(CustomScopeStatus).sort()).toEqual(
|
|
33
|
+
["Approved", "None", "Pending", "Rejected"].sort(),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shapes for the AI bash-approval reviewer.
|
|
3
|
+
*
|
|
4
|
+
* The Zod runtime envelope lives in `api/src/utils/ai/aiApprovalEnvelope.ts` (zod is an api
|
|
5
|
+
* dependency, not a bash-common one). This file is the type-only source of truth that both
|
|
6
|
+
* the api worker and bash-app's admin UI/host-facing banners read.
|
|
7
|
+
*
|
|
8
|
+
* The persisted `BashEvent.aiApprovalReasons` JSON column is exactly an `AiApprovalReasons`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AiApprovalVerdict } from "@prisma/client";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Each criterion the model grades on.
|
|
15
|
+
*
|
|
16
|
+
* Keep this list short and stable — it controls the persisted JSON shape, the prompt
|
|
17
|
+
* the model sees, and the badges the admin UI renders. Adding a new criterion is a
|
|
18
|
+
* schema change (existing rows will be missing it).
|
|
19
|
+
*/
|
|
20
|
+
export const AI_APPROVAL_CRITERION_KEYS = [
|
|
21
|
+
"completeness",
|
|
22
|
+
"authenticity",
|
|
23
|
+
"safety",
|
|
24
|
+
"policyFit",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type AiApprovalCriterionKey = (typeof AI_APPROVAL_CRITERION_KEYS)[number];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Display labels for criteria — used in the admin queue and any host-facing breakdown.
|
|
31
|
+
* Keep keys exhaustive against `AiApprovalCriterionKey` (compiler will yell if drift).
|
|
32
|
+
*/
|
|
33
|
+
export const AiApprovalCriterionLabel: Record<AiApprovalCriterionKey, string> = {
|
|
34
|
+
completeness: "Completeness",
|
|
35
|
+
authenticity: "Authenticity",
|
|
36
|
+
safety: "Safety",
|
|
37
|
+
policyFit: "Policy fit",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One criterion's grade. `score` is 0..1; `notes` is short free-text the model wrote
|
|
42
|
+
* about why (shown in the admin UI tooltip / host rejection reasons).
|
|
43
|
+
*/
|
|
44
|
+
export interface AiApprovalCriterionResult {
|
|
45
|
+
score: number;
|
|
46
|
+
notes: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Persisted shape for `BashEvent.aiApprovalReasons` (Json column).
|
|
51
|
+
*
|
|
52
|
+
* - `criteria` keys are exhaustive over `AiApprovalCriterionKey`.
|
|
53
|
+
* - `hostFacingMessage` is shown to the host on rejection; written specifically to be
|
|
54
|
+
* actionable ("Add a start time and a location, then resubmit"), not generic.
|
|
55
|
+
* Null on approve.
|
|
56
|
+
* - `adminFacingNotes` is for super-users in the review queue; can be more candid
|
|
57
|
+
* ("Looks like a duplicate of bash xyz" / "Probably a service ad in disguise").
|
|
58
|
+
*/
|
|
59
|
+
export interface AiApprovalReasons {
|
|
60
|
+
criteria: Record<AiApprovalCriterionKey, AiApprovalCriterionResult>;
|
|
61
|
+
hostFacingMessage: string | null;
|
|
62
|
+
adminFacingNotes: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Runtime values for the Prisma `AiApprovalVerdict` enum.
|
|
67
|
+
*
|
|
68
|
+
* Browser code (bash-app) cannot rely on `@prisma/client`'s runtime enum exports — the
|
|
69
|
+
* package's `browser` resolution doesn't reliably surface them when imported transitively
|
|
70
|
+
* via `@bash-app/bash-common`. So we mirror the enum here as a plain string-keyed object
|
|
71
|
+
* and `satisfies Record<AiApprovalVerdict, AiApprovalVerdict>` to guarantee at compile
|
|
72
|
+
* time that the keys/values stay in lockstep with `schema.prisma`. Prefer
|
|
73
|
+
* `AI_APPROVAL_VERDICTS.Approve` over `AiApprovalVerdict.Approve` in any code path that
|
|
74
|
+
* may be bundled for the browser.
|
|
75
|
+
*/
|
|
76
|
+
export const AI_APPROVAL_VERDICTS = {
|
|
77
|
+
Approve: "Approve",
|
|
78
|
+
Flag: "Flag",
|
|
79
|
+
Reject: "Reject",
|
|
80
|
+
} as const satisfies Record<AiApprovalVerdict, AiApprovalVerdict>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Display labels for the Prisma `AiApprovalVerdict` enum, used in admin pills and the
|
|
84
|
+
* host rejection banner so callers don't reach for ad-hoc switch statements.
|
|
85
|
+
*
|
|
86
|
+
* The `Record<AiApprovalVerdict, string>` annotation enforces exhaustiveness at compile
|
|
87
|
+
* time, so adding/renaming an enum member in `schema.prisma` will fail tsc here.
|
|
88
|
+
*/
|
|
89
|
+
export const AiApprovalVerdictLabel: Record<AiApprovalVerdict, string> = {
|
|
90
|
+
Approve: "Approve",
|
|
91
|
+
Flag: "Flag for review",
|
|
92
|
+
Reject: "Reject",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Feature-flag values for `AI_BASH_APPROVAL_MODE`.
|
|
97
|
+
*
|
|
98
|
+
* - `off` - AI service is never invoked. Existing manual-only workflow.
|
|
99
|
+
* - `shadow` - AI runs and persists verdict + reasons, but every outcome is `flag`.
|
|
100
|
+
* Admins still decide everything; we use the data to tune thresholds.
|
|
101
|
+
* - `auto-with-flag` - High-confidence approve/reject act automatically; the rest flag for human.
|
|
102
|
+
* - `auto-approve-only` - High-confidence approve acts automatically; rejects always flag for human.
|
|
103
|
+
*/
|
|
104
|
+
export const AI_BASH_APPROVAL_MODES = [
|
|
105
|
+
"off",
|
|
106
|
+
"shadow",
|
|
107
|
+
"auto-with-flag",
|
|
108
|
+
"auto-approve-only",
|
|
109
|
+
] as const;
|
|
110
|
+
|
|
111
|
+
export type AiBashApprovalMode = (typeof AI_BASH_APPROVAL_MODES)[number];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Default thresholds. Tune in the api consumer if behavior needs to change post-rollout;
|
|
115
|
+
* exporting here so tests in api can import a single source of truth.
|
|
116
|
+
*/
|
|
117
|
+
export const AI_APPROVAL_AUTO_APPROVE_CONFIDENCE = 0.85;
|
|
118
|
+
export const AI_APPROVAL_AUTO_REJECT_CONFIDENCE = 0.9;
|
package/src/definitions.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AgePolicy,
|
|
2
3
|
BashEventType,
|
|
3
4
|
BashPassTier,
|
|
4
5
|
BracketType,
|
|
5
6
|
CompetitionType,
|
|
6
7
|
Contact,
|
|
8
|
+
CustomScopeStatus,
|
|
7
9
|
EntertainmentServiceType,
|
|
10
|
+
GovIdRetentionPolicy,
|
|
8
11
|
JudgingType,
|
|
9
12
|
MembershipTier,
|
|
10
13
|
MusicGenreType,
|
|
11
14
|
Prisma,
|
|
15
|
+
SchoolNameSource,
|
|
12
16
|
ServiceTypes,
|
|
13
17
|
Ticket,
|
|
14
18
|
TicketTier,
|
|
@@ -922,7 +926,140 @@ export const YearsOfExperienceToString: { [key in YearsOfExperience]: string } =
|
|
|
922
926
|
};
|
|
923
927
|
|
|
924
928
|
// Export Prisma enums for use in frontend
|
|
925
|
-
export {
|
|
929
|
+
export {
|
|
930
|
+
AgePolicy,
|
|
931
|
+
BracketType,
|
|
932
|
+
CompetitionType,
|
|
933
|
+
CustomScopeStatus,
|
|
934
|
+
EntertainmentServiceType,
|
|
935
|
+
GovIdRetentionPolicy,
|
|
936
|
+
JudgingType,
|
|
937
|
+
MusicGenreType,
|
|
938
|
+
SchoolNameSource,
|
|
939
|
+
ServiceTypes,
|
|
940
|
+
YearsOfExperience,
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Display labels for `User.govIdRetentionPolicy` (settings UI, ID verification gate).
|
|
945
|
+
*
|
|
946
|
+
* The enum is the source of truth - read sites should never branch on a string literal
|
|
947
|
+
* like `"30days"`; use `GovIdRetentionPolicy.ThirtyDays` instead so a typo can't quietly
|
|
948
|
+
* mismatch on either the API or DB side.
|
|
949
|
+
*/
|
|
950
|
+
export const GovIdRetentionPolicyLabel: Record<GovIdRetentionPolicy, string> = {
|
|
951
|
+
[GovIdRetentionPolicy.ThirtyDays]: "Delete after 30 days",
|
|
952
|
+
[GovIdRetentionPolicy.UntilIdExpires]: "Keep until ID expires",
|
|
953
|
+
[GovIdRetentionPolicy.Indefinite]: "Keep on file (indefinite)",
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Display labels for `Service.customScopeStatus` (admin dashboard, provider pricing UI).
|
|
958
|
+
*/
|
|
959
|
+
export const CustomScopeStatusLabel: Record<CustomScopeStatus, string> = {
|
|
960
|
+
[CustomScopeStatus.None]: "None",
|
|
961
|
+
[CustomScopeStatus.Pending]: "Pending Review",
|
|
962
|
+
[CustomScopeStatus.Approved]: "Approved",
|
|
963
|
+
[CustomScopeStatus.Rejected]: "Rejected",
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
/** Display label for an event's age policy (BashCard pill, filter UI, wizard). */
|
|
967
|
+
export const AgePolicyLabel: Record<AgePolicy, string> = {
|
|
968
|
+
[AgePolicy.AllAges]: "All Ages",
|
|
969
|
+
[AgePolicy.EighteenPlus]: "18+",
|
|
970
|
+
[AgePolicy.TwentyOnePlus]: "21+",
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/** Minimum age (in years) implied by an event's age policy. `null` for All Ages. */
|
|
974
|
+
export const AgePolicyMinimumAge: Record<AgePolicy, number | null> = {
|
|
975
|
+
[AgePolicy.AllAges]: null,
|
|
976
|
+
[AgePolicy.EighteenPlus]: 18,
|
|
977
|
+
[AgePolicy.TwentyOnePlus]: 21,
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
/** Returns the user's age in whole years, or null if the DOB is missing/invalid. */
|
|
981
|
+
export function getAgeFromDob(dob: Date | string | null | undefined): number | null {
|
|
982
|
+
if (!dob) return null;
|
|
983
|
+
const birth = dob instanceof Date ? dob : new Date(dob);
|
|
984
|
+
if (Number.isNaN(birth.getTime())) return null;
|
|
985
|
+
const now = new Date();
|
|
986
|
+
let age = now.getFullYear() - birth.getFullYear();
|
|
987
|
+
const monthDelta = now.getMonth() - birth.getMonth();
|
|
988
|
+
if (monthDelta < 0 || (monthDelta === 0 && now.getDate() < birth.getDate())) {
|
|
989
|
+
age -= 1;
|
|
990
|
+
}
|
|
991
|
+
return age;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Whether the given DOB satisfies the policy's minimum age. AllAges is always true. */
|
|
995
|
+
export function meetsAgePolicy(
|
|
996
|
+
policy: AgePolicy,
|
|
997
|
+
dob: Date | string | null | undefined
|
|
998
|
+
): boolean {
|
|
999
|
+
const min = AgePolicyMinimumAge[policy];
|
|
1000
|
+
if (min === null) return true;
|
|
1001
|
+
const age = getAgeFromDob(dob);
|
|
1002
|
+
if (age === null) return false;
|
|
1003
|
+
return age >= min;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Discover-filter labels (kept human-friendly for chips and modal options). */
|
|
1007
|
+
export type AgePolicyFilterLabel = "All Ages" | "18+" | "21+";
|
|
1008
|
+
|
|
1009
|
+
/** Map an enum value to the discover-filter label. */
|
|
1010
|
+
export function agePolicyToFilterLabel(
|
|
1011
|
+
policy: AgePolicy | null | undefined
|
|
1012
|
+
): AgePolicyFilterLabel {
|
|
1013
|
+
if (policy === AgePolicy.EighteenPlus) return "18+";
|
|
1014
|
+
if (policy === AgePolicy.TwentyOnePlus) return "21+";
|
|
1015
|
+
// Untagged legacy events behave as All Ages so old data doesn't disappear from filters.
|
|
1016
|
+
return "All Ages";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Whether an event's `agePolicy` matches a set of selected filter labels.
|
|
1021
|
+
* Tolerates legacy "Everyone" copy by treating it as a synonym for "All Ages".
|
|
1022
|
+
*/
|
|
1023
|
+
export function eventMatchesAgeFilter(
|
|
1024
|
+
eventAgePolicy: AgePolicy | null | undefined,
|
|
1025
|
+
selectedLabels: ReadonlyArray<string>
|
|
1026
|
+
): boolean {
|
|
1027
|
+
if (selectedLabels.length === 0) return true;
|
|
1028
|
+
const eventLabel = agePolicyToFilterLabel(eventAgePolicy);
|
|
1029
|
+
return selectedLabels.some(
|
|
1030
|
+
(raw) => (raw === "Everyone" ? "All Ages" : raw) === eventLabel
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Legacy fallback for events without `agePolicy`: scans the unstructured
|
|
1036
|
+
* `targetAudience.ageRange` blob (which historically held arrays of strings,
|
|
1037
|
+
* checkbox payloads, and free-text) for the canonical filter labels so that
|
|
1038
|
+
* old data still appears in the All Ages / 18+ / 21+ filters.
|
|
1039
|
+
*
|
|
1040
|
+
* Matching rules (in priority order):
|
|
1041
|
+
* - "21+" filter matches when the blob contains "21+".
|
|
1042
|
+
* - "18+" filter matches when the blob contains "18+".
|
|
1043
|
+
* - "All Ages" / "Everyone" matches when the blob contains "All Ages" or
|
|
1044
|
+
* "Family Friendly", OR when neither "18+" nor "21+" appears.
|
|
1045
|
+
*/
|
|
1046
|
+
export function legacyAgeRangeMatchesFilter(
|
|
1047
|
+
ageRange: unknown,
|
|
1048
|
+
selectedLabels: ReadonlyArray<string>
|
|
1049
|
+
): boolean {
|
|
1050
|
+
if (selectedLabels.length === 0) return true;
|
|
1051
|
+
const blob = JSON.stringify(ageRange ?? "");
|
|
1052
|
+
return selectedLabels.some((raw) => {
|
|
1053
|
+
const normalized = raw === "Everyone" ? "All Ages" : raw;
|
|
1054
|
+
if (normalized === "18+") return blob.includes("18+");
|
|
1055
|
+
if (normalized === "21+") return blob.includes("21+");
|
|
1056
|
+
return (
|
|
1057
|
+
blob.includes("All Ages") ||
|
|
1058
|
+
blob.includes("Family Friendly") ||
|
|
1059
|
+
(!blob.includes("18+") && !blob.includes("21+"))
|
|
1060
|
+
);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
926
1063
|
|
|
927
1064
|
export type BashEventTypeToStringType = {
|
|
928
1065
|
[key in BashEventType]: string;
|