@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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promoCodesUtils — locate / aggregate / re-distribute promo codes across a
|
|
3
|
+
* BashEvent's ticket tiers.
|
|
4
|
+
*
|
|
5
|
+
* These three helpers are tiny but they sit on the path between checkout
|
|
6
|
+
* UI ↔ pricing engine. A bug in `findPromoCodeFromBashEvent` would cause a
|
|
7
|
+
* promo to silently fail to apply at checkout (lost revenue / angry user).
|
|
8
|
+
* A bug in `updatePromoCodesOnBashEventTicketTiers` would scramble which
|
|
9
|
+
* tier a promo code belongs to after edits in the host wizard.
|
|
10
|
+
*/
|
|
11
|
+
import { findPromoCodeFromBashEvent, getPromoCodesFromBashEvent, updatePromoCodesOnBashEventTicketTiers, } from "../promoCodesUtils.js";
|
|
12
|
+
function makePromo(id, ticketTierId, overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
ticketTierId,
|
|
16
|
+
code: `CODE-${id}`,
|
|
17
|
+
discountPercent: 10,
|
|
18
|
+
discountAmount: null,
|
|
19
|
+
maxRedemptions: null,
|
|
20
|
+
redemptionCount: 0,
|
|
21
|
+
expiresAt: null,
|
|
22
|
+
createdAt: new Date(0),
|
|
23
|
+
updatedAt: new Date(0),
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function makeTier(id, promoCodes) {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
title: `Tier ${id}`,
|
|
31
|
+
promoCodes,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function makeBashEvent(tiers) {
|
|
35
|
+
return { ticketTiers: tiers };
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// findPromoCodeFromBashEvent
|
|
39
|
+
// ============================================================================
|
|
40
|
+
describe("findPromoCodeFromBashEvent", () => {
|
|
41
|
+
it("returns undefined when promoCodeId is undefined (no lookup attempted)", () => {
|
|
42
|
+
const bash = makeBashEvent([makeTier("t1", [makePromo("p1", "t1")])]);
|
|
43
|
+
expect(findPromoCodeFromBashEvent(bash, undefined)).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
it("returns undefined when promoCodeId is the empty string (falsy guard)", () => {
|
|
46
|
+
const bash = makeBashEvent([makeTier("t1", [makePromo("p1", "t1")])]);
|
|
47
|
+
expect(findPromoCodeFromBashEvent(bash, "")).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it("returns undefined when bashEvent is null", () => {
|
|
50
|
+
expect(findPromoCodeFromBashEvent(null, "p1")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it("returns undefined when bashEvent is undefined", () => {
|
|
53
|
+
expect(findPromoCodeFromBashEvent(undefined, "p1")).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
it("returns undefined when ticketTiers is empty", () => {
|
|
56
|
+
expect(findPromoCodeFromBashEvent(makeBashEvent([]), "p1")).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
it("finds a promo on the FIRST tier", () => {
|
|
59
|
+
const promo = makePromo("p1", "t1");
|
|
60
|
+
const bash = makeBashEvent([
|
|
61
|
+
makeTier("t1", [promo]),
|
|
62
|
+
makeTier("t2", [makePromo("p2", "t2")]),
|
|
63
|
+
]);
|
|
64
|
+
expect(findPromoCodeFromBashEvent(bash, "p1")).toBe(promo);
|
|
65
|
+
});
|
|
66
|
+
it("finds a promo on a NON-FIRST tier (iterates across tiers)", () => {
|
|
67
|
+
const promo = makePromo("p2", "t2");
|
|
68
|
+
const bash = makeBashEvent([
|
|
69
|
+
makeTier("t1", [makePromo("p1", "t1")]),
|
|
70
|
+
makeTier("t2", [promo]),
|
|
71
|
+
]);
|
|
72
|
+
expect(findPromoCodeFromBashEvent(bash, "p2")).toBe(promo);
|
|
73
|
+
});
|
|
74
|
+
it("returns the FIRST match if the same id appears on multiple tiers (defensive — shouldn't happen but guard the order)", () => {
|
|
75
|
+
const dupOnT1 = makePromo("dup", "t1");
|
|
76
|
+
const dupOnT2 = makePromo("dup", "t2");
|
|
77
|
+
const bash = makeBashEvent([
|
|
78
|
+
makeTier("t1", [dupOnT1]),
|
|
79
|
+
makeTier("t2", [dupOnT2]),
|
|
80
|
+
]);
|
|
81
|
+
expect(findPromoCodeFromBashEvent(bash, "dup")).toBe(dupOnT1);
|
|
82
|
+
});
|
|
83
|
+
it("returns undefined when promoCodeId doesn't match any tier's promo", () => {
|
|
84
|
+
const bash = makeBashEvent([makeTier("t1", [makePromo("p1", "t1")])]);
|
|
85
|
+
expect(findPromoCodeFromBashEvent(bash, "missing")).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
it("does not throw when a tier has no promoCodes array (defensive)", () => {
|
|
88
|
+
const tier = { id: "t1", promoCodes: [] };
|
|
89
|
+
expect(() => findPromoCodeFromBashEvent(makeBashEvent([tier]), "p1")).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// getPromoCodesFromBashEvent
|
|
94
|
+
// ============================================================================
|
|
95
|
+
describe("getPromoCodesFromBashEvent", () => {
|
|
96
|
+
it("returns [] when bashEvent is null", () => {
|
|
97
|
+
expect(getPromoCodesFromBashEvent(null)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
it("returns [] when bashEvent is undefined", () => {
|
|
100
|
+
expect(getPromoCodesFromBashEvent(undefined)).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
it("returns [] when there are no ticket tiers", () => {
|
|
103
|
+
expect(getPromoCodesFromBashEvent(makeBashEvent([]))).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
it("returns [] when no tier has any promo codes", () => {
|
|
106
|
+
const bash = makeBashEvent([makeTier("t1", []), makeTier("t2", [])]);
|
|
107
|
+
expect(getPromoCodesFromBashEvent(bash)).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
it("flattens promo codes across multiple tiers in tier order", () => {
|
|
110
|
+
const p1 = makePromo("p1", "t1");
|
|
111
|
+
const p2 = makePromo("p2", "t2");
|
|
112
|
+
const p3 = makePromo("p3", "t2");
|
|
113
|
+
const bash = makeBashEvent([
|
|
114
|
+
makeTier("t1", [p1]),
|
|
115
|
+
makeTier("t2", [p2, p3]),
|
|
116
|
+
]);
|
|
117
|
+
expect(getPromoCodesFromBashEvent(bash)).toEqual([p1, p2, p3]);
|
|
118
|
+
});
|
|
119
|
+
it("skips tiers with empty promoCodes (filter step)", () => {
|
|
120
|
+
const p1 = makePromo("p1", "t1");
|
|
121
|
+
const bash = makeBashEvent([
|
|
122
|
+
makeTier("t1", [p1]),
|
|
123
|
+
makeTier("t2", []),
|
|
124
|
+
makeTier("t3", [makePromo("p3", "t3")]),
|
|
125
|
+
]);
|
|
126
|
+
const all = getPromoCodesFromBashEvent(bash);
|
|
127
|
+
expect(all.map((p) => p.id)).toEqual(["p1", "p3"]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// updatePromoCodesOnBashEventTicketTiers
|
|
132
|
+
// ============================================================================
|
|
133
|
+
describe("updatePromoCodesOnBashEventTicketTiers", () => {
|
|
134
|
+
it("re-buckets promos onto tiers by ticketTierId (mutates in place)", () => {
|
|
135
|
+
const bash = makeBashEvent([
|
|
136
|
+
makeTier("t1", []),
|
|
137
|
+
makeTier("t2", []),
|
|
138
|
+
]);
|
|
139
|
+
const promos = [
|
|
140
|
+
makePromo("p1", "t1"),
|
|
141
|
+
makePromo("p2", "t2"),
|
|
142
|
+
makePromo("p3", "t1"),
|
|
143
|
+
];
|
|
144
|
+
updatePromoCodesOnBashEventTicketTiers(bash, promos);
|
|
145
|
+
expect(bash.ticketTiers?.[0].promoCodes.map((p) => p.id)).toEqual([
|
|
146
|
+
"p1",
|
|
147
|
+
"p3",
|
|
148
|
+
]);
|
|
149
|
+
expect(bash.ticketTiers?.[1].promoCodes.map((p) => p.id)).toEqual(["p2"]);
|
|
150
|
+
});
|
|
151
|
+
it("REPLACES existing promo codes on each tier (not appended)", () => {
|
|
152
|
+
const oldPromo = makePromo("old", "t1");
|
|
153
|
+
const bash = makeBashEvent([makeTier("t1", [oldPromo])]);
|
|
154
|
+
updatePromoCodesOnBashEventTicketTiers(bash, [makePromo("new", "t1")]);
|
|
155
|
+
expect(bash.ticketTiers?.[0].promoCodes.map((p) => p.id)).toEqual([
|
|
156
|
+
"new",
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
it("clears tiers that have no matching promos in the new list", () => {
|
|
160
|
+
const bash = makeBashEvent([
|
|
161
|
+
makeTier("t1", [makePromo("oldA", "t1")]),
|
|
162
|
+
makeTier("t2", [makePromo("oldB", "t2")]),
|
|
163
|
+
]);
|
|
164
|
+
updatePromoCodesOnBashEventTicketTiers(bash, [makePromo("newA", "t1")]);
|
|
165
|
+
expect(bash.ticketTiers?.[0].promoCodes.map((p) => p.id)).toEqual([
|
|
166
|
+
"newA",
|
|
167
|
+
]);
|
|
168
|
+
expect(bash.ticketTiers?.[1].promoCodes).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
it("ignores promos whose ticketTierId doesn't match any tier on the event (orphans dropped)", () => {
|
|
171
|
+
const bash = makeBashEvent([makeTier("t1", [])]);
|
|
172
|
+
updatePromoCodesOnBashEventTicketTiers(bash, [
|
|
173
|
+
makePromo("p1", "t1"),
|
|
174
|
+
makePromo("orphan", "tDoesNotExist"),
|
|
175
|
+
]);
|
|
176
|
+
expect(bash.ticketTiers?.[0].promoCodes.map((p) => p.id)).toEqual(["p1"]);
|
|
177
|
+
});
|
|
178
|
+
it("is a no-op when ticketTiers is undefined", () => {
|
|
179
|
+
const bash = { ticketTiers: undefined };
|
|
180
|
+
expect(() => updatePromoCodesOnBashEventTicketTiers(bash, [makePromo("p1", "t1")])).not.toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=promoCodesUtils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promoCodesUtils.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/promoCodesUtils.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qrCodeUtils.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__tests__/qrCodeUtils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qrCodeUtils — URL builders for the *navigable* `/ticket-details` deep
|
|
3
|
+
* links that get encoded into ticket QR codes (and createQrCodeImage that
|
|
4
|
+
* renders them as PNG data URLs).
|
|
5
|
+
*
|
|
6
|
+
* The contract these tests pin (consumed by the door-scan flow, ticket
|
|
7
|
+
* emails, and tier-share QR codes):
|
|
8
|
+
*
|
|
9
|
+
* bashEventPrismaCheckoutQrCodeUrl(eventId, checkoutId)
|
|
10
|
+
* → "/ticket-details/{eventId}/{checkoutId}"
|
|
11
|
+
*
|
|
12
|
+
* bashEventQrCodeUrl(eventId) → "/ticket-details/{eventId}"
|
|
13
|
+
* bashEventQrCodeUrl(eventId, tierId) → "/ticket-details/{eventId}/ticket-tier/{tierId}"
|
|
14
|
+
* bashEventQrCodeUrl(eventId, tierId, ticketId) → "/ticket-details/{eventId}/ticket/{ticketId}" (ticketId WINS over tierId)
|
|
15
|
+
* bashEventQrCodeUrl(eventId, _, _, slug) → "/ticket-details/{eventId}-{slug}" (slug version of the deep link)
|
|
16
|
+
*
|
|
17
|
+
* createQrCodeImage(data) → Promise<"data:image/png;base64,...">
|
|
18
|
+
*
|
|
19
|
+
* Bugs in any of these route door-scanners to the wrong page (= entry chaos).
|
|
20
|
+
*/
|
|
21
|
+
import { bashEventPrismaCheckoutQrCodeUrl, bashEventQrCodeUrl, createQrCodeImage, } from "../qrCodeUtils.js";
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// bashEventPrismaCheckoutQrCodeUrl — checkout-id deep link
|
|
24
|
+
// ============================================================================
|
|
25
|
+
describe("bashEventPrismaCheckoutQrCodeUrl", () => {
|
|
26
|
+
it("composes /ticket-details/{eventId}/{checkoutId}", () => {
|
|
27
|
+
expect(bashEventPrismaCheckoutQrCodeUrl("evt_123", "co_456")).toBe("/ticket-details/evt_123/co_456");
|
|
28
|
+
});
|
|
29
|
+
it("does NOT throw on null/undefined inputs (downstream link is broken-but-safe)", () => {
|
|
30
|
+
// The current contract just stringifies via template literal, so we pin
|
|
31
|
+
// the literal output rather than asserting an exception.
|
|
32
|
+
expect(bashEventPrismaCheckoutQrCodeUrl(null, null)).toBe("/ticket-details/null/null");
|
|
33
|
+
expect(bashEventPrismaCheckoutQrCodeUrl(undefined, undefined)).toBe("/ticket-details/undefined/undefined");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// bashEventQrCodeUrl — base / tier / ticket / slug variants
|
|
38
|
+
// ============================================================================
|
|
39
|
+
describe("bashEventQrCodeUrl", () => {
|
|
40
|
+
describe("base url (no tier, no ticket, no slug)", () => {
|
|
41
|
+
it("returns /ticket-details/{eventId}", () => {
|
|
42
|
+
expect(bashEventQrCodeUrl("evt_1")).toBe("/ticket-details/evt_1");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("with ticketTierId", () => {
|
|
46
|
+
it("appends /ticket-tier/{tierId}", () => {
|
|
47
|
+
expect(bashEventQrCodeUrl("evt_1", "tier_a")).toBe("/ticket-details/evt_1/ticket-tier/tier_a");
|
|
48
|
+
});
|
|
49
|
+
it("ignores null ticketId in favor of tier", () => {
|
|
50
|
+
expect(bashEventQrCodeUrl("evt_1", "tier_a", null)).toBe("/ticket-details/evt_1/ticket-tier/tier_a");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("with ticketId (the door-scan link)", () => {
|
|
54
|
+
it("appends /ticket/{ticketId}", () => {
|
|
55
|
+
expect(bashEventQrCodeUrl("evt_1", undefined, "ticket_xyz")).toBe("/ticket-details/evt_1/ticket/ticket_xyz");
|
|
56
|
+
});
|
|
57
|
+
it("PREFERS ticketId over ticketTierId when both are provided", () => {
|
|
58
|
+
// Tickets are more specific than tiers; the deep link must resolve to the
|
|
59
|
+
// individual ticket (otherwise the door scanner can't validate redemption).
|
|
60
|
+
expect(bashEventQrCodeUrl("evt_1", "tier_a", "ticket_xyz")).toBe("/ticket-details/evt_1/ticket/ticket_xyz");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("with slug", () => {
|
|
64
|
+
it("uses the slugged base (`/ticket-details/{eventId}-{slug}`)", () => {
|
|
65
|
+
expect(bashEventQrCodeUrl("evt_1", undefined, undefined, "summer-rave")).toBe("/ticket-details/evt_1-summer-rave");
|
|
66
|
+
});
|
|
67
|
+
it("appends ticketId on the slugged base", () => {
|
|
68
|
+
expect(bashEventQrCodeUrl("evt_1", undefined, "ticket_xyz", "summer-rave")).toBe("/ticket-details/evt_1-summer-rave/ticket/ticket_xyz");
|
|
69
|
+
});
|
|
70
|
+
it("appends ticket-tier on the slugged base when no ticketId", () => {
|
|
71
|
+
expect(bashEventQrCodeUrl("evt_1", "tier_a", undefined, "summer-rave")).toBe("/ticket-details/evt_1-summer-rave/ticket-tier/tier_a");
|
|
72
|
+
});
|
|
73
|
+
it("falls back to non-slug base when slug is null", () => {
|
|
74
|
+
expect(bashEventQrCodeUrl("evt_1", null, null, null)).toBe("/ticket-details/evt_1");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// createQrCodeImage — PNG data URL
|
|
80
|
+
// ============================================================================
|
|
81
|
+
describe("createQrCodeImage", () => {
|
|
82
|
+
it("returns a base64-encoded PNG data URL", async () => {
|
|
83
|
+
const dataUrl = await createQrCodeImage("https://bash.app/ticket-details/evt_1/ticket/abc");
|
|
84
|
+
expect(dataUrl).toMatch(/^data:image\/png;base64,/);
|
|
85
|
+
// Sanity: a real QR image will have *meaningful* base64 length, not a few bytes.
|
|
86
|
+
expect(dataUrl.length).toBeGreaterThan(200);
|
|
87
|
+
});
|
|
88
|
+
it("produces a deterministic PNG for the same input (no nonce / timestamp leakage)", async () => {
|
|
89
|
+
const a = await createQrCodeImage("ticket://abc");
|
|
90
|
+
const b = await createQrCodeImage("ticket://abc");
|
|
91
|
+
expect(a).toBe(b);
|
|
92
|
+
});
|
|
93
|
+
it("produces DIFFERENT output for different payloads", async () => {
|
|
94
|
+
const a = await createQrCodeImage("ticket://abc");
|
|
95
|
+
const b = await createQrCodeImage("ticket://xyz");
|
|
96
|
+
expect(a).not.toBe(b);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=qrCodeUtils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qrCodeUtils.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/qrCodeUtils.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripeAccountUtils.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__tests__/stripeAccountUtils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stripeAccountUtils — the gate that decides whether a host can accept money.
|
|
3
|
+
*
|
|
4
|
+
* `stripeAccountFullyLinked` returns `true` ONLY when ALL FOUR of these are true:
|
|
5
|
+
* - linkingStatus is non-null
|
|
6
|
+
* - hasOutstandingRequirements === false
|
|
7
|
+
* - isOnboarded === true
|
|
8
|
+
* - paymentsEnabled === true
|
|
9
|
+
* - taxMonitoringEnabled === true
|
|
10
|
+
*
|
|
11
|
+
* This is the single check used everywhere we decide "can this host get paid?"
|
|
12
|
+
* Returning `true` when any one is false leads to checkouts that can't actually
|
|
13
|
+
* disburse money. We pin all 5 axes individually so a future refactor that
|
|
14
|
+
* accidentally drops one will fail loudly.
|
|
15
|
+
*/
|
|
16
|
+
import { stripeAccountFullyLinked } from "../stripeAccountUtils.js";
|
|
17
|
+
const fullyLinked = {
|
|
18
|
+
isOnboarded: true,
|
|
19
|
+
hasOutstandingRequirements: false,
|
|
20
|
+
paymentsEnabled: true,
|
|
21
|
+
taxMonitoringEnabled: true,
|
|
22
|
+
};
|
|
23
|
+
describe("stripeAccountFullyLinked", () => {
|
|
24
|
+
describe("nullish guard", () => {
|
|
25
|
+
it("returns false for null", () => {
|
|
26
|
+
expect(stripeAccountFullyLinked(null)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it("returns false for undefined", () => {
|
|
29
|
+
expect(stripeAccountFullyLinked(undefined)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("happy path", () => {
|
|
33
|
+
it("returns true when ALL four flags are correct", () => {
|
|
34
|
+
expect(stripeAccountFullyLinked(fullyLinked)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("each individual gate must be satisfied", () => {
|
|
38
|
+
it("returns false when isOnboarded is false", () => {
|
|
39
|
+
expect(stripeAccountFullyLinked({ ...fullyLinked, isOnboarded: false })).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it("returns false when hasOutstandingRequirements is true", () => {
|
|
42
|
+
expect(stripeAccountFullyLinked({
|
|
43
|
+
...fullyLinked,
|
|
44
|
+
hasOutstandingRequirements: true,
|
|
45
|
+
})).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("returns false when paymentsEnabled is false", () => {
|
|
48
|
+
expect(stripeAccountFullyLinked({ ...fullyLinked, paymentsEnabled: false })).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it("returns false when taxMonitoringEnabled is false", () => {
|
|
51
|
+
expect(stripeAccountFullyLinked({
|
|
52
|
+
...fullyLinked,
|
|
53
|
+
taxMonitoringEnabled: false,
|
|
54
|
+
})).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("multi-flag failures (any combination = false)", () => {
|
|
58
|
+
it("returns false when payments AND tax monitoring are both disabled", () => {
|
|
59
|
+
expect(stripeAccountFullyLinked({
|
|
60
|
+
...fullyLinked,
|
|
61
|
+
paymentsEnabled: false,
|
|
62
|
+
taxMonitoringEnabled: false,
|
|
63
|
+
})).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("returns false when EVERY flag is in the wrong state", () => {
|
|
66
|
+
expect(stripeAccountFullyLinked({
|
|
67
|
+
isOnboarded: false,
|
|
68
|
+
hasOutstandingRequirements: true,
|
|
69
|
+
paymentsEnabled: false,
|
|
70
|
+
taxMonitoringEnabled: false,
|
|
71
|
+
})).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=stripeAccountUtils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripeAccountUtils.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/stripeAccountUtils.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,MAAM,WAAW,GAAwB;IACvC,WAAW,EAAE,IAAI;IACjB,0BAA0B,EAAE,KAAK;IACjC,eAAe,EAAE,IAAI;IACrB,oBAAoB,EAAE,IAAI;CAC3B,CAAC;AAEF,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;YAChC,MAAM,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACtD,MAAM,CAAC,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;QACtD,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,CACJ,wBAAwB,CAAC,EAAE,GAAG,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CACjE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;YAC/D,MAAM,CACJ,wBAAwB,CAAC;gBACvB,GAAG,WAAW;gBACd,0BAA0B,EAAE,IAAI;aACjC,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;YACrD,MAAM,CACJ,wBAAwB,CAAC,EAAE,GAAG,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CACrE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;YAC1D,MAAM,CACJ,wBAAwB,CAAC;gBACvB,GAAG,WAAW;gBACd,oBAAoB,EAAE,KAAK;aAC5B,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,+CAA+C,EAAE,GAAG,EAAE;QAC7D,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;YAC1E,MAAM,CACJ,wBAAwB,CAAC;gBACvB,GAAG,WAAW;gBACd,eAAe,EAAE,KAAK;gBACtB,oBAAoB,EAAE,KAAK;aAC5B,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;YAC7D,MAAM,CACJ,wBAAwB,CAAC;gBACvB,WAAW,EAAE,KAAK;gBAClB,0BAA0B,EAAE,IAAI;gBAChC,eAAe,EAAE,KAAK;gBACtB,oBAAoB,EAAE,KAAK;aAC5B,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getRelatedBlogPosts.test.d.ts","sourceRoot":"","sources":["../../../../src/utils/blog/__tests__/getRelatedBlogPosts.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Tests for getRelatedBlogPosts -- the tag-overlap-first scoring algorithm
|
|
2
|
+
// that powers the "Related reading" rail under every blog detail page. The
|
|
3
|
+
// scoring function is the SEO-critical surface (controls the internal-link
|
|
4
|
+
// graph) so we exercise every branch of the priority sort:
|
|
5
|
+
// 1. tag overlap count (descending)
|
|
6
|
+
// 2. shared category (1/0)
|
|
7
|
+
// 3. recency (most recent first)
|
|
8
|
+
//
|
|
9
|
+
// Plus the edge cases:
|
|
10
|
+
// - source post does not exist -> []
|
|
11
|
+
// - source has no tags + no category -> still returns recent published posts
|
|
12
|
+
// - limit cap
|
|
13
|
+
// - publishedAt null on a candidate is treated as oldest
|
|
14
|
+
import { BlogStatus } from "@prisma/client";
|
|
15
|
+
import { getRelatedBlogPosts } from "../blogDbUtils.js";
|
|
16
|
+
function makePrismaMock(opts) {
|
|
17
|
+
const findUnique = jest.fn().mockResolvedValue(opts.source);
|
|
18
|
+
const findMany = jest.fn().mockResolvedValue(opts.candidates);
|
|
19
|
+
return {
|
|
20
|
+
blogPost: { findUnique, findMany },
|
|
21
|
+
__findUnique: findUnique,
|
|
22
|
+
__findMany: findMany,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function candidate(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
id: "c-default",
|
|
28
|
+
slug: "c-default",
|
|
29
|
+
title: "Default Candidate",
|
|
30
|
+
excerpt: "default",
|
|
31
|
+
coverImage: null,
|
|
32
|
+
readTime: 5,
|
|
33
|
+
publishedAt: new Date("2026-01-01T00:00:00Z"),
|
|
34
|
+
categoryId: null,
|
|
35
|
+
tags: [],
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
describe("getRelatedBlogPosts", () => {
|
|
40
|
+
it("returns [] when the source post does not exist", async () => {
|
|
41
|
+
const prisma = makePrismaMock({ source: null, candidates: [] });
|
|
42
|
+
const out = await getRelatedBlogPosts(prisma, "missing-id");
|
|
43
|
+
expect(out).toEqual([]);
|
|
44
|
+
expect(prisma.__findMany).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
it("ranks candidates by shared tag count (descending)", async () => {
|
|
47
|
+
const source = {
|
|
48
|
+
id: "src",
|
|
49
|
+
categoryId: null,
|
|
50
|
+
tags: [{ id: "t1" }, { id: "t2" }, { id: "t3" }],
|
|
51
|
+
};
|
|
52
|
+
const candidates = [
|
|
53
|
+
candidate({
|
|
54
|
+
id: "low",
|
|
55
|
+
title: "One tag in common",
|
|
56
|
+
tags: [{ id: "t1" }],
|
|
57
|
+
publishedAt: new Date("2026-03-01"),
|
|
58
|
+
}),
|
|
59
|
+
candidate({
|
|
60
|
+
id: "high",
|
|
61
|
+
title: "Three tags in common",
|
|
62
|
+
tags: [{ id: "t1" }, { id: "t2" }, { id: "t3" }],
|
|
63
|
+
publishedAt: new Date("2026-01-01"),
|
|
64
|
+
}),
|
|
65
|
+
candidate({
|
|
66
|
+
id: "mid",
|
|
67
|
+
title: "Two tags in common",
|
|
68
|
+
tags: [{ id: "t1" }, { id: "t2" }],
|
|
69
|
+
publishedAt: new Date("2026-02-01"),
|
|
70
|
+
}),
|
|
71
|
+
];
|
|
72
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
73
|
+
const out = await getRelatedBlogPosts(prisma, "src", 4);
|
|
74
|
+
expect(out.map((p) => p.id)).toEqual(["high", "mid", "low"]);
|
|
75
|
+
});
|
|
76
|
+
it("uses shared category as the tiebreaker when tag counts tie", async () => {
|
|
77
|
+
const source = {
|
|
78
|
+
id: "src",
|
|
79
|
+
categoryId: "cat-1",
|
|
80
|
+
tags: [{ id: "t1" }],
|
|
81
|
+
};
|
|
82
|
+
const candidates = [
|
|
83
|
+
candidate({
|
|
84
|
+
id: "without-category",
|
|
85
|
+
tags: [{ id: "t1" }],
|
|
86
|
+
categoryId: "cat-other",
|
|
87
|
+
publishedAt: new Date("2026-03-01"),
|
|
88
|
+
}),
|
|
89
|
+
candidate({
|
|
90
|
+
id: "with-category",
|
|
91
|
+
tags: [{ id: "t1" }],
|
|
92
|
+
categoryId: "cat-1",
|
|
93
|
+
publishedAt: new Date("2026-01-01"),
|
|
94
|
+
}),
|
|
95
|
+
];
|
|
96
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
97
|
+
const out = await getRelatedBlogPosts(prisma, "src", 4);
|
|
98
|
+
expect(out.map((p) => p.id)).toEqual(["with-category", "without-category"]);
|
|
99
|
+
});
|
|
100
|
+
it("falls back to recency when both tag count and category tie", async () => {
|
|
101
|
+
const source = {
|
|
102
|
+
id: "src",
|
|
103
|
+
categoryId: "cat-1",
|
|
104
|
+
tags: [{ id: "t1" }],
|
|
105
|
+
};
|
|
106
|
+
const candidates = [
|
|
107
|
+
candidate({
|
|
108
|
+
id: "older",
|
|
109
|
+
tags: [{ id: "t1" }],
|
|
110
|
+
categoryId: "cat-1",
|
|
111
|
+
publishedAt: new Date("2026-01-01"),
|
|
112
|
+
}),
|
|
113
|
+
candidate({
|
|
114
|
+
id: "newer",
|
|
115
|
+
tags: [{ id: "t1" }],
|
|
116
|
+
categoryId: "cat-1",
|
|
117
|
+
publishedAt: new Date("2026-04-01"),
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
121
|
+
const out = await getRelatedBlogPosts(prisma, "src", 4);
|
|
122
|
+
expect(out.map((p) => p.id)).toEqual(["newer", "older"]);
|
|
123
|
+
});
|
|
124
|
+
it("treats null publishedAt as the oldest possible candidate", async () => {
|
|
125
|
+
const source = {
|
|
126
|
+
id: "src",
|
|
127
|
+
categoryId: null,
|
|
128
|
+
tags: [{ id: "t1" }],
|
|
129
|
+
};
|
|
130
|
+
const candidates = [
|
|
131
|
+
candidate({
|
|
132
|
+
id: "no-date",
|
|
133
|
+
tags: [{ id: "t1" }],
|
|
134
|
+
publishedAt: null,
|
|
135
|
+
}),
|
|
136
|
+
candidate({
|
|
137
|
+
id: "dated",
|
|
138
|
+
tags: [{ id: "t1" }],
|
|
139
|
+
publishedAt: new Date("2026-01-01"),
|
|
140
|
+
}),
|
|
141
|
+
];
|
|
142
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
143
|
+
const out = await getRelatedBlogPosts(prisma, "src", 4);
|
|
144
|
+
expect(out.map((p) => p.id)).toEqual(["dated", "no-date"]);
|
|
145
|
+
});
|
|
146
|
+
it("respects the limit parameter", async () => {
|
|
147
|
+
const source = {
|
|
148
|
+
id: "src",
|
|
149
|
+
categoryId: null,
|
|
150
|
+
tags: [{ id: "t1" }],
|
|
151
|
+
};
|
|
152
|
+
const candidates = Array.from({ length: 8 }, (_, i) => candidate({
|
|
153
|
+
id: `c${i}`,
|
|
154
|
+
title: `Candidate ${i}`,
|
|
155
|
+
tags: [{ id: "t1" }],
|
|
156
|
+
publishedAt: new Date(2026, 0, i + 1),
|
|
157
|
+
}));
|
|
158
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
159
|
+
const out = await getRelatedBlogPosts(prisma, "src", 3);
|
|
160
|
+
expect(out).toHaveLength(3);
|
|
161
|
+
});
|
|
162
|
+
it("defaults the limit to 4 when not provided", async () => {
|
|
163
|
+
const source = {
|
|
164
|
+
id: "src",
|
|
165
|
+
categoryId: null,
|
|
166
|
+
tags: [{ id: "t1" }],
|
|
167
|
+
};
|
|
168
|
+
const candidates = Array.from({ length: 10 }, (_, i) => candidate({ id: `c${i}`, tags: [{ id: "t1" }] }));
|
|
169
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
170
|
+
const out = await getRelatedBlogPosts(prisma, "src");
|
|
171
|
+
expect(out).toHaveLength(4);
|
|
172
|
+
});
|
|
173
|
+
it("queries by tag-overlap OR shared category, excluding the source", async () => {
|
|
174
|
+
const source = {
|
|
175
|
+
id: "src",
|
|
176
|
+
categoryId: "cat-1",
|
|
177
|
+
tags: [{ id: "t1" }, { id: "t2" }],
|
|
178
|
+
};
|
|
179
|
+
const prisma = makePrismaMock({ source, candidates: [] });
|
|
180
|
+
await getRelatedBlogPosts(prisma, "src", 5);
|
|
181
|
+
const where = prisma.__findMany.mock.calls[0][0].where;
|
|
182
|
+
expect(where.id).toEqual({ not: "src" });
|
|
183
|
+
expect(where.status).toBe(BlogStatus.Published);
|
|
184
|
+
expect(where.publishedAt).toEqual({ not: null });
|
|
185
|
+
expect(where.OR).toEqual([
|
|
186
|
+
{ tags: { some: { id: { in: ["t1", "t2"] } } } },
|
|
187
|
+
{ categoryId: "cat-1" },
|
|
188
|
+
]);
|
|
189
|
+
// candidate pool size is limit * 4 to give the scorer headroom
|
|
190
|
+
expect(prisma.__findMany.mock.calls[0][0].take).toBe(20);
|
|
191
|
+
});
|
|
192
|
+
it("omits the OR clause entirely when source has no tags and no category", async () => {
|
|
193
|
+
const source = { id: "src", categoryId: null, tags: [] };
|
|
194
|
+
const prisma = makePrismaMock({ source, candidates: [] });
|
|
195
|
+
await getRelatedBlogPosts(prisma, "src");
|
|
196
|
+
const where = prisma.__findMany.mock.calls[0][0].where;
|
|
197
|
+
expect(where.OR).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
it("returns the trimmed RelatedBlogPost shape (no internal scoring fields)", async () => {
|
|
200
|
+
const source = {
|
|
201
|
+
id: "src",
|
|
202
|
+
categoryId: null,
|
|
203
|
+
tags: [{ id: "t1" }],
|
|
204
|
+
};
|
|
205
|
+
const candidates = [
|
|
206
|
+
candidate({
|
|
207
|
+
id: "c1",
|
|
208
|
+
slug: "c1-slug",
|
|
209
|
+
title: "C1",
|
|
210
|
+
excerpt: "ex",
|
|
211
|
+
coverImage: "img.png",
|
|
212
|
+
readTime: 7,
|
|
213
|
+
publishedAt: new Date("2026-01-01"),
|
|
214
|
+
tags: [{ id: "t1" }],
|
|
215
|
+
categoryId: "cat-1",
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
const prisma = makePrismaMock({ source, candidates });
|
|
219
|
+
const out = await getRelatedBlogPosts(prisma, "src", 4);
|
|
220
|
+
expect(out[0]).toEqual({
|
|
221
|
+
id: "c1",
|
|
222
|
+
slug: "c1-slug",
|
|
223
|
+
title: "C1",
|
|
224
|
+
excerpt: "ex",
|
|
225
|
+
coverImage: "img.png",
|
|
226
|
+
readTime: 7,
|
|
227
|
+
publishedAt: new Date("2026-01-01"),
|
|
228
|
+
});
|
|
229
|
+
expect(Object.keys(out[0])).not.toContain("categoryId");
|
|
230
|
+
expect(Object.keys(out[0])).not.toContain("tags");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
//# sourceMappingURL=getRelatedBlogPosts.test.js.map
|