@odla-ai/chapter 0.54.0 → 0.54.2
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/README.md +16 -4
- package/dist/{chunk-NDZCR3OW.js → chunk-ZAE24YKY.js} +54 -21
- package/dist/{chunk-NDZCR3OW.js.map → chunk-ZAE24YKY.js.map} +1 -1
- package/dist/{copy-context-qvFo6eA7.d.ts → copy-context-CaUXIxUI.d.ts} +17 -2
- package/dist/index.cjs +281 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -17
- package/dist/index.d.ts +76 -17
- package/dist/index.js +266 -117
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +689 -544
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +26 -3
- package/dist/worker/index.d.ts +26 -3
- package/dist/worker/index.js +635 -490
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1101,6 +1101,7 @@ Warmly,
|
|
|
1101
1101
|
${name}`;
|
|
1102
1102
|
return {
|
|
1103
1103
|
adminNotification: {
|
|
1104
|
+
enabled: true,
|
|
1104
1105
|
subject: `New application: {{firstName}} {{lastName}}`,
|
|
1105
1106
|
text: `A new application came in for ${name}.
|
|
1106
1107
|
|
|
@@ -1111,18 +1112,21 @@ Tier: {{tier}}
|
|
|
1111
1112
|
Review it: {{adminUrl}}`
|
|
1112
1113
|
},
|
|
1113
1114
|
submitConfirmation: {
|
|
1115
|
+
enabled: true,
|
|
1114
1116
|
subject: `We received your application to ${name}`,
|
|
1115
1117
|
text: `Hi {{firstName}},
|
|
1116
1118
|
|
|
1117
1119
|
Thanks for applying to ${name}. {{submitNextStep}}${sign}`
|
|
1118
1120
|
},
|
|
1119
1121
|
paymentConfirmation: {
|
|
1122
|
+
enabled: true,
|
|
1120
1123
|
subject: `Welcome to ${name}`,
|
|
1121
1124
|
text: `Hi {{firstName}},
|
|
1122
1125
|
|
|
1123
1126
|
Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}}${sign}`
|
|
1124
1127
|
},
|
|
1125
1128
|
prepEmail: {
|
|
1129
|
+
enabled: true,
|
|
1126
1130
|
subject: `Your ${name} intro call`,
|
|
1127
1131
|
text: `Hi {{firstName}},
|
|
1128
1132
|
|
|
@@ -1131,18 +1135,19 @@ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startA
|
|
|
1131
1135
|
Join here: {{meetingLink}}${sign}`
|
|
1132
1136
|
},
|
|
1133
1137
|
onboardingInvite: {
|
|
1138
|
+
enabled: true,
|
|
1134
1139
|
subject: `You're in, ${name}`,
|
|
1135
1140
|
text: `Hi {{firstName}},
|
|
1136
1141
|
|
|
1137
1142
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
1138
1143
|
},
|
|
1139
|
-
seatAccepted: SEAT_ACCEPTED_TEMPLATE
|
|
1144
|
+
seatAccepted: { ...SEAT_ACCEPTED_TEMPLATE, enabled: true }
|
|
1140
1145
|
};
|
|
1141
1146
|
}
|
|
1142
1147
|
function buildGroupSeed(config) {
|
|
1143
1148
|
const prices = config.prices;
|
|
1144
1149
|
const emails = config.emails;
|
|
1145
|
-
const
|
|
1150
|
+
const policy2 = config.policy ?? {};
|
|
1146
1151
|
const scheduling = {
|
|
1147
1152
|
...DEFAULT_SCHEDULING,
|
|
1148
1153
|
...config.scheduling ?? {},
|
|
@@ -1157,17 +1162,17 @@ function buildGroupSeed(config) {
|
|
|
1157
1162
|
interval: prices?.interval ?? "year",
|
|
1158
1163
|
notificationEmail: emails?.notificationEmail ?? "",
|
|
1159
1164
|
replyTo: emails?.replyTo ?? emails?.notificationEmail ?? "",
|
|
1160
|
-
disclaimerText:
|
|
1161
|
-
refundPolicyText:
|
|
1162
|
-
merchantDisclosureText:
|
|
1163
|
-
trustCopy:
|
|
1165
|
+
disclaimerText: policy2.disclaimerText ?? "",
|
|
1166
|
+
refundPolicyText: policy2.refundPolicyText ?? "",
|
|
1167
|
+
merchantDisclosureText: policy2.merchantDisclosureText ?? "",
|
|
1168
|
+
trustCopy: policy2.trustCopy ?? "",
|
|
1164
1169
|
emailTemplates: emails?.templates ?? defaultEmailTemplates(config.name),
|
|
1165
1170
|
schedulingJson: scheduling
|
|
1166
1171
|
};
|
|
1167
1172
|
if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
|
|
1168
1173
|
if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
|
|
1169
|
-
if (
|
|
1170
|
-
if (
|
|
1174
|
+
if (policy2.commitmentText) row2.commitmentText = policy2.commitmentText;
|
|
1175
|
+
if (policy2.normsText) row2.normsText = policy2.normsText;
|
|
1171
1176
|
return row2;
|
|
1172
1177
|
}
|
|
1173
1178
|
|
|
@@ -2476,13 +2481,13 @@ function firstPrice(object2) {
|
|
|
2476
2481
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2477
2482
|
}
|
|
2478
2483
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2479
|
-
const
|
|
2480
|
-
const eventId = providerId(
|
|
2481
|
-
const eventType = text(
|
|
2482
|
-
const object2 = raw(raw(
|
|
2484
|
+
const event2 = raw(input);
|
|
2485
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
2486
|
+
const eventType = text(event2.type) ?? "";
|
|
2487
|
+
const object2 = raw(raw(event2.data).object);
|
|
2483
2488
|
const objectId = providerId(object2.id) ?? "";
|
|
2484
2489
|
const candidates = metadataCandidates(object2);
|
|
2485
|
-
const
|
|
2490
|
+
const identity2 = {};
|
|
2486
2491
|
const identityKeys2 = {
|
|
2487
2492
|
appId: "odlaAppId",
|
|
2488
2493
|
chapterId: "chapterId",
|
|
@@ -2492,7 +2497,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2492
2497
|
};
|
|
2493
2498
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
2494
2499
|
const value = metadataValue(candidates, source);
|
|
2495
|
-
if (value)
|
|
2500
|
+
if (value) identity2[target] = value;
|
|
2496
2501
|
}
|
|
2497
2502
|
let objectKind = "ignored";
|
|
2498
2503
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -2514,11 +2519,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2514
2519
|
eventId,
|
|
2515
2520
|
digest,
|
|
2516
2521
|
eventType,
|
|
2517
|
-
eventCreated: integer(
|
|
2518
|
-
livemode:
|
|
2522
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
2523
|
+
livemode: event2.livemode === true,
|
|
2519
2524
|
objectKind,
|
|
2520
2525
|
objectId,
|
|
2521
|
-
identity,
|
|
2526
|
+
identity: identity2,
|
|
2522
2527
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2523
2528
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2524
2529
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -3365,15 +3370,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
3365
3370
|
// src/membership-offer.ts
|
|
3366
3371
|
function createMembershipCommercialOffer(input) {
|
|
3367
3372
|
const offering = membershipOfferingVersion(input.offering);
|
|
3368
|
-
const
|
|
3373
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
3369
3374
|
const now = input.now;
|
|
3370
3375
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
3371
3376
|
throw new TypeError("invalid offer clock or founding count");
|
|
3372
3377
|
}
|
|
3373
3378
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
3374
|
-
if (
|
|
3375
|
-
const founding = Boolean(
|
|
3376
|
-
const earned = founding ?
|
|
3379
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
3380
|
+
const founding = Boolean(policy2 && policy2.status === "published" && offering.basePriceCents > 0 && policy2.cohortEligibleSkuIds.includes(offering.baseSkuId) && now >= policy2.qualifyingStartsAt && now < policy2.qualifyingEndsAt && input.approvedFoundingCount < policy2.softTarget);
|
|
3381
|
+
const earned = founding ? policy2 : null;
|
|
3377
3382
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
3378
3383
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
3379
3384
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -3465,15 +3470,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
3465
3470
|
}
|
|
3466
3471
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3467
3472
|
const state = await authoritativeState(db, input);
|
|
3468
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3469
|
-
const approvedFoundingCount =
|
|
3473
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
3474
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3470
3475
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3471
3476
|
const contract = createMembershipCommercialOffer({
|
|
3472
3477
|
offering,
|
|
3473
|
-
discountPolicy:
|
|
3478
|
+
discountPolicy: policy2,
|
|
3474
3479
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3475
3480
|
now: input.now,
|
|
3476
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3481
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3477
3482
|
});
|
|
3478
3483
|
const discount = contract.billingTerms.discount;
|
|
3479
3484
|
return {
|
|
@@ -3502,12 +3507,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
3502
3507
|
});
|
|
3503
3508
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3504
3509
|
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3505
|
-
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
3510
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
|
|
3506
3511
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3507
3512
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3508
|
-
const
|
|
3509
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
3510
|
-
return { offering, policy };
|
|
3513
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3514
|
+
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy2 && policies.filter((item) => item.version === policy2.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3515
|
+
return { offering, policy: policy2 };
|
|
3511
3516
|
}
|
|
3512
3517
|
async function issueMembershipCommercialOffer(db, input) {
|
|
3513
3518
|
validate(input);
|
|
@@ -3515,15 +3520,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3515
3520
|
const state = await authoritativeState(db, input);
|
|
3516
3521
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3517
3522
|
if (existing) return restore(existing, input);
|
|
3518
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3519
|
-
const approvedFoundingCount =
|
|
3523
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
3524
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3520
3525
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3521
3526
|
const contract = createMembershipCommercialOffer({
|
|
3522
3527
|
offering,
|
|
3523
|
-
discountPolicy:
|
|
3528
|
+
discountPolicy: policy2,
|
|
3524
3529
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3525
3530
|
now: input.now,
|
|
3526
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3531
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3527
3532
|
});
|
|
3528
3533
|
const value = {
|
|
3529
3534
|
id: input.offerId,
|
|
@@ -5305,7 +5310,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5305
5310
|
};
|
|
5306
5311
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5307
5312
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5308
|
-
const
|
|
5313
|
+
const checkout2 = {
|
|
5309
5314
|
...target,
|
|
5310
5315
|
priceId: peer.seatPriceId,
|
|
5311
5316
|
recipientName: String(input.recipientName),
|
|
@@ -5313,7 +5318,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5313
5318
|
purchaserEmail: String(input.purchaserEmail),
|
|
5314
5319
|
purchaserUserId: String(input.purchaserUserId)
|
|
5315
5320
|
};
|
|
5316
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
5321
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
5317
5322
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5318
5323
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5319
5324
|
if (action === "seat-decline") {
|
|
@@ -5329,7 +5334,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5329
5334
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5330
5335
|
}
|
|
5331
5336
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5332
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5337
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
5333
5338
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5334
5339
|
if (action === "seat-payments") {
|
|
5335
5340
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5400,9 +5405,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5400
5405
|
const identities = /* @__PURE__ */ new Set();
|
|
5401
5406
|
for (const peer of peers) {
|
|
5402
5407
|
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
|
|
5403
|
-
const
|
|
5404
|
-
if (identities.has(
|
|
5405
|
-
identities.add(
|
|
5408
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
5409
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
5410
|
+
identities.add(identity2);
|
|
5406
5411
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
5407
5412
|
}
|
|
5408
5413
|
return async (req) => {
|
|
@@ -6714,6 +6719,41 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
6714
6719
|
return { document, digest: await builderLibraryDigest(document) };
|
|
6715
6720
|
}
|
|
6716
6721
|
|
|
6722
|
+
// src/email-retirement.ts
|
|
6723
|
+
function isRetiredEmailCopy(subject, text6) {
|
|
6724
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
6725
|
+
const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
6726
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
6727
|
+
}
|
|
6728
|
+
|
|
6729
|
+
// src/email.ts
|
|
6730
|
+
import { validateEmailCopy } from "@odla-ai/crm";
|
|
6731
|
+
|
|
6732
|
+
// src/email-events.ts
|
|
6733
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
6734
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
6735
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
6736
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
6737
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
6738
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
6739
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
6740
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
6741
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
6742
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
6743
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
6744
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
6745
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
6746
|
+
};
|
|
6747
|
+
function chapterEmailEvents(chapter) {
|
|
6748
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
6749
|
+
label: key3,
|
|
6750
|
+
trigger: "Administrator sends a message",
|
|
6751
|
+
audience: "Selected contact",
|
|
6752
|
+
variables: spec.vars
|
|
6753
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
6754
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
6755
|
+
}
|
|
6756
|
+
|
|
6717
6757
|
// src/email.ts
|
|
6718
6758
|
function render(template, vars) {
|
|
6719
6759
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -6737,19 +6777,158 @@ function isAlreadySent(priorRows) {
|
|
|
6737
6777
|
function planDelivery(input) {
|
|
6738
6778
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
6739
6779
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
6740
|
-
if (tpl.enabled
|
|
6780
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
6781
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
6782
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
6741
6783
|
const vars = groupVars(input.group, input.vars);
|
|
6784
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
6785
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
6786
|
+
const issues = validateEmailCopy(tpl, {
|
|
6787
|
+
allowedVariables: event2?.variables,
|
|
6788
|
+
optionalVariables: event2?.optionalVariables,
|
|
6789
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
6790
|
+
values: vars,
|
|
6791
|
+
tierId: vars.tierId
|
|
6792
|
+
});
|
|
6793
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
6742
6794
|
const isProd = input.envName === "prod";
|
|
6743
6795
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
6744
6796
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
6745
6797
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
6746
|
-
const
|
|
6798
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
6799
|
+
const renderedText = render(tpl.text, vars);
|
|
6800
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
6801
|
+
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
6802
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
6747
6803
|
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
6748
6804
|
|
|
6749
|
-
` +
|
|
6805
|
+
` + renderedText : renderedText;
|
|
6750
6806
|
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
6751
6807
|
}
|
|
6752
6808
|
|
|
6809
|
+
// src/email-readiness.ts
|
|
6810
|
+
import { isEmailTierName, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
6811
|
+
function validateEmailReadiness(input) {
|
|
6812
|
+
const issues = [];
|
|
6813
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
6814
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
6815
|
+
}
|
|
6816
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
6817
|
+
for (const tier of input.tiers ?? []) {
|
|
6818
|
+
if (!isEmailTierName(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
6819
|
+
}
|
|
6820
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => isEmailTierName(tier.name, tier.id));
|
|
6821
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
6822
|
+
const events2 = Object.entries(input.events).map(([key3, event2]) => {
|
|
6823
|
+
const template = input.templates[key3];
|
|
6824
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy2(template, { allowedVariables: event2.variables });
|
|
6825
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
6826
|
+
if (template) {
|
|
6827
|
+
const used = [...`${template.subject ?? ""}
|
|
6828
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
6829
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
6830
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
6834
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
6835
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
6836
|
+
});
|
|
6837
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
6838
|
+
}
|
|
6839
|
+
|
|
6840
|
+
// src/notify.ts
|
|
6841
|
+
import { validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
6842
|
+
|
|
6843
|
+
// src/email-event-values.ts
|
|
6844
|
+
import { isEmailTierName as isEmailTierName2 } from "@odla-ai/crm";
|
|
6845
|
+
|
|
6846
|
+
// src/tiers-store.ts
|
|
6847
|
+
function rowToTier(row2, groupId) {
|
|
6848
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || typeof row2.id !== "string" || !row2.id || row2.groupId !== void 0 && row2.groupId !== groupId || !Number.isSafeInteger(row2.priceCents) || Number(row2.priceCents) < 0 || row2.currency !== void 0 && (typeof row2.currency !== "string" || !/^[a-z]{3}$/.test(row2.currency)) || row2.interval !== void 0 && row2.interval !== "month" && row2.interval !== "year") {
|
|
6849
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
6850
|
+
}
|
|
6851
|
+
return {
|
|
6852
|
+
id: String(row2.id ?? ""),
|
|
6853
|
+
groupId: String(row2.groupId ?? groupId),
|
|
6854
|
+
name: String(row2.name ?? ""),
|
|
6855
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
6856
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
6857
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
6858
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
6859
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
6860
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
6861
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
6862
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
6863
|
+
active: row2.active !== false
|
|
6864
|
+
};
|
|
6865
|
+
}
|
|
6866
|
+
async function loadTiers(db, group) {
|
|
6867
|
+
const groupId = String(group.id ?? "");
|
|
6868
|
+
let rows3;
|
|
6869
|
+
try {
|
|
6870
|
+
rows3 = (await db.query({
|
|
6871
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
6872
|
+
})).tiers;
|
|
6873
|
+
} catch {
|
|
6874
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
6875
|
+
}
|
|
6876
|
+
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
6877
|
+
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
6878
|
+
return resolveTiers(group, list);
|
|
6879
|
+
}
|
|
6880
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
6881
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
// src/notification-variables.ts
|
|
6885
|
+
async function frozenCheckoutVariables(db, app) {
|
|
6886
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
6887
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
6888
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
6889
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
6890
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
6891
|
+
let formatted = "";
|
|
6892
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
6893
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
6894
|
+
}
|
|
6895
|
+
return {
|
|
6896
|
+
paymentAmount: formatted,
|
|
6897
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
6898
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
async function catalogTierName(db, group, app) {
|
|
6902
|
+
try {
|
|
6903
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
6904
|
+
} catch {
|
|
6905
|
+
return "";
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
async function checkoutVariables(db, group, app) {
|
|
6909
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
6910
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
// src/email-event-values.ts
|
|
6914
|
+
async function emailTierVariables(db, input) {
|
|
6915
|
+
try {
|
|
6916
|
+
const [apps, groups2] = await Promise.all([
|
|
6917
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
6918
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
6919
|
+
]);
|
|
6920
|
+
const app = apps.applications?.[0], group = groups2.groups?.[0];
|
|
6921
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
6922
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
6923
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
6924
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
6925
|
+
const tierName = isEmailTierName2(name, tierId) ? name.trim() : "";
|
|
6926
|
+
return { tierName, tier: tierName, tierId };
|
|
6927
|
+
} catch {
|
|
6928
|
+
return { tierName: "", tier: "" };
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6753
6932
|
// src/notify.ts
|
|
6754
6933
|
function fromAddress(configured, name) {
|
|
6755
6934
|
const display = name?.trim();
|
|
@@ -6767,14 +6946,17 @@ async function sendTemplated(deps, input) {
|
|
|
6767
6946
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
6768
6947
|
const override = deps.debugEmail?.trim();
|
|
6769
6948
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
6949
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
6950
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
6770
6951
|
const decision = planDelivery({
|
|
6771
6952
|
envName: deps.envName,
|
|
6772
6953
|
group,
|
|
6773
6954
|
template: input.template,
|
|
6774
6955
|
to: input.to,
|
|
6775
|
-
vars
|
|
6956
|
+
vars,
|
|
6776
6957
|
cloudflareReady,
|
|
6777
|
-
force: input.force
|
|
6958
|
+
force: input.force,
|
|
6959
|
+
event: event2
|
|
6778
6960
|
});
|
|
6779
6961
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
6780
6962
|
const previous = prior[0];
|
|
@@ -6799,6 +6981,8 @@ async function sendTemplated(deps, input) {
|
|
|
6799
6981
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
6800
6982
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
6801
6983
|
};
|
|
6984
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
6985
|
+
if (validateEmailCopy3({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
6802
6986
|
try {
|
|
6803
6987
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id10, attrs: row2 }], {
|
|
6804
6988
|
mutationId: `email-claim:${id10}:${attempt}`,
|
|
@@ -6952,35 +7136,35 @@ function findGiftRef(obj) {
|
|
|
6952
7136
|
...chapterId ? { chapterId } : {}
|
|
6953
7137
|
};
|
|
6954
7138
|
}
|
|
6955
|
-
function normalizeWebhookEvent(
|
|
6956
|
-
const obj =
|
|
6957
|
-
if (
|
|
7139
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
7140
|
+
const obj = event2.data?.object ?? {};
|
|
7141
|
+
if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
|
|
6958
7142
|
const gift = findGiftRef(obj);
|
|
6959
7143
|
if (gift.giftIntentId) {
|
|
6960
|
-
const { giftIntentId, providerObjectId, ...
|
|
6961
|
-
if (
|
|
6962
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
7144
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
7145
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
7146
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
6963
7147
|
}
|
|
6964
|
-
if (
|
|
6965
|
-
if (
|
|
6966
|
-
return { kind: "ignored", type:
|
|
7148
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
7149
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
7150
|
+
return { kind: "ignored", type: event2.type };
|
|
6967
7151
|
}
|
|
6968
7152
|
const ref = findApplicationRef(obj);
|
|
6969
7153
|
const seatId = obj.metadata?.seatId;
|
|
6970
7154
|
if (typeof seatId === "string" && seatId) {
|
|
6971
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
6972
|
-
if (
|
|
6973
|
-
if (
|
|
6974
|
-
return { kind: "ignored", type:
|
|
7155
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7156
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
7157
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
7158
|
+
return { kind: "ignored", type: event2.type };
|
|
6975
7159
|
}
|
|
6976
|
-
if (
|
|
6977
|
-
switch (
|
|
7160
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7161
|
+
switch (event2.type) {
|
|
6978
7162
|
case "invoice.payment_failed":
|
|
6979
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
7163
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
6980
7164
|
case "invoice.paid":
|
|
6981
7165
|
case "invoice.payment_succeeded": {
|
|
6982
7166
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
6983
|
-
return { kind: "ignored", type:
|
|
7167
|
+
return { kind: "ignored", type: event2.type };
|
|
6984
7168
|
}
|
|
6985
7169
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
6986
7170
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -6994,11 +7178,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
6994
7178
|
case "charge.refunded":
|
|
6995
7179
|
return { kind: "refunded", ...ref };
|
|
6996
7180
|
case "customer.subscription.updated":
|
|
6997
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
7181
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
6998
7182
|
case "customer.subscription.deleted":
|
|
6999
7183
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
7000
7184
|
default:
|
|
7001
|
-
return { kind: "ignored", type:
|
|
7185
|
+
return { kind: "ignored", type: event2.type };
|
|
7002
7186
|
}
|
|
7003
7187
|
}
|
|
7004
7188
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -8376,44 +8560,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
8376
8560
|
};
|
|
8377
8561
|
}
|
|
8378
8562
|
|
|
8379
|
-
// src/tiers-store.ts
|
|
8380
|
-
function rowToTier(row2, groupId) {
|
|
8381
|
-
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || typeof row2.id !== "string" || !row2.id || row2.groupId !== void 0 && row2.groupId !== groupId || !Number.isSafeInteger(row2.priceCents) || Number(row2.priceCents) < 0 || row2.currency !== void 0 && (typeof row2.currency !== "string" || !/^[a-z]{3}$/.test(row2.currency)) || row2.interval !== void 0 && row2.interval !== "month" && row2.interval !== "year") {
|
|
8382
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
8383
|
-
}
|
|
8384
|
-
return {
|
|
8385
|
-
id: String(row2.id ?? ""),
|
|
8386
|
-
groupId: String(row2.groupId ?? groupId),
|
|
8387
|
-
name: String(row2.name ?? ""),
|
|
8388
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
8389
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
8390
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
8391
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
8392
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
8393
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
8394
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
8395
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
8396
|
-
active: row2.active !== false
|
|
8397
|
-
};
|
|
8398
|
-
}
|
|
8399
|
-
async function loadTiers(db, group) {
|
|
8400
|
-
const groupId = String(group.id ?? "");
|
|
8401
|
-
let rows3;
|
|
8402
|
-
try {
|
|
8403
|
-
rows3 = (await db.query({
|
|
8404
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
8405
|
-
})).tiers;
|
|
8406
|
-
} catch {
|
|
8407
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
8408
|
-
}
|
|
8409
|
-
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
8410
|
-
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8411
|
-
return resolveTiers(group, list);
|
|
8412
|
-
}
|
|
8413
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
8414
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
8415
|
-
}
|
|
8416
|
-
|
|
8417
8563
|
// src/signup-control-contract.ts
|
|
8418
8564
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
8419
8565
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
@@ -8721,15 +8867,15 @@ function policyOf(value, errors, tiers2) {
|
|
|
8721
8867
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
8722
8868
|
else for (const [id10, raw2] of Object.entries(overrides)) {
|
|
8723
8869
|
const path = `document.policy.tierPolicies.${id10}`;
|
|
8724
|
-
const
|
|
8725
|
-
if (!tiers2.some((tier) => tier.id === id10) || !
|
|
8870
|
+
const policy2 = recordOf(raw2);
|
|
8871
|
+
if (!tiers2.some((tier) => tier.id === id10) || !policy2) {
|
|
8726
8872
|
errors.push(`${path} must name a declared tier and policy`);
|
|
8727
8873
|
continue;
|
|
8728
8874
|
}
|
|
8729
|
-
unknownKeys3(
|
|
8875
|
+
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
8730
8876
|
tierPolicies[id10] = {
|
|
8731
|
-
refundPolicyText: text5(
|
|
8732
|
-
...
|
|
8877
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8878
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8733
8879
|
};
|
|
8734
8880
|
}
|
|
8735
8881
|
}
|
|
@@ -8784,7 +8930,7 @@ function validateSignupControlDocument(input) {
|
|
|
8784
8930
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
8785
8931
|
}
|
|
8786
8932
|
const application = applicationOf(root.application, errors);
|
|
8787
|
-
const
|
|
8933
|
+
const policy2 = policyOf(root.policy, errors, tiers2);
|
|
8788
8934
|
let scheduling;
|
|
8789
8935
|
if (root.scheduling !== void 0) {
|
|
8790
8936
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -8804,7 +8950,7 @@ function validateSignupControlDocument(input) {
|
|
|
8804
8950
|
tiers: tiers2,
|
|
8805
8951
|
membershipPage: page.value,
|
|
8806
8952
|
...application ? { application } : {},
|
|
8807
|
-
...
|
|
8953
|
+
...policy2 ? { policy: policy2 } : {},
|
|
8808
8954
|
...scheduling ? { scheduling } : {},
|
|
8809
8955
|
...theme ? { theme } : {},
|
|
8810
8956
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -9519,23 +9665,23 @@ function memberSystemAvailability(input) {
|
|
|
9519
9665
|
...!input.bnfFrontendReachable ? { reason: "bnf_frontend_unavailable" } : {}
|
|
9520
9666
|
};
|
|
9521
9667
|
}
|
|
9522
|
-
function classifyMemberStripeEvent(
|
|
9668
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
9523
9669
|
const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
9524
|
-
const supplied = keys2.filter((key3) =>
|
|
9525
|
-
if (supplied.some((key3) =>
|
|
9670
|
+
const supplied = keys2.filter((key3) => identity2[key3] !== void 0);
|
|
9671
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
9526
9672
|
if (supplied.length === keys2.length) return "apply";
|
|
9527
9673
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
9528
9674
|
}
|
|
9529
9675
|
function planMemberSystemEventReplay(events2) {
|
|
9530
9676
|
const seen = /* @__PURE__ */ new Map();
|
|
9531
9677
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
9532
|
-
for (const
|
|
9533
|
-
const digest = seen.get(
|
|
9678
|
+
for (const event2 of events2) {
|
|
9679
|
+
const digest = seen.get(event2.id);
|
|
9534
9680
|
if (digest === void 0) {
|
|
9535
|
-
seen.set(
|
|
9536
|
-
plan.accepted.push(
|
|
9537
|
-
} else if (digest ===
|
|
9538
|
-
else plan.collisions.push(
|
|
9681
|
+
seen.set(event2.id, event2.digest);
|
|
9682
|
+
plan.accepted.push(event2.id);
|
|
9683
|
+
} else if (digest === event2.digest) plan.replayed.push(event2.id);
|
|
9684
|
+
else plan.collisions.push(event2.id);
|
|
9539
9685
|
}
|
|
9540
9686
|
return plan;
|
|
9541
9687
|
}
|
|
@@ -9615,6 +9761,7 @@ export {
|
|
|
9615
9761
|
BUILDER_LIBRARY_MANIFEST_PATH,
|
|
9616
9762
|
BUILDER_LIBRARY_PROMOTE_PATH,
|
|
9617
9763
|
BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
9764
|
+
CHAPTER_EMAIL_EVENTS,
|
|
9618
9765
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
9619
9766
|
CHAPTER_PROMOTION_ADAPTER,
|
|
9620
9767
|
DEFAULT_CHAPTER_COPY,
|
|
@@ -9671,6 +9818,7 @@ export {
|
|
|
9671
9818
|
canonicalSignupControl,
|
|
9672
9819
|
chapterBrandFromTokens,
|
|
9673
9820
|
chapterDb,
|
|
9821
|
+
chapterEmailEvents,
|
|
9674
9822
|
chapterFlowFieldContract,
|
|
9675
9823
|
clampArray,
|
|
9676
9824
|
classifyMemberStripeEvent,
|
|
@@ -9850,6 +9998,7 @@ export {
|
|
|
9850
9998
|
updateClerkUserMetadataByEmail,
|
|
9851
9999
|
validateAdmissionGrantDocument,
|
|
9852
10000
|
validateBuilderLibraryDocument,
|
|
10001
|
+
validateEmailReadiness,
|
|
9853
10002
|
validateGiftOfferCatalog,
|
|
9854
10003
|
validateMemberContentDocument,
|
|
9855
10004
|
validateMemberSystemAuthority,
|