@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.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(src_exports, {
|
|
|
27
27
|
BUILDER_LIBRARY_MANIFEST_PATH: () => BUILDER_LIBRARY_MANIFEST_PATH,
|
|
28
28
|
BUILDER_LIBRARY_PROMOTE_PATH: () => BUILDER_LIBRARY_PROMOTE_PATH,
|
|
29
29
|
BUILDER_LIBRARY_SCHEMA_VERSION: () => BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
30
|
+
CHAPTER_EMAIL_EVENTS: () => CHAPTER_EMAIL_EVENTS,
|
|
30
31
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
31
32
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
32
33
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
@@ -83,6 +84,7 @@ __export(src_exports, {
|
|
|
83
84
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
84
85
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
85
86
|
chapterDb: () => chapterDb,
|
|
87
|
+
chapterEmailEvents: () => chapterEmailEvents,
|
|
86
88
|
chapterFlowFieldContract: () => chapterFlowFieldContract,
|
|
87
89
|
clampArray: () => clampArray,
|
|
88
90
|
classifyMemberStripeEvent: () => classifyMemberStripeEvent,
|
|
@@ -262,6 +264,7 @@ __export(src_exports, {
|
|
|
262
264
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
263
265
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
264
266
|
validateBuilderLibraryDocument: () => validateBuilderLibraryDocument,
|
|
267
|
+
validateEmailReadiness: () => validateEmailReadiness,
|
|
265
268
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
266
269
|
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
267
270
|
validateMemberSystemAuthority: () => validateMemberSystemAuthority,
|
|
@@ -1380,6 +1383,7 @@ Warmly,
|
|
|
1380
1383
|
${name}`;
|
|
1381
1384
|
return {
|
|
1382
1385
|
adminNotification: {
|
|
1386
|
+
enabled: true,
|
|
1383
1387
|
subject: `New application: {{firstName}} {{lastName}}`,
|
|
1384
1388
|
text: `A new application came in for ${name}.
|
|
1385
1389
|
|
|
@@ -1390,18 +1394,21 @@ Tier: {{tier}}
|
|
|
1390
1394
|
Review it: {{adminUrl}}`
|
|
1391
1395
|
},
|
|
1392
1396
|
submitConfirmation: {
|
|
1397
|
+
enabled: true,
|
|
1393
1398
|
subject: `We received your application to ${name}`,
|
|
1394
1399
|
text: `Hi {{firstName}},
|
|
1395
1400
|
|
|
1396
1401
|
Thanks for applying to ${name}. {{submitNextStep}}${sign}`
|
|
1397
1402
|
},
|
|
1398
1403
|
paymentConfirmation: {
|
|
1404
|
+
enabled: true,
|
|
1399
1405
|
subject: `Welcome to ${name}`,
|
|
1400
1406
|
text: `Hi {{firstName}},
|
|
1401
1407
|
|
|
1402
1408
|
Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}}${sign}`
|
|
1403
1409
|
},
|
|
1404
1410
|
prepEmail: {
|
|
1411
|
+
enabled: true,
|
|
1405
1412
|
subject: `Your ${name} intro call`,
|
|
1406
1413
|
text: `Hi {{firstName}},
|
|
1407
1414
|
|
|
@@ -1410,18 +1417,19 @@ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startA
|
|
|
1410
1417
|
Join here: {{meetingLink}}${sign}`
|
|
1411
1418
|
},
|
|
1412
1419
|
onboardingInvite: {
|
|
1420
|
+
enabled: true,
|
|
1413
1421
|
subject: `You're in, ${name}`,
|
|
1414
1422
|
text: `Hi {{firstName}},
|
|
1415
1423
|
|
|
1416
1424
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
1417
1425
|
},
|
|
1418
|
-
seatAccepted: SEAT_ACCEPTED_TEMPLATE
|
|
1426
|
+
seatAccepted: { ...SEAT_ACCEPTED_TEMPLATE, enabled: true }
|
|
1419
1427
|
};
|
|
1420
1428
|
}
|
|
1421
1429
|
function buildGroupSeed(config) {
|
|
1422
1430
|
const prices = config.prices;
|
|
1423
1431
|
const emails = config.emails;
|
|
1424
|
-
const
|
|
1432
|
+
const policy2 = config.policy ?? {};
|
|
1425
1433
|
const scheduling = {
|
|
1426
1434
|
...DEFAULT_SCHEDULING,
|
|
1427
1435
|
...config.scheduling ?? {},
|
|
@@ -1436,17 +1444,17 @@ function buildGroupSeed(config) {
|
|
|
1436
1444
|
interval: prices?.interval ?? "year",
|
|
1437
1445
|
notificationEmail: emails?.notificationEmail ?? "",
|
|
1438
1446
|
replyTo: emails?.replyTo ?? emails?.notificationEmail ?? "",
|
|
1439
|
-
disclaimerText:
|
|
1440
|
-
refundPolicyText:
|
|
1441
|
-
merchantDisclosureText:
|
|
1442
|
-
trustCopy:
|
|
1447
|
+
disclaimerText: policy2.disclaimerText ?? "",
|
|
1448
|
+
refundPolicyText: policy2.refundPolicyText ?? "",
|
|
1449
|
+
merchantDisclosureText: policy2.merchantDisclosureText ?? "",
|
|
1450
|
+
trustCopy: policy2.trustCopy ?? "",
|
|
1443
1451
|
emailTemplates: emails?.templates ?? defaultEmailTemplates(config.name),
|
|
1444
1452
|
schedulingJson: scheduling
|
|
1445
1453
|
};
|
|
1446
1454
|
if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
|
|
1447
1455
|
if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
|
|
1448
|
-
if (
|
|
1449
|
-
if (
|
|
1456
|
+
if (policy2.commitmentText) row2.commitmentText = policy2.commitmentText;
|
|
1457
|
+
if (policy2.normsText) row2.normsText = policy2.normsText;
|
|
1450
1458
|
return row2;
|
|
1451
1459
|
}
|
|
1452
1460
|
|
|
@@ -2755,13 +2763,13 @@ function firstPrice(object2) {
|
|
|
2755
2763
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2756
2764
|
}
|
|
2757
2765
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2758
|
-
const
|
|
2759
|
-
const eventId = providerId(
|
|
2760
|
-
const eventType = text(
|
|
2761
|
-
const object2 = raw(raw(
|
|
2766
|
+
const event2 = raw(input);
|
|
2767
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
2768
|
+
const eventType = text(event2.type) ?? "";
|
|
2769
|
+
const object2 = raw(raw(event2.data).object);
|
|
2762
2770
|
const objectId = providerId(object2.id) ?? "";
|
|
2763
2771
|
const candidates = metadataCandidates(object2);
|
|
2764
|
-
const
|
|
2772
|
+
const identity2 = {};
|
|
2765
2773
|
const identityKeys2 = {
|
|
2766
2774
|
appId: "odlaAppId",
|
|
2767
2775
|
chapterId: "chapterId",
|
|
@@ -2771,7 +2779,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2771
2779
|
};
|
|
2772
2780
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
2773
2781
|
const value = metadataValue(candidates, source);
|
|
2774
|
-
if (value)
|
|
2782
|
+
if (value) identity2[target] = value;
|
|
2775
2783
|
}
|
|
2776
2784
|
let objectKind = "ignored";
|
|
2777
2785
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -2793,11 +2801,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2793
2801
|
eventId,
|
|
2794
2802
|
digest,
|
|
2795
2803
|
eventType,
|
|
2796
|
-
eventCreated: integer(
|
|
2797
|
-
livemode:
|
|
2804
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
2805
|
+
livemode: event2.livemode === true,
|
|
2798
2806
|
objectKind,
|
|
2799
2807
|
objectId,
|
|
2800
|
-
identity,
|
|
2808
|
+
identity: identity2,
|
|
2801
2809
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2802
2810
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2803
2811
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -3644,15 +3652,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
3644
3652
|
// src/membership-offer.ts
|
|
3645
3653
|
function createMembershipCommercialOffer(input) {
|
|
3646
3654
|
const offering = membershipOfferingVersion(input.offering);
|
|
3647
|
-
const
|
|
3655
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
3648
3656
|
const now = input.now;
|
|
3649
3657
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
3650
3658
|
throw new TypeError("invalid offer clock or founding count");
|
|
3651
3659
|
}
|
|
3652
3660
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
3653
|
-
if (
|
|
3654
|
-
const founding = Boolean(
|
|
3655
|
-
const earned = founding ?
|
|
3661
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
3662
|
+
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);
|
|
3663
|
+
const earned = founding ? policy2 : null;
|
|
3656
3664
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
3657
3665
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
3658
3666
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -3744,15 +3752,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
3744
3752
|
}
|
|
3745
3753
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3746
3754
|
const state = await authoritativeState(db, input);
|
|
3747
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3748
|
-
const approvedFoundingCount =
|
|
3755
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
3756
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3749
3757
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3750
3758
|
const contract = createMembershipCommercialOffer({
|
|
3751
3759
|
offering,
|
|
3752
|
-
discountPolicy:
|
|
3760
|
+
discountPolicy: policy2,
|
|
3753
3761
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3754
3762
|
now: input.now,
|
|
3755
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3763
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3756
3764
|
});
|
|
3757
3765
|
const discount = contract.billingTerms.discount;
|
|
3758
3766
|
return {
|
|
@@ -3781,12 +3789,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
3781
3789
|
});
|
|
3782
3790
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3783
3791
|
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));
|
|
3784
|
-
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
3792
|
+
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);
|
|
3785
3793
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3786
3794
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3787
|
-
const
|
|
3788
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
3789
|
-
return { offering, policy };
|
|
3795
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3796
|
+
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");
|
|
3797
|
+
return { offering, policy: policy2 };
|
|
3790
3798
|
}
|
|
3791
3799
|
async function issueMembershipCommercialOffer(db, input) {
|
|
3792
3800
|
validate(input);
|
|
@@ -3794,15 +3802,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3794
3802
|
const state = await authoritativeState(db, input);
|
|
3795
3803
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3796
3804
|
if (existing) return restore(existing, input);
|
|
3797
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3798
|
-
const approvedFoundingCount =
|
|
3805
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
3806
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3799
3807
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3800
3808
|
const contract = createMembershipCommercialOffer({
|
|
3801
3809
|
offering,
|
|
3802
|
-
discountPolicy:
|
|
3810
|
+
discountPolicy: policy2,
|
|
3803
3811
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3804
3812
|
now: input.now,
|
|
3805
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3813
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3806
3814
|
});
|
|
3807
3815
|
const value = {
|
|
3808
3816
|
id: input.offerId,
|
|
@@ -5584,7 +5592,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5584
5592
|
};
|
|
5585
5593
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5586
5594
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5587
|
-
const
|
|
5595
|
+
const checkout2 = {
|
|
5588
5596
|
...target,
|
|
5589
5597
|
priceId: peer.seatPriceId,
|
|
5590
5598
|
recipientName: String(input.recipientName),
|
|
@@ -5592,7 +5600,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5592
5600
|
purchaserEmail: String(input.purchaserEmail),
|
|
5593
5601
|
purchaserUserId: String(input.purchaserUserId)
|
|
5594
5602
|
};
|
|
5595
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
5603
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
5596
5604
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5597
5605
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5598
5606
|
if (action === "seat-decline") {
|
|
@@ -5608,7 +5616,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5608
5616
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5609
5617
|
}
|
|
5610
5618
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5611
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5619
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
5612
5620
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5613
5621
|
if (action === "seat-payments") {
|
|
5614
5622
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5679,9 +5687,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5679
5687
|
const identities = /* @__PURE__ */ new Set();
|
|
5680
5688
|
for (const peer of peers) {
|
|
5681
5689
|
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");
|
|
5682
|
-
const
|
|
5683
|
-
if (identities.has(
|
|
5684
|
-
identities.add(
|
|
5690
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
5691
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
5692
|
+
identities.add(identity2);
|
|
5685
5693
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
5686
5694
|
}
|
|
5687
5695
|
return async (req) => {
|
|
@@ -6993,6 +7001,41 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
6993
7001
|
return { document, digest: await builderLibraryDigest(document) };
|
|
6994
7002
|
}
|
|
6995
7003
|
|
|
7004
|
+
// src/email-retirement.ts
|
|
7005
|
+
function isRetiredEmailCopy(subject, text6) {
|
|
7006
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
7007
|
+
const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
7008
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
7009
|
+
}
|
|
7010
|
+
|
|
7011
|
+
// src/email.ts
|
|
7012
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
7013
|
+
|
|
7014
|
+
// src/email-events.ts
|
|
7015
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
7016
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
7017
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
7018
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
7019
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
7020
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
7021
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
7022
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
7023
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
7024
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
7025
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
7026
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
7027
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
7028
|
+
};
|
|
7029
|
+
function chapterEmailEvents(chapter) {
|
|
7030
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
7031
|
+
label: key3,
|
|
7032
|
+
trigger: "Administrator sends a message",
|
|
7033
|
+
audience: "Selected contact",
|
|
7034
|
+
variables: spec.vars
|
|
7035
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
7036
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6996
7039
|
// src/email.ts
|
|
6997
7040
|
function render(template, vars) {
|
|
6998
7041
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -7016,19 +7059,158 @@ function isAlreadySent(priorRows) {
|
|
|
7016
7059
|
function planDelivery(input) {
|
|
7017
7060
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
7018
7061
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
7019
|
-
if (tpl.enabled
|
|
7062
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
7063
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
7064
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
7020
7065
|
const vars = groupVars(input.group, input.vars);
|
|
7066
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
7067
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7068
|
+
const issues = (0, import_crm5.validateEmailCopy)(tpl, {
|
|
7069
|
+
allowedVariables: event2?.variables,
|
|
7070
|
+
optionalVariables: event2?.optionalVariables,
|
|
7071
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
7072
|
+
values: vars,
|
|
7073
|
+
tierId: vars.tierId
|
|
7074
|
+
});
|
|
7075
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
7021
7076
|
const isProd = input.envName === "prod";
|
|
7022
7077
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
7023
7078
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
7024
7079
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
7025
|
-
const
|
|
7080
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
7081
|
+
const renderedText = render(tpl.text, vars);
|
|
7082
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
7083
|
+
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
7084
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
7026
7085
|
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
7027
7086
|
|
|
7028
|
-
` +
|
|
7087
|
+
` + renderedText : renderedText;
|
|
7029
7088
|
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
7030
7089
|
}
|
|
7031
7090
|
|
|
7091
|
+
// src/email-readiness.ts
|
|
7092
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
7093
|
+
function validateEmailReadiness(input) {
|
|
7094
|
+
const issues = [];
|
|
7095
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
7096
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
7097
|
+
}
|
|
7098
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
7099
|
+
for (const tier of input.tiers ?? []) {
|
|
7100
|
+
if (!(0, import_crm6.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
7101
|
+
}
|
|
7102
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm6.isEmailTierName)(tier.name, tier.id));
|
|
7103
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
7104
|
+
const events2 = Object.entries(input.events).map(([key3, event2]) => {
|
|
7105
|
+
const template = input.templates[key3];
|
|
7106
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm6.validateEmailCopy)(template, { allowedVariables: event2.variables });
|
|
7107
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
7108
|
+
if (template) {
|
|
7109
|
+
const used = [...`${template.subject ?? ""}
|
|
7110
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
7111
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
7112
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7116
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7117
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
7118
|
+
});
|
|
7119
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7120
|
+
}
|
|
7121
|
+
|
|
7122
|
+
// src/notify.ts
|
|
7123
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
7124
|
+
|
|
7125
|
+
// src/email-event-values.ts
|
|
7126
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
7127
|
+
|
|
7128
|
+
// src/tiers-store.ts
|
|
7129
|
+
function rowToTier(row2, groupId) {
|
|
7130
|
+
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") {
|
|
7131
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
7132
|
+
}
|
|
7133
|
+
return {
|
|
7134
|
+
id: String(row2.id ?? ""),
|
|
7135
|
+
groupId: String(row2.groupId ?? groupId),
|
|
7136
|
+
name: String(row2.name ?? ""),
|
|
7137
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
7138
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
7139
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
7140
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
7141
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
7142
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
7143
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
7144
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
7145
|
+
active: row2.active !== false
|
|
7146
|
+
};
|
|
7147
|
+
}
|
|
7148
|
+
async function loadTiers(db, group) {
|
|
7149
|
+
const groupId = String(group.id ?? "");
|
|
7150
|
+
let rows3;
|
|
7151
|
+
try {
|
|
7152
|
+
rows3 = (await db.query({
|
|
7153
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
7154
|
+
})).tiers;
|
|
7155
|
+
} catch {
|
|
7156
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
7157
|
+
}
|
|
7158
|
+
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
7159
|
+
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
7160
|
+
return resolveTiers(group, list);
|
|
7161
|
+
}
|
|
7162
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
7163
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// src/notification-variables.ts
|
|
7167
|
+
async function frozenCheckoutVariables(db, app) {
|
|
7168
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
7169
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
7170
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
7171
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
7172
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
7173
|
+
let formatted = "";
|
|
7174
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
7175
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
7176
|
+
}
|
|
7177
|
+
return {
|
|
7178
|
+
paymentAmount: formatted,
|
|
7179
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
7180
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
7181
|
+
};
|
|
7182
|
+
}
|
|
7183
|
+
async function catalogTierName(db, group, app) {
|
|
7184
|
+
try {
|
|
7185
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
7186
|
+
} catch {
|
|
7187
|
+
return "";
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
async function checkoutVariables(db, group, app) {
|
|
7191
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
7192
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7195
|
+
// src/email-event-values.ts
|
|
7196
|
+
async function emailTierVariables(db, input) {
|
|
7197
|
+
try {
|
|
7198
|
+
const [apps, groups2] = await Promise.all([
|
|
7199
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
7200
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
7201
|
+
]);
|
|
7202
|
+
const app = apps.applications?.[0], group = groups2.groups?.[0];
|
|
7203
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
7204
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
7205
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
7206
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
7207
|
+
const tierName = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
|
|
7208
|
+
return { tierName, tier: tierName, tierId };
|
|
7209
|
+
} catch {
|
|
7210
|
+
return { tierName: "", tier: "" };
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
|
|
7032
7214
|
// src/notify.ts
|
|
7033
7215
|
function fromAddress(configured, name) {
|
|
7034
7216
|
const display = name?.trim();
|
|
@@ -7046,14 +7228,17 @@ async function sendTemplated(deps, input) {
|
|
|
7046
7228
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
7047
7229
|
const override = deps.debugEmail?.trim();
|
|
7048
7230
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
7231
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7232
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
7049
7233
|
const decision = planDelivery({
|
|
7050
7234
|
envName: deps.envName,
|
|
7051
7235
|
group,
|
|
7052
7236
|
template: input.template,
|
|
7053
7237
|
to: input.to,
|
|
7054
|
-
vars
|
|
7238
|
+
vars,
|
|
7055
7239
|
cloudflareReady,
|
|
7056
|
-
force: input.force
|
|
7240
|
+
force: input.force,
|
|
7241
|
+
event: event2
|
|
7057
7242
|
});
|
|
7058
7243
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
7059
7244
|
const previous = prior[0];
|
|
@@ -7078,6 +7263,8 @@ async function sendTemplated(deps, input) {
|
|
|
7078
7263
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
7079
7264
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
7080
7265
|
};
|
|
7266
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
7267
|
+
if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
7081
7268
|
try {
|
|
7082
7269
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id10, attrs: row2 }], {
|
|
7083
7270
|
mutationId: `email-claim:${id10}:${attempt}`,
|
|
@@ -7231,35 +7418,35 @@ function findGiftRef(obj) {
|
|
|
7231
7418
|
...chapterId ? { chapterId } : {}
|
|
7232
7419
|
};
|
|
7233
7420
|
}
|
|
7234
|
-
function normalizeWebhookEvent(
|
|
7235
|
-
const obj =
|
|
7236
|
-
if (
|
|
7421
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
7422
|
+
const obj = event2.data?.object ?? {};
|
|
7423
|
+
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 };
|
|
7237
7424
|
const gift = findGiftRef(obj);
|
|
7238
7425
|
if (gift.giftIntentId) {
|
|
7239
|
-
const { giftIntentId, providerObjectId, ...
|
|
7240
|
-
if (
|
|
7241
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
7426
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
7427
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
7428
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
7242
7429
|
}
|
|
7243
|
-
if (
|
|
7244
|
-
if (
|
|
7245
|
-
return { kind: "ignored", type:
|
|
7430
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
7431
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
7432
|
+
return { kind: "ignored", type: event2.type };
|
|
7246
7433
|
}
|
|
7247
7434
|
const ref = findApplicationRef(obj);
|
|
7248
7435
|
const seatId = obj.metadata?.seatId;
|
|
7249
7436
|
if (typeof seatId === "string" && seatId) {
|
|
7250
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
7251
|
-
if (
|
|
7252
|
-
if (
|
|
7253
|
-
return { kind: "ignored", type:
|
|
7437
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7438
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
7439
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
7440
|
+
return { kind: "ignored", type: event2.type };
|
|
7254
7441
|
}
|
|
7255
|
-
if (
|
|
7256
|
-
switch (
|
|
7442
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7443
|
+
switch (event2.type) {
|
|
7257
7444
|
case "invoice.payment_failed":
|
|
7258
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
7445
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
7259
7446
|
case "invoice.paid":
|
|
7260
7447
|
case "invoice.payment_succeeded": {
|
|
7261
7448
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
7262
|
-
return { kind: "ignored", type:
|
|
7449
|
+
return { kind: "ignored", type: event2.type };
|
|
7263
7450
|
}
|
|
7264
7451
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
7265
7452
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -7273,11 +7460,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
7273
7460
|
case "charge.refunded":
|
|
7274
7461
|
return { kind: "refunded", ...ref };
|
|
7275
7462
|
case "customer.subscription.updated":
|
|
7276
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
7463
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
7277
7464
|
case "customer.subscription.deleted":
|
|
7278
7465
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
7279
7466
|
default:
|
|
7280
|
-
return { kind: "ignored", type:
|
|
7467
|
+
return { kind: "ignored", type: event2.type };
|
|
7281
7468
|
}
|
|
7282
7469
|
}
|
|
7283
7470
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -7625,10 +7812,10 @@ function findGiftOffer(catalog, offerId, now) {
|
|
|
7625
7812
|
}
|
|
7626
7813
|
|
|
7627
7814
|
// src/network.ts
|
|
7628
|
-
var
|
|
7815
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
7629
7816
|
|
|
7630
7817
|
// src/crm-person-projection.ts
|
|
7631
|
-
var
|
|
7818
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
7632
7819
|
async function ensureProjectedPerson(deps, input, options = {}) {
|
|
7633
7820
|
const email = String(input.email ?? "").trim().toLowerCase();
|
|
7634
7821
|
if (!email) throw new Error("crm_projection_email_required");
|
|
@@ -7646,7 +7833,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
7646
7833
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
7647
7834
|
const id10 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7648
7835
|
try {
|
|
7649
|
-
const created = await (0,
|
|
7836
|
+
const created = await (0, import_crm9.createRecord)({ ...deps, db: deps.db }, {
|
|
7650
7837
|
id: id10,
|
|
7651
7838
|
type: "person",
|
|
7652
7839
|
input: { ...input, email },
|
|
@@ -7662,7 +7849,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
7662
7849
|
}
|
|
7663
7850
|
|
|
7664
7851
|
// src/applicant-projection.ts
|
|
7665
|
-
var
|
|
7852
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
7666
7853
|
|
|
7667
7854
|
// src/crm-application-fields.ts
|
|
7668
7855
|
function applicationCrmFields(crm, app) {
|
|
@@ -7681,10 +7868,10 @@ async function upsertPerson(deps, opts) {
|
|
|
7681
7868
|
const email = opts.email.toLowerCase();
|
|
7682
7869
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
7683
7870
|
const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
|
|
7684
|
-
if (!created) await (0,
|
|
7871
|
+
if (!created) await (0, import_crm10.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
|
|
7685
7872
|
const initial = deps.crm.initialStage("person");
|
|
7686
7873
|
if (!created && record4.stage == null && initial) {
|
|
7687
|
-
await (0,
|
|
7874
|
+
await (0, import_crm10.setStage)(crmDeps, {
|
|
7688
7875
|
id: String(record4.id),
|
|
7689
7876
|
to: initial,
|
|
7690
7877
|
authorId: "system",
|
|
@@ -7807,7 +7994,7 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
7807
7994
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
7808
7995
|
}
|
|
7809
7996
|
async function findSharedRecord(deps, record4, tag) {
|
|
7810
|
-
const structured = await (0,
|
|
7997
|
+
const structured = await (0, import_crm11.getRecordByOrigin)(
|
|
7811
7998
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
7812
7999
|
record4.sourceId,
|
|
7813
8000
|
record4.sourceRecordId
|
|
@@ -7859,15 +8046,15 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7859
8046
|
const existing = await findSharedRecord(deps, record4, tag);
|
|
7860
8047
|
let recordId;
|
|
7861
8048
|
if (existing && typeof existing.id === "string") {
|
|
7862
|
-
await (0,
|
|
8049
|
+
await (0, import_crm11.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
|
|
7863
8050
|
recordId = existing.id;
|
|
7864
8051
|
} else if (record4.type === "person" && typeof record4.input.email === "string") {
|
|
7865
8052
|
const projected = await ensureProjectedPerson(deps, record4.input);
|
|
7866
8053
|
recordId = String(projected.record.id);
|
|
7867
|
-
if (!projected.created) await (0,
|
|
8054
|
+
if (!projected.created) await (0, import_crm11.updateRecord)(crmDeps, { id: recordId, input: record4.input });
|
|
7868
8055
|
} else {
|
|
7869
8056
|
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
7870
|
-
await (0,
|
|
8057
|
+
await (0, import_crm11.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
7871
8058
|
type: record4.type,
|
|
7872
8059
|
input: record4.input,
|
|
7873
8060
|
mutationId: `share-create:${tag}`
|
|
@@ -7877,7 +8064,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7877
8064
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
7878
8065
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
7879
8066
|
);
|
|
7880
|
-
await (0,
|
|
8067
|
+
await (0, import_crm11.upsertRecordOrigin)(crmDeps, {
|
|
7881
8068
|
recordId,
|
|
7882
8069
|
sourceId: record4.sourceId,
|
|
7883
8070
|
sourceRecordId: record4.sourceRecordId,
|
|
@@ -7888,7 +8075,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7888
8075
|
}
|
|
7889
8076
|
|
|
7890
8077
|
// src/crm-sync.ts
|
|
7891
|
-
var
|
|
8078
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
7892
8079
|
|
|
7893
8080
|
// src/crm-account-projection.ts
|
|
7894
8081
|
async function syncAccountToCrm(deps, userId) {
|
|
@@ -7973,9 +8160,9 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
7973
8160
|
const stage = opts.stage || void 0;
|
|
7974
8161
|
const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
|
|
7975
8162
|
const recordId = String(existing.id);
|
|
7976
|
-
if (!created) await (0,
|
|
8163
|
+
if (!created) await (0, import_crm12.updateRecord)(recordDeps, { id: recordId, input });
|
|
7977
8164
|
if (!created && stage && existing.stage !== stage) {
|
|
7978
|
-
await (0,
|
|
8165
|
+
await (0, import_crm12.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
|
|
7979
8166
|
}
|
|
7980
8167
|
const billing = billingColumns(opts.app);
|
|
7981
8168
|
await deps.db.transact([
|
|
@@ -7983,7 +8170,7 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
7983
8170
|
// Optional typed dates are removed with a retract, never an invalid null.
|
|
7984
8171
|
...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
|
|
7985
8172
|
]);
|
|
7986
|
-
await (0,
|
|
8173
|
+
await (0, import_crm12.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
7987
8174
|
return recordId;
|
|
7988
8175
|
}
|
|
7989
8176
|
async function backfillCrm(deps) {
|
|
@@ -8650,44 +8837,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
8650
8837
|
};
|
|
8651
8838
|
}
|
|
8652
8839
|
|
|
8653
|
-
// src/tiers-store.ts
|
|
8654
|
-
function rowToTier(row2, groupId) {
|
|
8655
|
-
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") {
|
|
8656
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
8657
|
-
}
|
|
8658
|
-
return {
|
|
8659
|
-
id: String(row2.id ?? ""),
|
|
8660
|
-
groupId: String(row2.groupId ?? groupId),
|
|
8661
|
-
name: String(row2.name ?? ""),
|
|
8662
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
8663
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
8664
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
8665
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
8666
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
8667
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
8668
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
8669
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
8670
|
-
active: row2.active !== false
|
|
8671
|
-
};
|
|
8672
|
-
}
|
|
8673
|
-
async function loadTiers(db, group) {
|
|
8674
|
-
const groupId = String(group.id ?? "");
|
|
8675
|
-
let rows3;
|
|
8676
|
-
try {
|
|
8677
|
-
rows3 = (await db.query({
|
|
8678
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
8679
|
-
})).tiers;
|
|
8680
|
-
} catch {
|
|
8681
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
8682
|
-
}
|
|
8683
|
-
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
8684
|
-
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8685
|
-
return resolveTiers(group, list);
|
|
8686
|
-
}
|
|
8687
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
8688
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
8689
|
-
}
|
|
8690
|
-
|
|
8691
8840
|
// src/signup-control-contract.ts
|
|
8692
8841
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
8693
8842
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
@@ -8995,15 +9144,15 @@ function policyOf(value, errors, tiers2) {
|
|
|
8995
9144
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
8996
9145
|
else for (const [id10, raw2] of Object.entries(overrides)) {
|
|
8997
9146
|
const path = `document.policy.tierPolicies.${id10}`;
|
|
8998
|
-
const
|
|
8999
|
-
if (!tiers2.some((tier) => tier.id === id10) || !
|
|
9147
|
+
const policy2 = recordOf(raw2);
|
|
9148
|
+
if (!tiers2.some((tier) => tier.id === id10) || !policy2) {
|
|
9000
9149
|
errors.push(`${path} must name a declared tier and policy`);
|
|
9001
9150
|
continue;
|
|
9002
9151
|
}
|
|
9003
|
-
unknownKeys3(
|
|
9152
|
+
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
9004
9153
|
tierPolicies[id10] = {
|
|
9005
|
-
refundPolicyText: text5(
|
|
9006
|
-
...
|
|
9154
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
9155
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
9007
9156
|
};
|
|
9008
9157
|
}
|
|
9009
9158
|
}
|
|
@@ -9058,7 +9207,7 @@ function validateSignupControlDocument(input) {
|
|
|
9058
9207
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
9059
9208
|
}
|
|
9060
9209
|
const application = applicationOf(root.application, errors);
|
|
9061
|
-
const
|
|
9210
|
+
const policy2 = policyOf(root.policy, errors, tiers2);
|
|
9062
9211
|
let scheduling;
|
|
9063
9212
|
if (root.scheduling !== void 0) {
|
|
9064
9213
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -9078,7 +9227,7 @@ function validateSignupControlDocument(input) {
|
|
|
9078
9227
|
tiers: tiers2,
|
|
9079
9228
|
membershipPage: page.value,
|
|
9080
9229
|
...application ? { application } : {},
|
|
9081
|
-
...
|
|
9230
|
+
...policy2 ? { policy: policy2 } : {},
|
|
9082
9231
|
...scheduling ? { scheduling } : {},
|
|
9083
9232
|
...theme ? { theme } : {},
|
|
9084
9233
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -9791,23 +9940,23 @@ function memberSystemAvailability(input) {
|
|
|
9791
9940
|
...!input.bnfFrontendReachable ? { reason: "bnf_frontend_unavailable" } : {}
|
|
9792
9941
|
};
|
|
9793
9942
|
}
|
|
9794
|
-
function classifyMemberStripeEvent(
|
|
9943
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
9795
9944
|
const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
9796
|
-
const supplied = keys2.filter((key3) =>
|
|
9797
|
-
if (supplied.some((key3) =>
|
|
9945
|
+
const supplied = keys2.filter((key3) => identity2[key3] !== void 0);
|
|
9946
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
9798
9947
|
if (supplied.length === keys2.length) return "apply";
|
|
9799
9948
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
9800
9949
|
}
|
|
9801
9950
|
function planMemberSystemEventReplay(events2) {
|
|
9802
9951
|
const seen = /* @__PURE__ */ new Map();
|
|
9803
9952
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
9804
|
-
for (const
|
|
9805
|
-
const digest = seen.get(
|
|
9953
|
+
for (const event2 of events2) {
|
|
9954
|
+
const digest = seen.get(event2.id);
|
|
9806
9955
|
if (digest === void 0) {
|
|
9807
|
-
seen.set(
|
|
9808
|
-
plan.accepted.push(
|
|
9809
|
-
} else if (digest ===
|
|
9810
|
-
else plan.collisions.push(
|
|
9956
|
+
seen.set(event2.id, event2.digest);
|
|
9957
|
+
plan.accepted.push(event2.id);
|
|
9958
|
+
} else if (digest === event2.digest) plan.replayed.push(event2.id);
|
|
9959
|
+
else plan.collisions.push(event2.id);
|
|
9811
9960
|
}
|
|
9812
9961
|
return plan;
|
|
9813
9962
|
}
|