@odla-ai/chapter 0.54.1 → 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 +14 -3
- 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 +688 -544
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -2
- package/dist/worker/index.d.ts +17 -2
- package/dist/worker/index.js +634 -490
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
package/dist/worker/index.js
CHANGED
|
@@ -383,12 +383,12 @@ function canonical(value) {
|
|
|
383
383
|
return value;
|
|
384
384
|
}
|
|
385
385
|
async function stripeEventDigest(rawBody) {
|
|
386
|
-
const
|
|
387
|
-
if (!
|
|
386
|
+
const event2 = JSON.parse(rawBody);
|
|
387
|
+
if (!event2 || typeof event2 !== "object" || Array.isArray(event2)) throw new TypeError("invalid Stripe event");
|
|
388
388
|
const keys3 = ["id", "type", "created", "livemode", "account", "context", "api_version", "data"];
|
|
389
389
|
return stripePayloadDigest(JSON.stringify(canonical({
|
|
390
390
|
digestVersion: 1,
|
|
391
|
-
...Object.fromEntries(keys3.filter((key3) => Object.hasOwn(
|
|
391
|
+
...Object.fromEntries(keys3.filter((key3) => Object.hasOwn(event2, key3)).map((key3) => [key3, event2[key3]]))
|
|
392
392
|
})));
|
|
393
393
|
}
|
|
394
394
|
|
|
@@ -655,13 +655,13 @@ function firstPrice(object2) {
|
|
|
655
655
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
656
656
|
}
|
|
657
657
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
658
|
-
const
|
|
659
|
-
const eventId = providerId(
|
|
660
|
-
const eventType = text(
|
|
661
|
-
const object2 = raw(raw(
|
|
658
|
+
const event2 = raw(input);
|
|
659
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
660
|
+
const eventType = text(event2.type) ?? "";
|
|
661
|
+
const object2 = raw(raw(event2.data).object);
|
|
662
662
|
const objectId = providerId(object2.id) ?? "";
|
|
663
663
|
const candidates = metadataCandidates(object2);
|
|
664
|
-
const
|
|
664
|
+
const identity2 = {};
|
|
665
665
|
const identityKeys2 = {
|
|
666
666
|
appId: "odlaAppId",
|
|
667
667
|
chapterId: "chapterId",
|
|
@@ -671,7 +671,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
671
671
|
};
|
|
672
672
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
673
673
|
const value = metadataValue(candidates, source);
|
|
674
|
-
if (value)
|
|
674
|
+
if (value) identity2[target] = value;
|
|
675
675
|
}
|
|
676
676
|
let objectKind = "ignored";
|
|
677
677
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -693,11 +693,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
693
693
|
eventId,
|
|
694
694
|
digest,
|
|
695
695
|
eventType,
|
|
696
|
-
eventCreated: integer(
|
|
697
|
-
livemode:
|
|
696
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
697
|
+
livemode: event2.livemode === true,
|
|
698
698
|
objectKind,
|
|
699
699
|
objectId,
|
|
700
|
-
identity,
|
|
700
|
+
identity: identity2,
|
|
701
701
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
702
702
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
703
703
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -1174,35 +1174,35 @@ function findGiftRef(obj) {
|
|
|
1174
1174
|
...chapterId ? { chapterId } : {}
|
|
1175
1175
|
};
|
|
1176
1176
|
}
|
|
1177
|
-
function normalizeWebhookEvent(
|
|
1178
|
-
const obj =
|
|
1179
|
-
if (
|
|
1177
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
1178
|
+
const obj = event2.data?.object ?? {};
|
|
1179
|
+
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 };
|
|
1180
1180
|
const gift = findGiftRef(obj);
|
|
1181
1181
|
if (gift.giftIntentId) {
|
|
1182
|
-
const { giftIntentId, providerObjectId, ...
|
|
1183
|
-
if (
|
|
1184
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
1182
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
1183
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
1184
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
1185
1185
|
}
|
|
1186
|
-
if (
|
|
1187
|
-
if (
|
|
1188
|
-
return { kind: "ignored", type:
|
|
1186
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
1187
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
1188
|
+
return { kind: "ignored", type: event2.type };
|
|
1189
1189
|
}
|
|
1190
1190
|
const ref = findApplicationRef(obj);
|
|
1191
1191
|
const seatId = obj.metadata?.seatId;
|
|
1192
1192
|
if (typeof seatId === "string" && seatId) {
|
|
1193
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
1194
|
-
if (
|
|
1195
|
-
if (
|
|
1196
|
-
return { kind: "ignored", type:
|
|
1193
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
1194
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
1195
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
1196
|
+
return { kind: "ignored", type: event2.type };
|
|
1197
1197
|
}
|
|
1198
|
-
if (
|
|
1199
|
-
switch (
|
|
1198
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
1199
|
+
switch (event2.type) {
|
|
1200
1200
|
case "invoice.payment_failed":
|
|
1201
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
1201
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
1202
1202
|
case "invoice.paid":
|
|
1203
1203
|
case "invoice.payment_succeeded": {
|
|
1204
1204
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
1205
|
-
return { kind: "ignored", type:
|
|
1205
|
+
return { kind: "ignored", type: event2.type };
|
|
1206
1206
|
}
|
|
1207
1207
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
1208
1208
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -1216,11 +1216,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
1216
1216
|
case "charge.refunded":
|
|
1217
1217
|
return { kind: "refunded", ...ref };
|
|
1218
1218
|
case "customer.subscription.updated":
|
|
1219
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
1219
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
1220
1220
|
case "customer.subscription.deleted":
|
|
1221
1221
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
1222
1222
|
default:
|
|
1223
|
-
return { kind: "ignored", type:
|
|
1223
|
+
return { kind: "ignored", type: event2.type };
|
|
1224
1224
|
}
|
|
1225
1225
|
}
|
|
1226
1226
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -1584,6 +1584,41 @@ async function backfillCrm(deps) {
|
|
|
1584
1584
|
return { synced, errors };
|
|
1585
1585
|
}
|
|
1586
1586
|
|
|
1587
|
+
// src/email-retirement.ts
|
|
1588
|
+
function isRetiredEmailCopy(subject, text8) {
|
|
1589
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1590
|
+
const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1591
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/email.ts
|
|
1595
|
+
import { validateEmailCopy } from "@odla-ai/crm";
|
|
1596
|
+
|
|
1597
|
+
// src/email-events.ts
|
|
1598
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
1599
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
1600
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
1601
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
1602
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
1603
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
1604
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
1605
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
1606
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
1607
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
1608
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
1609
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
1610
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
1611
|
+
};
|
|
1612
|
+
function chapterEmailEvents(chapter) {
|
|
1613
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
1614
|
+
label: key3,
|
|
1615
|
+
trigger: "Administrator sends a message",
|
|
1616
|
+
audience: "Selected contact",
|
|
1617
|
+
variables: spec.vars
|
|
1618
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
1619
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1587
1622
|
// src/email.ts
|
|
1588
1623
|
function render(template, vars) {
|
|
1589
1624
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -1607,21 +1642,233 @@ function isAlreadySent(priorRows) {
|
|
|
1607
1642
|
function planDelivery(input) {
|
|
1608
1643
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
1609
1644
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
1610
|
-
if (tpl.enabled
|
|
1645
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
1646
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
1647
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
1611
1648
|
const vars = groupVars(input.group, input.vars);
|
|
1649
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
1650
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1651
|
+
const issues = validateEmailCopy(tpl, {
|
|
1652
|
+
allowedVariables: event2?.variables,
|
|
1653
|
+
optionalVariables: event2?.optionalVariables,
|
|
1654
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
1655
|
+
values: vars,
|
|
1656
|
+
tierId: vars.tierId
|
|
1657
|
+
});
|
|
1658
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
1612
1659
|
const isProd = input.envName === "prod";
|
|
1613
1660
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
1614
1661
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1615
1662
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1616
|
-
const
|
|
1663
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
1664
|
+
const renderedText = render(tpl.text, vars);
|
|
1665
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1666
|
+
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1667
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1617
1668
|
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1618
1669
|
|
|
1619
|
-
` +
|
|
1670
|
+
` + renderedText : renderedText;
|
|
1620
1671
|
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
1621
1672
|
}
|
|
1622
1673
|
|
|
1623
1674
|
// src/notify.ts
|
|
1624
|
-
|
|
1675
|
+
import { validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
1676
|
+
|
|
1677
|
+
// src/email-event-values.ts
|
|
1678
|
+
import { isEmailTierName } from "@odla-ai/crm";
|
|
1679
|
+
|
|
1680
|
+
// src/tiers.ts
|
|
1681
|
+
function tierIsFree(tier) {
|
|
1682
|
+
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
1683
|
+
}
|
|
1684
|
+
function tierPayable(tier, group, hasSecretKey) {
|
|
1685
|
+
if (tierIsFree(tier)) return true;
|
|
1686
|
+
return Boolean(group.stripePublishableKey && tier.stripePriceId && hasSecretKey);
|
|
1687
|
+
}
|
|
1688
|
+
function byOrder(a, b) {
|
|
1689
|
+
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1690
|
+
}
|
|
1691
|
+
function resolveTiers(group, rows5 = []) {
|
|
1692
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
1693
|
+
if (rows5.length) return [...stored].sort(byOrder);
|
|
1694
|
+
return [{
|
|
1695
|
+
id: "standard",
|
|
1696
|
+
groupId: String(group.id ?? ""),
|
|
1697
|
+
name: "Membership",
|
|
1698
|
+
priceCents: group.standardPriceCents ?? 0,
|
|
1699
|
+
currency: group.currency ?? "usd",
|
|
1700
|
+
interval: group.interval ?? "year",
|
|
1701
|
+
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
1702
|
+
sortOrder: 0,
|
|
1703
|
+
active: true
|
|
1704
|
+
}];
|
|
1705
|
+
}
|
|
1706
|
+
function offerableTiers(tiers, group, hasSecretKey) {
|
|
1707
|
+
return tiers.filter((tier) => tier.active && tierPayable(tier, group, hasSecretKey));
|
|
1708
|
+
}
|
|
1709
|
+
function findTier(tiers, tierId) {
|
|
1710
|
+
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1711
|
+
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1712
|
+
}
|
|
1713
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1714
|
+
return tiers.map((tier) => ({
|
|
1715
|
+
id: tier.id,
|
|
1716
|
+
name: tier.name,
|
|
1717
|
+
priceCents: tier.priceCents,
|
|
1718
|
+
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
1719
|
+
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1720
|
+
blurb: tier.blurb ?? "",
|
|
1721
|
+
free: tierIsFree(tier),
|
|
1722
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/tiers-store.ts
|
|
1727
|
+
function rowToTier(row2, groupId) {
|
|
1728
|
+
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") {
|
|
1729
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
1730
|
+
}
|
|
1731
|
+
return {
|
|
1732
|
+
id: String(row2.id ?? ""),
|
|
1733
|
+
groupId: String(row2.groupId ?? groupId),
|
|
1734
|
+
name: String(row2.name ?? ""),
|
|
1735
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
1736
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
1737
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
1738
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
1739
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
1740
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
1741
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
1742
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
1743
|
+
active: row2.active !== false
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
async function loadTiers(db, group) {
|
|
1747
|
+
const groupId = String(group.id ?? "");
|
|
1748
|
+
let rows5;
|
|
1749
|
+
try {
|
|
1750
|
+
rows5 = (await db.query({
|
|
1751
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
1752
|
+
})).tiers;
|
|
1753
|
+
} catch {
|
|
1754
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
1755
|
+
}
|
|
1756
|
+
if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
1757
|
+
const list = rows5.map((row2) => rowToTier(row2, groupId));
|
|
1758
|
+
return resolveTiers(group, list);
|
|
1759
|
+
}
|
|
1760
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
1761
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// src/notification-variables.ts
|
|
1765
|
+
function lifecycleNotifyDeps(db, env) {
|
|
1766
|
+
return {
|
|
1767
|
+
db,
|
|
1768
|
+
envName: env.ODLA_ENV,
|
|
1769
|
+
sender: env.SEND_EMAIL,
|
|
1770
|
+
from: env.EMAIL_FROM,
|
|
1771
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
1772
|
+
now: () => Date.now(),
|
|
1773
|
+
newId: () => crypto.randomUUID()
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
async function frozenCheckoutVariables(db, app) {
|
|
1777
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1778
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1779
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1780
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1781
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
1782
|
+
let formatted = "";
|
|
1783
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1784
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1785
|
+
}
|
|
1786
|
+
return {
|
|
1787
|
+
paymentAmount: formatted,
|
|
1788
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1789
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
async function catalogTierName(db, group, app) {
|
|
1793
|
+
try {
|
|
1794
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
1795
|
+
} catch {
|
|
1796
|
+
return "";
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async function checkoutVariables(db, group, app) {
|
|
1800
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
1801
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
1802
|
+
}
|
|
1803
|
+
function formatInstant(at2, timeZone) {
|
|
1804
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
1805
|
+
weekday: "long",
|
|
1806
|
+
month: "long",
|
|
1807
|
+
day: "numeric",
|
|
1808
|
+
year: "numeric",
|
|
1809
|
+
hour: "numeric",
|
|
1810
|
+
minute: "2-digit",
|
|
1811
|
+
timeZoneName: "short",
|
|
1812
|
+
timeZone
|
|
1813
|
+
}).format(new Date(at2));
|
|
1814
|
+
}
|
|
1815
|
+
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1816
|
+
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1817
|
+
function applicantVariables(app) {
|
|
1818
|
+
const text8 = (v) => typeof v === "string" ? v : "";
|
|
1819
|
+
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
1820
|
+
}
|
|
1821
|
+
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1822
|
+
const startAt = formatInstant(now + 864e5, timeZone);
|
|
1823
|
+
return {
|
|
1824
|
+
firstName: "Sample",
|
|
1825
|
+
lastName: "Person",
|
|
1826
|
+
email: "sample@example.com",
|
|
1827
|
+
phone: "(555) 010-0100",
|
|
1828
|
+
state: "CA",
|
|
1829
|
+
tier: "Membership",
|
|
1830
|
+
tierName: "Membership",
|
|
1831
|
+
paymentAmount: "$100.00",
|
|
1832
|
+
paymentAmountSummary: " of $100.00",
|
|
1833
|
+
paymentNextStep: "Next, schedule your introduction call.",
|
|
1834
|
+
submitNextStep: "Next, schedule your introduction call.",
|
|
1835
|
+
startAt,
|
|
1836
|
+
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
1837
|
+
timezone: timeZone,
|
|
1838
|
+
meetUrl: "https://meet.google.com/sample",
|
|
1839
|
+
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
1840
|
+
meetingTime: startAt,
|
|
1841
|
+
meetingLink: "https://meet.google.com/sample",
|
|
1842
|
+
adminUrl: adminUrlFor(origin),
|
|
1843
|
+
membersUrl: membersUrlFor(origin),
|
|
1844
|
+
purchaserName: "Sample Purchaser",
|
|
1845
|
+
recipientName: "Sample Recipient",
|
|
1846
|
+
chapterName: "Sample Chapter",
|
|
1847
|
+
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
1848
|
+
joinUrl: `${origin}/join?seat=sample`
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/email-event-values.ts
|
|
1853
|
+
async function emailTierVariables(db, input) {
|
|
1854
|
+
try {
|
|
1855
|
+
const [apps, groups] = await Promise.all([
|
|
1856
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
1857
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
1858
|
+
]);
|
|
1859
|
+
const app = apps.applications?.[0], group = groups.groups?.[0];
|
|
1860
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
1861
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
1862
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
1863
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
1864
|
+
const tierName2 = isEmailTierName(name, tierId) ? name.trim() : "";
|
|
1865
|
+
return { tierName: tierName2, tier: tierName2, tierId };
|
|
1866
|
+
} catch {
|
|
1867
|
+
return { tierName: "", tier: "" };
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/notify.ts
|
|
1625
1872
|
function fromAddress(configured, name) {
|
|
1626
1873
|
const display = name?.trim();
|
|
1627
1874
|
if (!display) return configured;
|
|
@@ -1638,14 +1885,17 @@ async function sendTemplated(deps, input) {
|
|
|
1638
1885
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
1639
1886
|
const override = deps.debugEmail?.trim();
|
|
1640
1887
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
1888
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1889
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
1641
1890
|
const decision = planDelivery({
|
|
1642
1891
|
envName: deps.envName,
|
|
1643
1892
|
group,
|
|
1644
1893
|
template: input.template,
|
|
1645
1894
|
to: input.to,
|
|
1646
|
-
vars
|
|
1895
|
+
vars,
|
|
1647
1896
|
cloudflareReady,
|
|
1648
|
-
force: input.force
|
|
1897
|
+
force: input.force,
|
|
1898
|
+
event: event2
|
|
1649
1899
|
});
|
|
1650
1900
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
1651
1901
|
const previous = prior[0];
|
|
@@ -1670,6 +1920,8 @@ async function sendTemplated(deps, input) {
|
|
|
1670
1920
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
1671
1921
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
1672
1922
|
};
|
|
1923
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1924
|
+
if (validateEmailCopy2({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1673
1925
|
try {
|
|
1674
1926
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
|
|
1675
1927
|
mutationId: `email-claim:${id6}:${attempt}`,
|
|
@@ -1716,17 +1968,17 @@ async function sendTemplated(deps, input) {
|
|
|
1716
1968
|
return { sent: true };
|
|
1717
1969
|
}
|
|
1718
1970
|
function emailGroupFrom(row2) {
|
|
1719
|
-
const
|
|
1971
|
+
const str4 = (v) => typeof v === "string" ? v : void 0;
|
|
1720
1972
|
const templates = row2.emailTemplates && typeof row2.emailTemplates === "object" ? row2.emailTemplates : {};
|
|
1721
1973
|
return {
|
|
1722
1974
|
id: String(row2.id),
|
|
1723
1975
|
name: String(row2.name ?? ""),
|
|
1724
|
-
replyTo:
|
|
1725
|
-
debugEmail:
|
|
1726
|
-
fromName:
|
|
1727
|
-
refundPolicyText:
|
|
1728
|
-
commitmentText:
|
|
1729
|
-
normsText:
|
|
1976
|
+
replyTo: str4(row2.replyTo) ?? "",
|
|
1977
|
+
debugEmail: str4(row2.debugEmail),
|
|
1978
|
+
fromName: str4(row2.fromName),
|
|
1979
|
+
refundPolicyText: str4(row2.refundPolicyText),
|
|
1980
|
+
commitmentText: str4(row2.commitmentText),
|
|
1981
|
+
normsText: str4(row2.normsText),
|
|
1730
1982
|
emailTemplates: templates
|
|
1731
1983
|
};
|
|
1732
1984
|
}
|
|
@@ -1839,15 +2091,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
1839
2091
|
// src/membership-offer.ts
|
|
1840
2092
|
function createMembershipCommercialOffer(input) {
|
|
1841
2093
|
const offering = membershipOfferingVersion(input.offering);
|
|
1842
|
-
const
|
|
2094
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
1843
2095
|
const now = input.now;
|
|
1844
2096
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
1845
2097
|
throw new TypeError("invalid offer clock or founding count");
|
|
1846
2098
|
}
|
|
1847
2099
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
1848
|
-
if (
|
|
1849
|
-
const founding = Boolean(
|
|
1850
|
-
const earned = founding ?
|
|
2100
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
2101
|
+
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);
|
|
2102
|
+
const earned = founding ? policy2 : null;
|
|
1851
2103
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
1852
2104
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
1853
2105
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -1939,15 +2191,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1939
2191
|
}
|
|
1940
2192
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
1941
2193
|
const state = await authoritativeState(db, input);
|
|
1942
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
1943
|
-
const approvedFoundingCount =
|
|
2194
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
2195
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
1944
2196
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1945
2197
|
const contract = createMembershipCommercialOffer({
|
|
1946
2198
|
offering,
|
|
1947
|
-
discountPolicy:
|
|
2199
|
+
discountPolicy: policy2,
|
|
1948
2200
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1949
2201
|
now: input.now,
|
|
1950
|
-
couponId: await membershipDiscountCouponId(input,
|
|
2202
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
1951
2203
|
});
|
|
1952
2204
|
const discount = contract.billingTerms.discount;
|
|
1953
2205
|
return {
|
|
@@ -1976,12 +2228,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
1976
2228
|
});
|
|
1977
2229
|
if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
1978
2230
|
const offerings = rows5.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));
|
|
1979
|
-
const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
2231
|
+
const policies = rows5.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);
|
|
1980
2232
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
1981
2233
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
1982
|
-
const
|
|
1983
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
1984
|
-
return { offering, policy };
|
|
2234
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
2235
|
+
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");
|
|
2236
|
+
return { offering, policy: policy2 };
|
|
1985
2237
|
}
|
|
1986
2238
|
async function issueMembershipCommercialOffer(db, input) {
|
|
1987
2239
|
validate(input);
|
|
@@ -1989,15 +2241,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
1989
2241
|
const state = await authoritativeState(db, input);
|
|
1990
2242
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
1991
2243
|
if (existing) return restore(existing, input);
|
|
1992
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
1993
|
-
const approvedFoundingCount =
|
|
2244
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
2245
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
1994
2246
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1995
2247
|
const contract = createMembershipCommercialOffer({
|
|
1996
2248
|
offering,
|
|
1997
|
-
discountPolicy:
|
|
2249
|
+
discountPolicy: policy2,
|
|
1998
2250
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1999
2251
|
now: input.now,
|
|
2000
|
-
couponId: await membershipDiscountCouponId(input,
|
|
2252
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
2001
2253
|
});
|
|
2002
2254
|
const value = {
|
|
2003
2255
|
id: input.offerId,
|
|
@@ -2264,90 +2516,6 @@ async function verifyMembershipPaymentReceipt(value, target) {
|
|
|
2264
2516
|
};
|
|
2265
2517
|
}
|
|
2266
2518
|
|
|
2267
|
-
// src/tiers.ts
|
|
2268
|
-
function tierIsFree(tier) {
|
|
2269
|
-
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
2270
|
-
}
|
|
2271
|
-
function tierPayable(tier, group, hasSecretKey) {
|
|
2272
|
-
if (tierIsFree(tier)) return true;
|
|
2273
|
-
return Boolean(group.stripePublishableKey && tier.stripePriceId && hasSecretKey);
|
|
2274
|
-
}
|
|
2275
|
-
function byOrder(a, b) {
|
|
2276
|
-
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
2277
|
-
}
|
|
2278
|
-
function resolveTiers(group, rows5 = []) {
|
|
2279
|
-
const stored = rows5.filter((tier) => tier.active);
|
|
2280
|
-
if (rows5.length) return [...stored].sort(byOrder);
|
|
2281
|
-
return [{
|
|
2282
|
-
id: "standard",
|
|
2283
|
-
groupId: String(group.id ?? ""),
|
|
2284
|
-
name: "Membership",
|
|
2285
|
-
priceCents: group.standardPriceCents ?? 0,
|
|
2286
|
-
currency: group.currency ?? "usd",
|
|
2287
|
-
interval: group.interval ?? "year",
|
|
2288
|
-
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
2289
|
-
sortOrder: 0,
|
|
2290
|
-
active: true
|
|
2291
|
-
}];
|
|
2292
|
-
}
|
|
2293
|
-
function offerableTiers(tiers, group, hasSecretKey) {
|
|
2294
|
-
return tiers.filter((tier) => tier.active && tierPayable(tier, group, hasSecretKey));
|
|
2295
|
-
}
|
|
2296
|
-
function findTier(tiers, tierId) {
|
|
2297
|
-
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
2298
|
-
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
2299
|
-
}
|
|
2300
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2301
|
-
return tiers.map((tier) => ({
|
|
2302
|
-
id: tier.id,
|
|
2303
|
-
name: tier.name,
|
|
2304
|
-
priceCents: tier.priceCents,
|
|
2305
|
-
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2306
|
-
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2307
|
-
blurb: tier.blurb ?? "",
|
|
2308
|
-
free: tierIsFree(tier),
|
|
2309
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2310
|
-
}));
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
// src/tiers-store.ts
|
|
2314
|
-
function rowToTier(row2, groupId) {
|
|
2315
|
-
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") {
|
|
2316
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
2317
|
-
}
|
|
2318
|
-
return {
|
|
2319
|
-
id: String(row2.id ?? ""),
|
|
2320
|
-
groupId: String(row2.groupId ?? groupId),
|
|
2321
|
-
name: String(row2.name ?? ""),
|
|
2322
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
2323
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
2324
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
2325
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
2326
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
2327
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
2328
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
2329
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
2330
|
-
active: row2.active !== false
|
|
2331
|
-
};
|
|
2332
|
-
}
|
|
2333
|
-
async function loadTiers(db, group) {
|
|
2334
|
-
const groupId = String(group.id ?? "");
|
|
2335
|
-
let rows5;
|
|
2336
|
-
try {
|
|
2337
|
-
rows5 = (await db.query({
|
|
2338
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2339
|
-
})).tiers;
|
|
2340
|
-
} catch {
|
|
2341
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
2342
|
-
}
|
|
2343
|
-
if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
2344
|
-
const list = rows5.map((row2) => rowToTier(row2, groupId));
|
|
2345
|
-
return resolveTiers(group, list);
|
|
2346
|
-
}
|
|
2347
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
2348
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
2519
|
// src/membership-restart-seat.ts
|
|
2352
2520
|
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
2353
2521
|
const ops = [], guards = [];
|
|
@@ -2854,89 +3022,6 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
2854
3022
|
}
|
|
2855
3023
|
}
|
|
2856
3024
|
|
|
2857
|
-
// src/notification-variables.ts
|
|
2858
|
-
function lifecycleNotifyDeps(db, env) {
|
|
2859
|
-
return {
|
|
2860
|
-
db,
|
|
2861
|
-
envName: env.ODLA_ENV,
|
|
2862
|
-
sender: env.SEND_EMAIL,
|
|
2863
|
-
from: env.EMAIL_FROM,
|
|
2864
|
-
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
2865
|
-
now: () => Date.now(),
|
|
2866
|
-
newId: () => crypto.randomUUID()
|
|
2867
|
-
};
|
|
2868
|
-
}
|
|
2869
|
-
async function frozenCheckoutVariables(db, app) {
|
|
2870
|
-
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
2871
|
-
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
2872
|
-
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
2873
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
2874
|
-
const currency = offering?.currency ?? intent?.currency;
|
|
2875
|
-
let formatted = "";
|
|
2876
|
-
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
2877
|
-
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
2878
|
-
}
|
|
2879
|
-
return {
|
|
2880
|
-
paymentAmount: formatted,
|
|
2881
|
-
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
2882
|
-
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
2883
|
-
};
|
|
2884
|
-
}
|
|
2885
|
-
async function catalogTierName(db, group, app) {
|
|
2886
|
-
try {
|
|
2887
|
-
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
2888
|
-
} catch {
|
|
2889
|
-
return "";
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
async function checkoutVariables(db, group, app) {
|
|
2893
|
-
const frozen = await frozenCheckoutVariables(db, app);
|
|
2894
|
-
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2895
|
-
}
|
|
2896
|
-
function formatInstant(at2, timeZone) {
|
|
2897
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
2898
|
-
weekday: "long",
|
|
2899
|
-
month: "long",
|
|
2900
|
-
day: "numeric",
|
|
2901
|
-
year: "numeric",
|
|
2902
|
-
hour: "numeric",
|
|
2903
|
-
minute: "2-digit",
|
|
2904
|
-
timeZoneName: "short",
|
|
2905
|
-
timeZone
|
|
2906
|
-
}).format(new Date(at2));
|
|
2907
|
-
}
|
|
2908
|
-
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2909
|
-
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
2910
|
-
function applicantVariables(app) {
|
|
2911
|
-
const text8 = (v) => typeof v === "string" ? v : "";
|
|
2912
|
-
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
2913
|
-
}
|
|
2914
|
-
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
2915
|
-
const startAt = formatInstant(now + 864e5, timeZone);
|
|
2916
|
-
return {
|
|
2917
|
-
firstName: "Sample",
|
|
2918
|
-
lastName: "Person",
|
|
2919
|
-
email: "sample@example.com",
|
|
2920
|
-
phone: "(555) 010-0100",
|
|
2921
|
-
state: "CA",
|
|
2922
|
-
tier: "Membership",
|
|
2923
|
-
tierName: "Membership",
|
|
2924
|
-
paymentAmount: "$100.00",
|
|
2925
|
-
paymentAmountSummary: " of $100.00",
|
|
2926
|
-
paymentNextStep: "Next, schedule your introduction call.",
|
|
2927
|
-
submitNextStep: "Next, schedule your introduction call.",
|
|
2928
|
-
startAt,
|
|
2929
|
-
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
2930
|
-
timezone: timeZone,
|
|
2931
|
-
meetUrl: "https://meet.google.com/sample",
|
|
2932
|
-
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
2933
|
-
meetingTime: startAt,
|
|
2934
|
-
meetingLink: "https://meet.google.com/sample",
|
|
2935
|
-
adminUrl: adminUrlFor(origin),
|
|
2936
|
-
membersUrl: membersUrlFor(origin)
|
|
2937
|
-
};
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
3025
|
// src/payment-notifications.ts
|
|
2941
3026
|
var groupFor = async (db, app) => {
|
|
2942
3027
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3222,24 +3307,24 @@ async function revokeGiftClaim(db, claim, reason, now) {
|
|
|
3222
3307
|
}
|
|
3223
3308
|
|
|
3224
3309
|
// src/payments-route-support.ts
|
|
3225
|
-
async function findPaymentApplication(db,
|
|
3226
|
-
if ("applicationId" in
|
|
3227
|
-
const rows5 = await db.query({ applications: { $: { where: { id:
|
|
3310
|
+
async function findPaymentApplication(db, event2) {
|
|
3311
|
+
if ("applicationId" in event2 && event2.applicationId) {
|
|
3312
|
+
const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
|
|
3228
3313
|
return rows5.applications?.[0];
|
|
3229
3314
|
}
|
|
3230
|
-
if (
|
|
3231
|
-
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId:
|
|
3315
|
+
if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
|
|
3316
|
+
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
|
|
3232
3317
|
return Array.isArray(applications) && applications.length === 1 ? applications[0] : void 0;
|
|
3233
3318
|
}
|
|
3234
3319
|
return void 0;
|
|
3235
3320
|
}
|
|
3236
|
-
function paymentEventPatch(
|
|
3237
|
-
if ((status2 === "refunded" || status2 === "declined") && (
|
|
3238
|
-
switch (
|
|
3321
|
+
function paymentEventPatch(event2, status2) {
|
|
3322
|
+
if ((status2 === "refunded" || status2 === "declined") && (event2.kind === "first_payment" || event2.kind === "renewal")) return {};
|
|
3323
|
+
switch (event2.kind) {
|
|
3239
3324
|
case "first_payment":
|
|
3240
|
-
return firstPaymentPatch(status2,
|
|
3325
|
+
return firstPaymentPatch(status2, event2.renewalAt);
|
|
3241
3326
|
case "renewal":
|
|
3242
|
-
return
|
|
3327
|
+
return event2.renewalAt !== void 0 ? renewalPatch(event2.renewalAt) : {};
|
|
3243
3328
|
case "refunded":
|
|
3244
3329
|
return refundedPatch();
|
|
3245
3330
|
case "canceled":
|
|
@@ -3260,10 +3345,10 @@ function paymentLineItems(group, tier) {
|
|
|
3260
3345
|
}
|
|
3261
3346
|
|
|
3262
3347
|
// src/member-system-contract.ts
|
|
3263
|
-
function classifyMemberStripeEvent(
|
|
3348
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3264
3349
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
3265
|
-
const supplied = keys3.filter((key3) =>
|
|
3266
|
-
if (supplied.some((key3) =>
|
|
3350
|
+
const supplied = keys3.filter((key3) => identity2[key3] !== void 0);
|
|
3351
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
3267
3352
|
if (supplied.length === keys3.length) return "apply";
|
|
3268
3353
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
3269
3354
|
}
|
|
@@ -3273,12 +3358,12 @@ var firstRow = async (db, ns, q) => {
|
|
|
3273
3358
|
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
3274
3359
|
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3275
3360
|
};
|
|
3276
|
-
var stripeEventIdentity = (
|
|
3277
|
-
...
|
|
3278
|
-
...
|
|
3279
|
-
...
|
|
3280
|
-
...
|
|
3281
|
-
...
|
|
3361
|
+
var stripeEventIdentity = (event2) => ({
|
|
3362
|
+
...event2.appId ? { appId: event2.appId } : {},
|
|
3363
|
+
...event2.environment ? { environment: event2.environment } : {},
|
|
3364
|
+
...event2.dataEnvironment ? { dataEnvironment: event2.dataEnvironment } : {},
|
|
3365
|
+
...event2.runtime ? { runtime: event2.runtime } : {},
|
|
3366
|
+
...event2.chapterId ? { chapterId: event2.chapterId } : {}
|
|
3282
3367
|
});
|
|
3283
3368
|
var stripeEventTarget = (env, ctx) => ({
|
|
3284
3369
|
appId: env.ODLA_APP_ID,
|
|
@@ -3288,12 +3373,12 @@ var stripeEventTarget = (env, ctx) => ({
|
|
|
3288
3373
|
chapterId: ctx.chapter.id
|
|
3289
3374
|
});
|
|
3290
3375
|
var receiptFor = (db, eventId) => firstRow(db, "stripeEventReceipts", { where: { eventId }, limit: 1 });
|
|
3291
|
-
function reportStripePartition(level, eventId,
|
|
3376
|
+
function reportStripePartition(level, eventId, event2, disposition) {
|
|
3292
3377
|
console[level]("chapter stripe partition", JSON.stringify({
|
|
3293
3378
|
eventId,
|
|
3294
|
-
kind:
|
|
3379
|
+
kind: event2.kind,
|
|
3295
3380
|
disposition,
|
|
3296
|
-
...stripeEventIdentity(
|
|
3381
|
+
...stripeEventIdentity(event2)
|
|
3297
3382
|
}));
|
|
3298
3383
|
}
|
|
3299
3384
|
var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
|
|
@@ -3312,8 +3397,8 @@ async function alertStripeQuarantine(db, ctx, eventId, disposition) {
|
|
|
3312
3397
|
artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
|
|
3313
3398
|
});
|
|
3314
3399
|
}
|
|
3315
|
-
async function reportStripeCollision(db, ctx, eventId,
|
|
3316
|
-
reportStripePartition("error", eventId,
|
|
3400
|
+
async function reportStripeCollision(db, ctx, eventId, event2) {
|
|
3401
|
+
reportStripePartition("error", eventId, event2, "quarantine_collision");
|
|
3317
3402
|
await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
|
|
3318
3403
|
}
|
|
3319
3404
|
async function storeStripeReceipt(db, input) {
|
|
@@ -3348,18 +3433,18 @@ async function storeStripeReceipt(db, input) {
|
|
|
3348
3433
|
const raced = await receiptFor(db, input.eventId);
|
|
3349
3434
|
return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3350
3435
|
}
|
|
3351
|
-
async function quarantineStripeEvent(db, eventId, digest,
|
|
3436
|
+
async function quarantineStripeEvent(db, eventId, digest, event2, ctx) {
|
|
3352
3437
|
const disposition = "quarantine_unmatched";
|
|
3353
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
3438
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event: event2, disposition, matched: false });
|
|
3354
3439
|
if (stored === "collision") {
|
|
3355
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3440
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3356
3441
|
return json({ ok: true, matched: false, collision: true });
|
|
3357
3442
|
}
|
|
3358
|
-
reportStripePartition("warn", eventId,
|
|
3443
|
+
reportStripePartition("warn", eventId, event2, disposition);
|
|
3359
3444
|
if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
|
|
3360
3445
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
3361
3446
|
}
|
|
3362
|
-
async function replayedStripeReceipt(db, eventId, digest,
|
|
3447
|
+
async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx) {
|
|
3363
3448
|
const existing = await receiptFor(db, eventId);
|
|
3364
3449
|
if (!existing) return null;
|
|
3365
3450
|
if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
|
|
@@ -3369,7 +3454,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, c
|
|
|
3369
3454
|
collisionDigest: digest,
|
|
3370
3455
|
retryReady: false
|
|
3371
3456
|
} }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
|
|
3372
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3457
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3373
3458
|
return json({ ok: true, matched: false, collision: true });
|
|
3374
3459
|
}
|
|
3375
3460
|
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
@@ -3383,42 +3468,42 @@ async function firstRow2(db, ns, q) {
|
|
|
3383
3468
|
}
|
|
3384
3469
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3385
3470
|
const db = ctx.makeDb(env);
|
|
3386
|
-
const { rawBody, eventId, event } = input;
|
|
3387
|
-
if (
|
|
3471
|
+
const { rawBody, eventId, event: event2 } = input;
|
|
3472
|
+
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3388
3473
|
const digest = await stripeEventDigest(rawBody);
|
|
3389
3474
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
3390
|
-
const
|
|
3475
|
+
const identity2 = stripeEventIdentity(event2);
|
|
3391
3476
|
const target = stripeEventTarget(env, ctx);
|
|
3392
|
-
const preliminary = classifyMemberStripeEvent(
|
|
3477
|
+
const preliminary = classifyMemberStripeEvent(identity2, target, false);
|
|
3393
3478
|
if (preliminary === "ignore_sibling") {
|
|
3394
|
-
reportStripePartition("warn", eventId,
|
|
3479
|
+
reportStripePartition("warn", eventId, event2, preliminary);
|
|
3395
3480
|
return json({ ok: true, matched: false, disposition: preliminary });
|
|
3396
3481
|
}
|
|
3397
|
-
if (
|
|
3398
|
-
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id:
|
|
3482
|
+
if (event2.kind === "gift_paid" || event2.kind === "gift_revoked") {
|
|
3483
|
+
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event2.giftIntentId }, limit: 1 });
|
|
3399
3484
|
if (intent && typeof intent.runtime === "string" && intent.runtime !== target.runtime) {
|
|
3400
|
-
reportStripePartition("warn", eventId,
|
|
3485
|
+
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
3401
3486
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3402
3487
|
}
|
|
3403
|
-
const replay2 = await replayedStripeReceipt(db, eventId, digest,
|
|
3488
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx);
|
|
3404
3489
|
if (replay2) return replay2;
|
|
3405
3490
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
3406
|
-
const disposition2 = classifyMemberStripeEvent(
|
|
3491
|
+
const disposition2 = classifyMemberStripeEvent(identity2, target, localIntent);
|
|
3407
3492
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
3408
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3493
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3409
3494
|
}
|
|
3410
|
-
if (
|
|
3411
|
-
if (intent.providerObjectId !==
|
|
3412
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3495
|
+
if (event2.kind === "gift_paid") {
|
|
3496
|
+
if (intent.providerObjectId !== event2.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
3497
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3413
3498
|
}
|
|
3414
3499
|
try {
|
|
3415
3500
|
const issued = await issueGiftClaim(db, intent, {
|
|
3416
3501
|
eventId,
|
|
3417
|
-
objectId:
|
|
3502
|
+
objectId: event2.providerObjectId
|
|
3418
3503
|
}, Date.now());
|
|
3419
|
-
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
|
|
3504
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition: disposition2, matched: true });
|
|
3420
3505
|
if (stored3 === "collision") {
|
|
3421
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3506
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3422
3507
|
return json({ ok: true, matched: false, collision: true });
|
|
3423
3508
|
}
|
|
3424
3509
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
@@ -3426,57 +3511,57 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3426
3511
|
return json({ error: "gift issuance failed" }, 503);
|
|
3427
3512
|
}
|
|
3428
3513
|
}
|
|
3429
|
-
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id:
|
|
3430
|
-
status:
|
|
3514
|
+
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: event2.giftIntentId, attrs: {
|
|
3515
|
+
status: event2.reason,
|
|
3431
3516
|
updatedAt: Date.now()
|
|
3432
3517
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
3433
|
-
const claim = await firstRow2(db, "giftClaims", { where: { intentId:
|
|
3434
|
-
if (claim) await revokeGiftClaim(db, claim,
|
|
3435
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
3518
|
+
const claim = await firstRow2(db, "giftClaims", { where: { intentId: event2.giftIntentId }, limit: 1 });
|
|
3519
|
+
if (claim) await revokeGiftClaim(db, claim, event2.reason, Date.now());
|
|
3520
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition: disposition2, matched: Boolean(claim) });
|
|
3436
3521
|
if (stored2 === "collision") {
|
|
3437
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3522
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3438
3523
|
return json({ ok: true, matched: false, collision: true });
|
|
3439
3524
|
}
|
|
3440
3525
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3441
3526
|
}
|
|
3442
|
-
const app = await findPaymentApplication(db,
|
|
3527
|
+
const app = await findPaymentApplication(db, event2);
|
|
3443
3528
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3444
|
-
reportStripePartition("warn", eventId,
|
|
3529
|
+
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
3445
3530
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3446
3531
|
}
|
|
3447
|
-
const replay = await replayedStripeReceipt(db, eventId, digest,
|
|
3532
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx);
|
|
3448
3533
|
if (replay) return replay;
|
|
3449
3534
|
const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
3450
|
-
const disposition = classifyMemberStripeEvent(
|
|
3535
|
+
const disposition = classifyMemberStripeEvent(identity2, target, localApplication);
|
|
3451
3536
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
3452
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3537
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3453
3538
|
}
|
|
3454
3539
|
if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
|
|
3455
|
-
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3540
|
+
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3456
3541
|
return json({ ok: true, historical: true });
|
|
3457
3542
|
}
|
|
3458
|
-
if (
|
|
3543
|
+
if (event2.kind === "seat_paid" || event2.kind === "seat_refunded") {
|
|
3459
3544
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3460
3545
|
if (!seats) return json({ error: "named seat reconciliation is not configured" }, 503);
|
|
3461
3546
|
try {
|
|
3462
3547
|
const intent = await firstRow2(db, "subscriptionCheckoutIntents", { where: { id: String(app.id) }, limit: 1 });
|
|
3463
3548
|
if (!intent) return json({ error: "membership checkout reconciliation pending" }, 503);
|
|
3464
3549
|
const target2 = await membershipTargetForIntent(intent, app, env, ctx);
|
|
3465
|
-
const receipt = await seats[
|
|
3466
|
-
if (receipt.seatId !==
|
|
3467
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3550
|
+
const receipt = await seats[event2.kind === "seat_paid" ? "confirmPayment" : "confirmRefund"]({ ...target2, seatId: event2.seatId, eventId }, env);
|
|
3551
|
+
if (receipt.seatId !== event2.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
|
|
3552
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3468
3553
|
if (stored2 === "collision") {
|
|
3469
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3554
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3470
3555
|
return json({ ok: true, matched: false, collision: true });
|
|
3471
3556
|
}
|
|
3472
|
-
return json({ ok: true, seatId:
|
|
3557
|
+
return json({ ok: true, seatId: event2.seatId });
|
|
3473
3558
|
} catch {
|
|
3474
3559
|
return json({ error: "named seat reconciliation pending" }, 503);
|
|
3475
3560
|
}
|
|
3476
3561
|
}
|
|
3477
|
-
let authoritativeEvent =
|
|
3562
|
+
let authoritativeEvent = event2;
|
|
3478
3563
|
let membershipProjection = {};
|
|
3479
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(
|
|
3564
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
3480
3565
|
try {
|
|
3481
3566
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3482
3567
|
membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
|
|
@@ -3485,13 +3570,13 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3485
3570
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3486
3571
|
}
|
|
3487
3572
|
}
|
|
3488
|
-
if (
|
|
3573
|
+
if (event2.kind === "first_payment") {
|
|
3489
3574
|
const intent = await firstRow2(db, "subscriptionCheckoutIntents", { where: { id: String(app.id) }, limit: 1 });
|
|
3490
3575
|
if (intent) {
|
|
3491
|
-
const evidence = verifyInitialMembershipInvoice(intent,
|
|
3576
|
+
const evidence = verifyInitialMembershipInvoice(intent, event2.invoice);
|
|
3492
3577
|
if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
|
|
3493
|
-
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest,
|
|
3494
|
-
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...
|
|
3578
|
+
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3579
|
+
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event2, renewalAt: evidence.renewalAt };
|
|
3495
3580
|
}
|
|
3496
3581
|
if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
|
|
3497
3582
|
try {
|
|
@@ -3501,7 +3586,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3501
3586
|
}
|
|
3502
3587
|
}
|
|
3503
3588
|
}
|
|
3504
|
-
const invoiceEvent =
|
|
3589
|
+
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
3505
3590
|
const patch = { ...invoiceEvent && app.canceled === true ? {} : paymentEventPatch(authoritativeEvent, String(app.status ?? "")), ...membershipProjection };
|
|
3506
3591
|
if (invoiceEvent && typeof patch.renewalAt === "number" && patch.renewalAt > 0 && typeof app.renewalAt === "number" && patch.renewalAt < app.renewalAt) delete patch.renewalAt;
|
|
3507
3592
|
let next = app;
|
|
@@ -3535,7 +3620,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3535
3620
|
return json({ error: "refund account access reconciliation pending" }, 503);
|
|
3536
3621
|
}
|
|
3537
3622
|
}
|
|
3538
|
-
if (
|
|
3623
|
+
if (event2.kind === "first_payment" && next.status !== "refunded" && next.status !== "declined" && next.canceled !== true) {
|
|
3539
3624
|
try {
|
|
3540
3625
|
await notifyPaymentConfirmed(db, env, eventId, next);
|
|
3541
3626
|
} catch {
|
|
@@ -3554,9 +3639,9 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3554
3639
|
if (!approval.ok) return json({ error: "membership approval reconciliation pending" }, 503);
|
|
3555
3640
|
}
|
|
3556
3641
|
}
|
|
3557
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3642
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3558
3643
|
if (stored === "collision") {
|
|
3559
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3644
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3560
3645
|
return json({ ok: true, matched: false, collision: true });
|
|
3561
3646
|
}
|
|
3562
3647
|
return json({ ok: true });
|
|
@@ -3714,8 +3799,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3714
3799
|
if (!String(row2.disposition).startsWith("quarantine_") && row2.retryReady !== true) return json({ error: "event is already reconciled" }, 409);
|
|
3715
3800
|
try {
|
|
3716
3801
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3717
|
-
const
|
|
3718
|
-
if (provider.event.livemode !== (env.ODLA_ENV === "prod") || classifyMemberStripeEvent(stripeEventIdentity(
|
|
3802
|
+
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3803
|
+
if (provider.event.livemode !== (env.ODLA_ENV === "prod") || classifyMemberStripeEvent(stripeEventIdentity(event2), target, false) !== "apply") return json({ error: "provider event belongs to another or unknown runtime" }, 409);
|
|
3719
3804
|
const id6 = crypto.randomUUID();
|
|
3720
3805
|
await db.transact([
|
|
3721
3806
|
{ t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
|
|
@@ -3736,7 +3821,7 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3736
3821
|
equals: { digest: row2.digest, disposition: row2.disposition, ...row2.reviewId ? { reviewId: row2.reviewId } : {} },
|
|
3737
3822
|
...!row2.reviewId ? { absent: ["reviewId"] } : {}
|
|
3738
3823
|
}] });
|
|
3739
|
-
return reconcileChapterStripeEvent({ rawBody: provider.rawBody, eventId, event, origin: url.origin }, env, ctx);
|
|
3824
|
+
return reconcileChapterStripeEvent({ rawBody: provider.rawBody, eventId, event: event2, origin: url.origin }, env, ctx);
|
|
3740
3825
|
} catch {
|
|
3741
3826
|
return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
|
|
3742
3827
|
}
|
|
@@ -4296,7 +4381,7 @@ function stateOf(item, all, now, window, rivals) {
|
|
|
4296
4381
|
if (closed) dates.push(mine.until);
|
|
4297
4382
|
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
4298
4383
|
}
|
|
4299
|
-
var mechanism = (
|
|
4384
|
+
var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
4300
4385
|
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
4301
4386
|
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
4302
4387
|
function normalize(rows5, collection, parse, invalid2) {
|
|
@@ -4408,16 +4493,16 @@ var couponOf = (discount) => {
|
|
|
4408
4493
|
async function retermPolicy(db, scope2, id6) {
|
|
4409
4494
|
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4410
4495
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4411
|
-
let
|
|
4496
|
+
let policy2;
|
|
4412
4497
|
try {
|
|
4413
|
-
|
|
4498
|
+
policy2 = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4414
4499
|
} catch {
|
|
4415
4500
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4416
4501
|
}
|
|
4417
|
-
if (
|
|
4418
|
-
return
|
|
4502
|
+
if (policy2.id !== id6 || policy2.chapterId !== scope2.chapterId || policy2.status !== "published" || policy2.benefitDuration.kind !== "while_active" || policy2.percentOffBps <= 0 || policy2.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
|
|
4503
|
+
return policy2;
|
|
4419
4504
|
}
|
|
4420
|
-
async function attachProviderDiscount(sk, scope2, member,
|
|
4505
|
+
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4421
4506
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4422
4507
|
const read2 = await stripeRequest6({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4423
4508
|
const sub = read2.body, items = sub?.items?.data;
|
|
@@ -4433,8 +4518,8 @@ async function attachProviderDiscount(sk, scope2, member, policy, couponId, prod
|
|
|
4433
4518
|
const written = await stripeRequest6({ secretKey: sk }, "POST", path, {
|
|
4434
4519
|
"discounts[0][coupon]": couponId,
|
|
4435
4520
|
"expand[0]": "discounts",
|
|
4436
|
-
"metadata[discountPolicyVersionId]":
|
|
4437
|
-
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${
|
|
4521
|
+
"metadata[discountPolicyVersionId]": policy2.id
|
|
4522
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
|
|
4438
4523
|
const after = written.body?.discounts;
|
|
4439
4524
|
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4440
4525
|
membershipFail("membership_reterm_provider_unconfirmed");
|
|
@@ -4448,8 +4533,8 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4448
4533
|
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4449
4534
|
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id6) => typeof id6 !== "string" || !SAFE.test(id6)))) throw new TypeError("invalid membership ids");
|
|
4450
4535
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4451
|
-
const
|
|
4452
|
-
const receipt = { policyVersionId:
|
|
4536
|
+
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4537
|
+
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4453
4538
|
const skip = (membershipId, reason) => {
|
|
4454
4539
|
receipt.skipped.push({ membershipId, reason });
|
|
4455
4540
|
};
|
|
@@ -4470,7 +4555,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4470
4555
|
}
|
|
4471
4556
|
for (const member of rows5) {
|
|
4472
4557
|
const id6 = String(member.id);
|
|
4473
|
-
if (member.discountPolicyVersionId ===
|
|
4558
|
+
if (member.discountPolicyVersionId === policy2.id) {
|
|
4474
4559
|
receipt.unchanged.push(id6);
|
|
4475
4560
|
continue;
|
|
4476
4561
|
}
|
|
@@ -4482,22 +4567,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4482
4567
|
skip(id6, "not_founding");
|
|
4483
4568
|
continue;
|
|
4484
4569
|
}
|
|
4485
|
-
if (member.cohort !==
|
|
4570
|
+
if (member.cohort !== policy2.cohortLabel) {
|
|
4486
4571
|
skip(id6, "cohort_mismatch");
|
|
4487
4572
|
continue;
|
|
4488
4573
|
}
|
|
4489
4574
|
const skuId = String(member.baseSkuId);
|
|
4490
|
-
if (!
|
|
4575
|
+
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4491
4576
|
skip(id6, "sku_not_discounted");
|
|
4492
4577
|
continue;
|
|
4493
4578
|
}
|
|
4494
4579
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4495
|
-
const renewal = Math.round(base * (1e4 -
|
|
4580
|
+
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4496
4581
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4497
4582
|
skip(id6, "would_not_widen");
|
|
4498
4583
|
continue;
|
|
4499
4584
|
}
|
|
4500
|
-
const couponId = await membershipDiscountCouponId({ ...scope2, skuId },
|
|
4585
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
4501
4586
|
let providerUpdated;
|
|
4502
4587
|
try {
|
|
4503
4588
|
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
@@ -4515,10 +4600,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4515
4600
|
discountProductId: productId,
|
|
4516
4601
|
billingTerms: {
|
|
4517
4602
|
offeringVersionId: String(member.offeringVersionId),
|
|
4518
|
-
discount: { policyVersionId:
|
|
4603
|
+
discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
|
|
4519
4604
|
}
|
|
4520
4605
|
}, false);
|
|
4521
|
-
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member,
|
|
4606
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
|
|
4522
4607
|
} catch (error) {
|
|
4523
4608
|
const code = error.code;
|
|
4524
4609
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
@@ -4529,17 +4614,17 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4529
4614
|
}
|
|
4530
4615
|
try {
|
|
4531
4616
|
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4532
|
-
discountPolicyVersionId:
|
|
4533
|
-
discountSnapshot:
|
|
4617
|
+
discountPolicyVersionId: policy2.id,
|
|
4618
|
+
discountSnapshot: policy2,
|
|
4534
4619
|
renewalAmountCents: renewal,
|
|
4535
4620
|
revision: Number(member.revision) + 1,
|
|
4536
4621
|
updatedAt: now
|
|
4537
4622
|
})], {
|
|
4538
|
-
mutationId: `membership-reterm:${id6}:${
|
|
4623
|
+
mutationId: `membership-reterm:${id6}:${policy2.id}:${member.revision}`,
|
|
4539
4624
|
guards: [
|
|
4540
4625
|
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4541
4626
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4542
|
-
{ ns: "discountPolicyVersion", id:
|
|
4627
|
+
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4543
4628
|
]
|
|
4544
4629
|
});
|
|
4545
4630
|
} catch (error) {
|
|
@@ -4785,18 +4870,18 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
4785
4870
|
|
|
4786
4871
|
// src/member-session-application.ts
|
|
4787
4872
|
var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
|
|
4788
|
-
async function memberSessionApplication(db, chapterId, runtime,
|
|
4873
|
+
async function memberSessionApplication(db, chapterId, runtime, identity2) {
|
|
4789
4874
|
const lookup = async (where) => {
|
|
4790
4875
|
const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
|
|
4791
4876
|
if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
|
|
4792
4877
|
return rows5.filter((row2) => local(row2, chapterId, runtime));
|
|
4793
4878
|
};
|
|
4794
|
-
let apps = await lookup({ clerkUserId:
|
|
4879
|
+
let apps = await lookup({ clerkUserId: identity2.userId });
|
|
4795
4880
|
let emailFallback = false;
|
|
4796
|
-
if (!apps.length &&
|
|
4797
|
-
const normalized = normalizeChapterEmail(
|
|
4881
|
+
if (!apps.length && identity2.email) {
|
|
4882
|
+
const normalized = normalizeChapterEmail(identity2.email);
|
|
4798
4883
|
const normalizedRows = await lookup({ emailNormalized: normalized });
|
|
4799
|
-
const legacyRows = await lookup({ email:
|
|
4884
|
+
const legacyRows = await lookup({ email: identity2.email });
|
|
4800
4885
|
apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
|
|
4801
4886
|
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
|
|
4802
4887
|
emailFallback = true;
|
|
@@ -4806,8 +4891,8 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
4806
4891
|
if (emailFallback) {
|
|
4807
4892
|
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
|
|
4808
4893
|
try {
|
|
4809
|
-
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId:
|
|
4810
|
-
mutationId: `member-account-link:${app.id}:${
|
|
4894
|
+
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
|
|
4895
|
+
mutationId: `member-account-link:${app.id}:${identity2.userId}`,
|
|
4811
4896
|
guards: [{
|
|
4812
4897
|
ns: "applications",
|
|
4813
4898
|
id: String(app.id),
|
|
@@ -4820,7 +4905,7 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
4820
4905
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4821
4906
|
}
|
|
4822
4907
|
const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
|
|
4823
|
-
if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !==
|
|
4908
|
+
if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
|
|
4824
4909
|
app = saved;
|
|
4825
4910
|
}
|
|
4826
4911
|
const meeting = (await db.query({ meetings: { $: {
|
|
@@ -5760,7 +5845,7 @@ function foundingOf(offer) {
|
|
|
5760
5845
|
return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
|
|
5761
5846
|
}
|
|
5762
5847
|
async function createPaymentQuote(input) {
|
|
5763
|
-
const { group, tier, policy } = input;
|
|
5848
|
+
const { group, tier, policy: policy2 } = input;
|
|
5764
5849
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
5765
5850
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5766
5851
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
@@ -5783,9 +5868,9 @@ async function createPaymentQuote(input) {
|
|
|
5783
5868
|
dueTodayCents: lines.dueTodayCents
|
|
5784
5869
|
} : lines,
|
|
5785
5870
|
...presentation ? { presentation } : {},
|
|
5786
|
-
refundPolicyText:
|
|
5871
|
+
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5787
5872
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
5788
|
-
trustCopy:
|
|
5873
|
+
trustCopy: policy2 ? policy2.trustCopy ?? "" : String(group.trustCopy ?? ""),
|
|
5789
5874
|
...terms2 ? { commercial: {
|
|
5790
5875
|
offeringVersionId: terms2.offeringVersionId,
|
|
5791
5876
|
...terms2.discount ? {
|
|
@@ -5857,13 +5942,13 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5857
5942
|
if (error.code !== "membership_quote_expired") throw error;
|
|
5858
5943
|
}
|
|
5859
5944
|
}
|
|
5860
|
-
const
|
|
5945
|
+
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5861
5946
|
const guards = [{
|
|
5862
5947
|
ns: "applications",
|
|
5863
5948
|
id: id6,
|
|
5864
5949
|
exists: true,
|
|
5865
|
-
equals: Object.fromEntries(
|
|
5866
|
-
absent:
|
|
5950
|
+
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
5951
|
+
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
5867
5952
|
}, { ns: "subscriptionCheckoutIntents", id: id6, exists: false }];
|
|
5868
5953
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
5869
5954
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
@@ -5999,11 +6084,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
5999
6084
|
const tier = findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null);
|
|
6000
6085
|
if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
|
|
6001
6086
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
6002
|
-
const
|
|
6087
|
+
const policy2 = control3?.document.policy?.tierPolicies?.[tier.id];
|
|
6003
6088
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6004
6089
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6005
6090
|
db,
|
|
6006
|
-
{ ...target, app, group, tier, policy, presentation, now: Date.now() },
|
|
6091
|
+
{ ...target, app, group, tier, policy: policy2, presentation, now: Date.now() },
|
|
6007
6092
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6008
6093
|
);
|
|
6009
6094
|
const quote = await createPaymentQuote({
|
|
@@ -6014,7 +6099,7 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6014
6099
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6015
6100
|
group,
|
|
6016
6101
|
tier,
|
|
6017
|
-
policy,
|
|
6102
|
+
policy: policy2,
|
|
6018
6103
|
presentation
|
|
6019
6104
|
});
|
|
6020
6105
|
return { tier, quote, billingTerms: void 0 };
|
|
@@ -7161,15 +7246,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7161
7246
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
7162
7247
|
else for (const [id6, raw2] of Object.entries(overrides)) {
|
|
7163
7248
|
const path = `document.policy.tierPolicies.${id6}`;
|
|
7164
|
-
const
|
|
7165
|
-
if (!tiers.some((tier) => tier.id === id6) || !
|
|
7249
|
+
const policy2 = recordOf(raw2);
|
|
7250
|
+
if (!tiers.some((tier) => tier.id === id6) || !policy2) {
|
|
7166
7251
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7167
7252
|
continue;
|
|
7168
7253
|
}
|
|
7169
|
-
unknownKeys(
|
|
7254
|
+
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7170
7255
|
tierPolicies[id6] = {
|
|
7171
|
-
refundPolicyText: text4(
|
|
7172
|
-
...
|
|
7256
|
+
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7257
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7173
7258
|
};
|
|
7174
7259
|
}
|
|
7175
7260
|
}
|
|
@@ -7224,7 +7309,7 @@ function validateSignupControlDocument(input) {
|
|
|
7224
7309
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
7225
7310
|
}
|
|
7226
7311
|
const application = applicationOf(root.application, errors);
|
|
7227
|
-
const
|
|
7312
|
+
const policy2 = policyOf(root.policy, errors, tiers);
|
|
7228
7313
|
let scheduling;
|
|
7229
7314
|
if (root.scheduling !== void 0) {
|
|
7230
7315
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -7244,7 +7329,7 @@ function validateSignupControlDocument(input) {
|
|
|
7244
7329
|
tiers,
|
|
7245
7330
|
membershipPage: page.value,
|
|
7246
7331
|
...application ? { application } : {},
|
|
7247
|
-
...
|
|
7332
|
+
...policy2 ? { policy: policy2 } : {},
|
|
7248
7333
|
...scheduling ? { scheduling } : {},
|
|
7249
7334
|
...theme ? { theme } : {},
|
|
7250
7335
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -8988,8 +9073,8 @@ function createStripeProvider(config) {
|
|
|
8988
9073
|
if (!webhookSecret || !await verifyStripeSignature2(rawBody, sigHeader, webhookSecret)) {
|
|
8989
9074
|
return { ok: false, reason: "bad_signature" };
|
|
8990
9075
|
}
|
|
8991
|
-
const
|
|
8992
|
-
return { ok: true, eventId:
|
|
9076
|
+
const event2 = JSON.parse(rawBody);
|
|
9077
|
+
return { ok: true, eventId: event2.id, event: normalizeWebhookEvent(event2, { requireProviderPayment: config.requireProviderPayment }) };
|
|
8993
9078
|
},
|
|
8994
9079
|
async refund(input) {
|
|
8995
9080
|
return refundSubscriptionPayment(requireSecret(secretKey), input);
|
|
@@ -9103,7 +9188,7 @@ async function startGiftIntent(db, input) {
|
|
|
9103
9188
|
throw new Error("gift purchase intent does not match this checkout");
|
|
9104
9189
|
}
|
|
9105
9190
|
if (stored.status === "paid") throw new Error("gift purchase is already paid");
|
|
9106
|
-
const
|
|
9191
|
+
const checkout2 = await startGiftProviderCheckout(
|
|
9107
9192
|
input.stripeSecret,
|
|
9108
9193
|
stored,
|
|
9109
9194
|
input.purchaserEmail,
|
|
@@ -9113,15 +9198,15 @@ async function startGiftIntent(db, input) {
|
|
|
9113
9198
|
);
|
|
9114
9199
|
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
|
|
9115
9200
|
status: "checkout_ready",
|
|
9116
|
-
providerObjectId:
|
|
9117
|
-
...
|
|
9201
|
+
providerObjectId: checkout2.providerObjectId,
|
|
9202
|
+
...checkout2.customerId ? { providerCustomerId: checkout2.customerId } : {},
|
|
9118
9203
|
checkoutStartedAt: input.now,
|
|
9119
9204
|
updatedAt: input.now
|
|
9120
|
-
} }], { mutationId: `gift-checkout-ready:${input.id}:${
|
|
9205
|
+
} }], { mutationId: `gift-checkout-ready:${input.id}:${checkout2.providerObjectId}` });
|
|
9121
9206
|
return {
|
|
9122
9207
|
gift: true,
|
|
9123
9208
|
intentId: input.id,
|
|
9124
|
-
clientSecret:
|
|
9209
|
+
clientSecret: checkout2.clientSecret,
|
|
9125
9210
|
publishableKey: input.publishableKey,
|
|
9126
9211
|
lineItems: { standardCents: input.offer.priceCents, discountCents: 0, dueTodayCents: input.offer.priceCents },
|
|
9127
9212
|
...giftCode ? { giftCode } : {}
|
|
@@ -9379,15 +9464,15 @@ async function startSubscription(req, env, ctx) {
|
|
|
9379
9464
|
return json({ error: error instanceof Error ? error.message : "gift checkout failed" }, 502);
|
|
9380
9465
|
}
|
|
9381
9466
|
}
|
|
9382
|
-
let
|
|
9467
|
+
let checkout2;
|
|
9383
9468
|
try {
|
|
9384
|
-
|
|
9469
|
+
checkout2 = await checkoutQuoteForApplication(db, app, group, env, ctx);
|
|
9385
9470
|
} catch (error) {
|
|
9386
9471
|
const expired = error.code === "membership_quote_expired";
|
|
9387
9472
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9388
9473
|
}
|
|
9389
|
-
const { tier, quote, billingTerms } =
|
|
9390
|
-
const membershipOffer = "membershipOffer" in
|
|
9474
|
+
const { tier, quote, billingTerms } = checkout2;
|
|
9475
|
+
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9391
9476
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9392
9477
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
9393
9478
|
if (tierIsFree(tier)) return json({ error: "tier is free", code: "tier_not_payable" }, 409);
|
|
@@ -9809,13 +9894,6 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9809
9894
|
}
|
|
9810
9895
|
};
|
|
9811
9896
|
|
|
9812
|
-
// src/named-seat-accepted-template.ts
|
|
9813
|
-
var SEAT_ACCEPTED_TEMPLATE = {
|
|
9814
|
-
enabled: true,
|
|
9815
|
-
subject: "{{recipientName}} accepted your {{chapterName}} seat",
|
|
9816
|
-
text: "Hi {{firstName}},\n\n{{recipientName}} has accepted the {{tierName}} seat you purchased at {{chapterName}}. They will complete their own application and any membership requirements from here; nothing more is needed from you.\n\nWarmly,\n{{chapterName}}"
|
|
9817
|
-
};
|
|
9818
|
-
|
|
9819
9897
|
// src/named-seat-accepted-notice.ts
|
|
9820
9898
|
var text5 = (value) => typeof value === "string" ? value : "";
|
|
9821
9899
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
@@ -9826,7 +9904,6 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
9826
9904
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9827
9905
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
9828
9906
|
const config = emailGroupFrom(group);
|
|
9829
|
-
config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
|
|
9830
9907
|
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
|
|
9831
9908
|
return sendTemplated({
|
|
9832
9909
|
db,
|
|
@@ -9842,6 +9919,7 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
9842
9919
|
to: text5(purchaser.email),
|
|
9843
9920
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
9844
9921
|
applicationId: input.primaryApplicationId,
|
|
9922
|
+
tierId: input.skuId,
|
|
9845
9923
|
vars: {
|
|
9846
9924
|
firstName: text5(purchaser.firstName),
|
|
9847
9925
|
purchaserName,
|
|
@@ -9858,7 +9936,7 @@ async function tierName(db, group, skuId) {
|
|
|
9858
9936
|
if (tier) return tier.name;
|
|
9859
9937
|
} catch {
|
|
9860
9938
|
}
|
|
9861
|
-
return
|
|
9939
|
+
return "";
|
|
9862
9940
|
}
|
|
9863
9941
|
|
|
9864
9942
|
// src/worker-routes-named-seat-recipient.ts
|
|
@@ -11073,7 +11151,122 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11073
11151
|
return patchApplication(db, env, ctx, app, body);
|
|
11074
11152
|
};
|
|
11075
11153
|
|
|
11154
|
+
// src/email-admin-config.ts
|
|
11155
|
+
import { getConfig, saveConfig, validateEmailCopy as validateEmailCopy4 } from "@odla-ai/crm";
|
|
11156
|
+
|
|
11157
|
+
// src/email-readiness.ts
|
|
11158
|
+
import { isEmailTierName as isEmailTierName2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
11159
|
+
function validateEmailReadiness(input) {
|
|
11160
|
+
const issues = [];
|
|
11161
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
11162
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
11163
|
+
}
|
|
11164
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
11165
|
+
for (const tier of input.tiers ?? []) {
|
|
11166
|
+
if (!isEmailTierName2(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
11167
|
+
}
|
|
11168
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => isEmailTierName2(tier.name, tier.id));
|
|
11169
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
11170
|
+
const events = Object.entries(input.events).map(([key3, event2]) => {
|
|
11171
|
+
const template = input.templates[key3];
|
|
11172
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy3(template, { allowedVariables: event2.variables });
|
|
11173
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
11174
|
+
if (template) {
|
|
11175
|
+
const used = [...`${template.subject ?? ""}
|
|
11176
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
11177
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
11178
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
11179
|
+
}
|
|
11180
|
+
}
|
|
11181
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
11182
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
11183
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
11184
|
+
});
|
|
11185
|
+
return { checkedAt: input.now ?? Date.now(), events, issues };
|
|
11186
|
+
}
|
|
11187
|
+
|
|
11188
|
+
// src/email-admin-config.ts
|
|
11189
|
+
var str2 = (value) => typeof value === "string" ? value : "";
|
|
11190
|
+
async function emailSettings(db, ctx, env) {
|
|
11191
|
+
const hub = ctx.chapter.mode === "hub";
|
|
11192
|
+
const crmDeps3 = { db, crm: ctx.chapter.crm };
|
|
11193
|
+
const group = hub ? await getConfig(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
11194
|
+
const raw2 = group?.emailTemplates;
|
|
11195
|
+
const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
|
|
11196
|
+
const events = chapterEmailEvents(ctx.chapter);
|
|
11197
|
+
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
11198
|
+
subject: str2(stored[key3]?.subject),
|
|
11199
|
+
text: str2(stored[key3]?.text),
|
|
11200
|
+
enabled: stored[key3]?.enabled
|
|
11201
|
+
}]));
|
|
11202
|
+
let tiers = [], tiersUnavailable = false;
|
|
11203
|
+
if (!hub) {
|
|
11204
|
+
try {
|
|
11205
|
+
tiers = await loadTiers(db, group ?? { id: ctx.chapter.id });
|
|
11206
|
+
} catch {
|
|
11207
|
+
tiersUnavailable = true;
|
|
11208
|
+
}
|
|
11209
|
+
}
|
|
11210
|
+
const groupValues = Object.fromEntries(["refundPolicyText", "commitmentText", "normsText"].map((key3) => [key3, str2(group?.[key3])]));
|
|
11211
|
+
const validation = validateEmailReadiness({ events, templates: stored, groupValues, tiers: hub ? void 0 : tiers, tiersUnavailable });
|
|
11212
|
+
return { group, events, data: {
|
|
11213
|
+
groupId: ctx.chapter.id,
|
|
11214
|
+
name: ctx.chapter.name,
|
|
11215
|
+
emailTemplates,
|
|
11216
|
+
validation,
|
|
11217
|
+
replyTo: str2(group?.replyTo),
|
|
11218
|
+
fromName: str2(group?.fromName),
|
|
11219
|
+
notificationEmail: str2(group?.notificationEmail),
|
|
11220
|
+
debugEmail: str2(group?.debugEmail),
|
|
11221
|
+
...groupValues,
|
|
11222
|
+
envName: env.ODLA_ENV,
|
|
11223
|
+
testAvailable: !hub,
|
|
11224
|
+
transport: env.SEND_EMAIL && env.EMAIL_FROM ? "cloudflare" : "log-only",
|
|
11225
|
+
fromEmail: env.EMAIL_FROM ?? null
|
|
11226
|
+
} };
|
|
11227
|
+
}
|
|
11228
|
+
async function updateEmailSettings(db, ctx, env, body) {
|
|
11229
|
+
const { group, events } = await emailSettings(db, ctx, env);
|
|
11230
|
+
if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
|
|
11231
|
+
const templates = body.emailTemplates;
|
|
11232
|
+
if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
|
|
11233
|
+
const clean = {};
|
|
11234
|
+
for (const [key3, value] of Object.entries(templates)) {
|
|
11235
|
+
if (!events[key3]) continue;
|
|
11236
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
11237
|
+
const t = value;
|
|
11238
|
+
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
11239
|
+
const subject = str2(t.subject).trim(), text8 = str2(t.text);
|
|
11240
|
+
const issues = validateEmailCopy4({ subject, text: text8 }, { allowedVariables: events[key3].variables });
|
|
11241
|
+
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
11242
|
+
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
11243
|
+
}
|
|
11244
|
+
clean[key3] = { subject, text: text8, enabled: t.enabled };
|
|
11245
|
+
}
|
|
11246
|
+
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
11247
|
+
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
11248
|
+
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
11249
|
+
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
11250
|
+
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
11251
|
+
const fromName = str2(body.fromName).trim();
|
|
11252
|
+
if (/[\r\n<>]/.test(fromName) || fromName.length > 100) return json({ error: "from name must be one line of at most 100 characters with no angle brackets" }, 400);
|
|
11253
|
+
const commitmentText = str2(body.commitmentText), normsText = str2(body.normsText);
|
|
11254
|
+
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
11255
|
+
if (ctx.chapter.mode === "hub") {
|
|
11256
|
+
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11257
|
+
await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11258
|
+
} else {
|
|
11259
|
+
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11260
|
+
await db.transact([
|
|
11261
|
+
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
11262
|
+
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11263
|
+
]);
|
|
11264
|
+
}
|
|
11265
|
+
return json({ ok: true });
|
|
11266
|
+
}
|
|
11267
|
+
|
|
11076
11268
|
// src/worker-routes-admin-comms.ts
|
|
11269
|
+
import { listEmailLog } from "@odla-ai/crm";
|
|
11077
11270
|
async function gate5(req, env, ctx) {
|
|
11078
11271
|
const rawDb = ctx.makeDb(env);
|
|
11079
11272
|
const u = await ctx.verifyUser(req, env);
|
|
@@ -11085,39 +11278,13 @@ async function loadGroup2(db, id6) {
|
|
|
11085
11278
|
const { groups } = await db.query({ groups: { $: { where: { id: id6 }, limit: 1 } } });
|
|
11086
11279
|
return groups?.[0] ?? null;
|
|
11087
11280
|
}
|
|
11088
|
-
var
|
|
11281
|
+
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
11089
11282
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
11090
11283
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
11091
11284
|
const got = await gate5(req, env, ctx);
|
|
11092
11285
|
if (got instanceof Response) return got;
|
|
11093
11286
|
const db = got;
|
|
11094
|
-
|
|
11095
|
-
if (!group) return json({ error: "not found" }, 404);
|
|
11096
|
-
if (req.method === "GET") {
|
|
11097
|
-
const stored = group.emailTemplates ?? {};
|
|
11098
|
-
const emailTemplates = {};
|
|
11099
|
-
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
11100
|
-
const t = stored[key3];
|
|
11101
|
-
if (t) emailTemplates[key3] = { subject: str2(t.subject), text: str2(t.text), enabled: t.enabled !== false };
|
|
11102
|
-
}
|
|
11103
|
-
return json({
|
|
11104
|
-
groupId: group.id,
|
|
11105
|
-
name: group.name,
|
|
11106
|
-
replyTo: str2(group.replyTo),
|
|
11107
|
-
fromName: str2(group.fromName),
|
|
11108
|
-
notificationEmail: str2(group.notificationEmail),
|
|
11109
|
-
debugEmail: str2(group.debugEmail),
|
|
11110
|
-
emailTemplates,
|
|
11111
|
-
commitmentText: str2(group.commitmentText),
|
|
11112
|
-
normsText: str2(group.normsText),
|
|
11113
|
-
refundPolicyText: str2(group.refundPolicyText),
|
|
11114
|
-
// Read-only delivery wiring — surfaced so "why did this not send?" is
|
|
11115
|
-
// answerable without logs.
|
|
11116
|
-
envName: env.ODLA_ENV,
|
|
11117
|
-
transport: env.SEND_EMAIL && env.EMAIL_FROM ? "cloudflare" : "log-only",
|
|
11118
|
-
fromEmail: env.EMAIL_FROM ?? null
|
|
11119
|
-
});
|
|
11120
|
-
}
|
|
11287
|
+
if (req.method === "GET") return json((await emailSettings(db, ctx, env)).data);
|
|
11121
11288
|
let body;
|
|
11122
11289
|
try {
|
|
11123
11290
|
body = await req.json();
|
|
@@ -11125,41 +11292,16 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
11125
11292
|
return json({ error: "invalid JSON body" }, 400);
|
|
11126
11293
|
}
|
|
11127
11294
|
if (!body || typeof body !== "object" || Array.isArray(body)) return json({ error: "object body required" }, 400);
|
|
11128
|
-
|
|
11129
|
-
if (!templates || typeof templates !== "object") return json({ error: "emailTemplates object required" }, 400);
|
|
11130
|
-
const clean = {};
|
|
11131
|
-
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
11132
|
-
const t = templates[key3];
|
|
11133
|
-
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
11134
|
-
const text8 = typeof t?.text === "string" ? t.text : "";
|
|
11135
|
-
if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
|
|
11136
|
-
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
|
|
11137
|
-
if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
|
|
11138
|
-
clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
|
|
11139
|
-
}
|
|
11140
|
-
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11141
|
-
const notificationEmail = str2(body.notificationEmail).trim();
|
|
11142
|
-
const replyTo = str2(body.replyTo).trim();
|
|
11143
|
-
const debugEmail = str2(body.debugEmail).trim();
|
|
11144
|
-
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
11145
|
-
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
11146
|
-
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
11147
|
-
const fromName = str2(body.fromName).trim();
|
|
11148
|
-
if (/[\r\n<>]/.test(fromName) || fromName.length > 100) return json({ error: "from name must be one line of at most 100 characters with no angle brackets" }, 400);
|
|
11149
|
-
const commitmentText = str2(body.commitmentText);
|
|
11150
|
-
const normsText = str2(body.normsText);
|
|
11151
|
-
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
11152
|
-
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11153
|
-
await db.transact([
|
|
11154
|
-
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
11155
|
-
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11156
|
-
]);
|
|
11157
|
-
return json({ ok: true });
|
|
11295
|
+
return updateEmailSettings(db, ctx, env, body);
|
|
11158
11296
|
};
|
|
11159
11297
|
var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
11160
11298
|
if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
|
|
11161
11299
|
const got = await gate5(req, env, ctx);
|
|
11162
11300
|
if (got instanceof Response) return got;
|
|
11301
|
+
if (ctx.chapter.mode === "hub") {
|
|
11302
|
+
const rows5 = await listEmailLog({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
11303
|
+
return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
11304
|
+
}
|
|
11163
11305
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
11164
11306
|
const visible = [];
|
|
11165
11307
|
for (const row2 of emailLog ?? []) {
|
|
@@ -11195,21 +11337,22 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
11195
11337
|
} catch {
|
|
11196
11338
|
return json({ error: "invalid JSON body" }, 400);
|
|
11197
11339
|
}
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
}
|
|
11340
|
+
if (ctx.chapter.mode === "hub") return json({ error: "Send CRM mail from a selected contact." }, 400);
|
|
11341
|
+
const template = str3(body.template);
|
|
11342
|
+
const events = chapterEmailEvents(ctx.chapter);
|
|
11343
|
+
if (!events[template]) return json({ error: "Unknown email event." }, 400);
|
|
11202
11344
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
11203
11345
|
if (!group) return json({ error: "not found" }, 404);
|
|
11204
11346
|
const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
11205
11347
|
group: emailGroupFrom(group),
|
|
11206
11348
|
template,
|
|
11207
|
-
to:
|
|
11349
|
+
to: str3(group.notificationEmail),
|
|
11208
11350
|
vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
|
|
11209
11351
|
dedupeKey: `test:${template}:${Date.now()}`,
|
|
11210
|
-
force: true
|
|
11352
|
+
force: true,
|
|
11353
|
+
event: events[template]
|
|
11211
11354
|
});
|
|
11212
|
-
return json({ ok: res.sent, reason: res.reason ?? null, to:
|
|
11355
|
+
return json({ ok: res.sent, reason: res.reason ?? null, to: str3(group.notificationEmail), redirected: env.ODLA_ENV !== "prod" && !!group.debugEmail });
|
|
11213
11356
|
};
|
|
11214
11357
|
var handleAdminComms = async (req, url, env, ctx) => {
|
|
11215
11358
|
const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
|
|
@@ -11231,13 +11374,13 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
11231
11374
|
});
|
|
11232
11375
|
const app = data.applications?.[0] ?? null;
|
|
11233
11376
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
11234
|
-
const group = await loadGroup2(db,
|
|
11235
|
-
const vars = app ? { firstName:
|
|
11377
|
+
const group = await loadGroup2(db, str3(app?.groupId, ctx.chapter.id));
|
|
11378
|
+
const vars = app ? { firstName: str3(app.firstName), lastName: str3(app.lastName), email: str3(app.email), phone: str3(app.phone), state: str3(app.state), adminUrl: adminUrlFor(url.origin), membersUrl: membersUrlFor(url.origin) } : null;
|
|
11236
11379
|
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
11237
11380
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
11238
11381
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
11239
11382
|
const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
|
|
11240
|
-
return { kind: "email", channel, label: String(r.template), subject:
|
|
11383
|
+
return { kind: "email", channel, label: String(r.template), subject: str3(r.subject), to: r.to ?? null, body: mailBody, at: r.sentAt, error: r.error ?? null };
|
|
11241
11384
|
});
|
|
11242
11385
|
const calendar = (data.meetings ?? []).map((m2) => ({
|
|
11243
11386
|
kind: "calendar",
|
|
@@ -12239,13 +12382,13 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12239
12382
|
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
12240
12383
|
const got = await gate8(req, env, ctx);
|
|
12241
12384
|
if ("response" in got) return got.response;
|
|
12242
|
-
const
|
|
12243
|
-
if (!
|
|
12385
|
+
const identity2 = noteIdentity(url, ctx);
|
|
12386
|
+
if (!identity2) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
12244
12387
|
if (req.method === "GET") {
|
|
12245
12388
|
const result = await got.db.query({
|
|
12246
12389
|
networkNotes: {
|
|
12247
12390
|
$: {
|
|
12248
|
-
where: { recordKey:
|
|
12391
|
+
where: { recordKey: identity2.recordKey },
|
|
12249
12392
|
order: { createdAt: "desc" },
|
|
12250
12393
|
limit: 100
|
|
12251
12394
|
}
|
|
@@ -12266,9 +12409,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12266
12409
|
const id6 = crypto.randomUUID();
|
|
12267
12410
|
const note = {
|
|
12268
12411
|
id: id6,
|
|
12269
|
-
targetId:
|
|
12270
|
-
recordType:
|
|
12271
|
-
recordId:
|
|
12412
|
+
targetId: identity2.targetId,
|
|
12413
|
+
recordType: identity2.recordType,
|
|
12414
|
+
recordId: identity2.recordId,
|
|
12272
12415
|
body,
|
|
12273
12416
|
authorId: got.user.userId,
|
|
12274
12417
|
...got.user.email ? { authorEmail: got.user.email } : {},
|
|
@@ -12278,7 +12421,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12278
12421
|
t: "update",
|
|
12279
12422
|
ns: "networkNotes",
|
|
12280
12423
|
id: id6,
|
|
12281
|
-
attrs: { ...note, recordKey:
|
|
12424
|
+
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12282
12425
|
}]);
|
|
12283
12426
|
return json({ note }, 201);
|
|
12284
12427
|
};
|
|
@@ -12544,8 +12687,8 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
12544
12687
|
}
|
|
12545
12688
|
}
|
|
12546
12689
|
const seen = /* @__PURE__ */ new Set();
|
|
12547
|
-
for (const
|
|
12548
|
-
const key3 = `${
|
|
12690
|
+
for (const identity2 of identities) {
|
|
12691
|
+
const key3 = `${identity2.kind}:${identity2.value}`;
|
|
12549
12692
|
if (seen.has(key3)) errors.push("duplicate identity in import");
|
|
12550
12693
|
seen.add(key3);
|
|
12551
12694
|
}
|
|
@@ -12601,8 +12744,8 @@ async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
|
12601
12744
|
async function networkChapterPersonSourceKey(chapterId, chapterPersonId) {
|
|
12602
12745
|
return `chapter_source_${await sha256(`${chapterId}\0${chapterPersonId}`)}`;
|
|
12603
12746
|
}
|
|
12604
|
-
function projectNetworkContactPolicy(
|
|
12605
|
-
return links.map((link) => ({ ...link, contactPolicy: { ...
|
|
12747
|
+
function projectNetworkContactPolicy(policy2, links) {
|
|
12748
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy2 } }));
|
|
12606
12749
|
}
|
|
12607
12750
|
function canonicalNetworkPersonId(personId, persons) {
|
|
12608
12751
|
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
@@ -12908,11 +13051,11 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
12908
13051
|
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
12909
13052
|
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
12910
13053
|
}
|
|
12911
|
-
const
|
|
12912
|
-
const next = { ...person, contactPolicy:
|
|
13054
|
+
const policy2 = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
13055
|
+
const next = { ...person, contactPolicy: policy2, revision: person.revision + 1, updatedAt: now };
|
|
12913
13056
|
await db.transact([
|
|
12914
13057
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
12915
|
-
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed:
|
|
13058
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy2.marketingSuppressed, discretionarySuppressed: policy2.discretionarySuppressed }, { personId: person.id })
|
|
12916
13059
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
12917
13060
|
return next;
|
|
12918
13061
|
}
|
|
@@ -13037,13 +13180,13 @@ async function derivedMutationId(batchMutationId, part) {
|
|
|
13037
13180
|
return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
|
|
13038
13181
|
}
|
|
13039
13182
|
function matchingEvent(events, mutationId, kind) {
|
|
13040
|
-
return events.find((
|
|
13183
|
+
return events.find((event2) => event2.mutationId === mutationId && event2.kind === kind);
|
|
13041
13184
|
}
|
|
13042
|
-
function eventString(
|
|
13043
|
-
return typeof
|
|
13185
|
+
function eventString(event2, key3) {
|
|
13186
|
+
return typeof event2.detail[key3] === "string" ? String(event2.detail[key3]) : "";
|
|
13044
13187
|
}
|
|
13045
|
-
function eventCounts(
|
|
13046
|
-
const value =
|
|
13188
|
+
function eventCounts(event2) {
|
|
13189
|
+
const value = event2.detail.counts;
|
|
13047
13190
|
return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
|
|
13048
13191
|
}
|
|
13049
13192
|
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
@@ -13170,11 +13313,11 @@ var redactedNetworkPersonLink = async (link) => ({
|
|
|
13170
13313
|
state: link.state,
|
|
13171
13314
|
createdAt: link.createdAt
|
|
13172
13315
|
});
|
|
13173
|
-
var safeNetworkContactPolicy = (
|
|
13174
|
-
marketingSuppressed:
|
|
13175
|
-
discretionarySuppressed:
|
|
13176
|
-
hasReason: Boolean(
|
|
13177
|
-
updatedAt:
|
|
13316
|
+
var safeNetworkContactPolicy = (policy2) => ({
|
|
13317
|
+
marketingSuppressed: policy2.marketingSuppressed,
|
|
13318
|
+
discretionarySuppressed: policy2.discretionarySuppressed,
|
|
13319
|
+
hasReason: Boolean(policy2.reason),
|
|
13320
|
+
updatedAt: policy2.updatedAt
|
|
13178
13321
|
});
|
|
13179
13322
|
var safeNetworkPersonAlias = async (alias) => ({
|
|
13180
13323
|
id: alias.id,
|
|
@@ -13229,14 +13372,14 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13229
13372
|
aliases,
|
|
13230
13373
|
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
13231
13374
|
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
13232
|
-
recentEvents: state.events.slice(0, 100).map((
|
|
13233
|
-
id:
|
|
13234
|
-
kind:
|
|
13235
|
-
personId:
|
|
13236
|
-
aliasId:
|
|
13237
|
-
actorId:
|
|
13238
|
-
mutationId:
|
|
13239
|
-
createdAt:
|
|
13375
|
+
recentEvents: state.events.slice(0, 100).map((event2) => ({
|
|
13376
|
+
id: event2.id,
|
|
13377
|
+
kind: event2.kind,
|
|
13378
|
+
personId: event2.personId ?? null,
|
|
13379
|
+
aliasId: event2.aliasId ?? null,
|
|
13380
|
+
actorId: event2.actorId,
|
|
13381
|
+
mutationId: event2.mutationId,
|
|
13382
|
+
createdAt: event2.createdAt
|
|
13240
13383
|
}))
|
|
13241
13384
|
};
|
|
13242
13385
|
}
|
|
@@ -13251,7 +13394,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
13251
13394
|
const target = input.kind === "person" ? state.persons.find((row2) => row2.id === input.id) : state.aliases.find((row2) => row2.id === input.id);
|
|
13252
13395
|
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
13253
13396
|
const ids2 = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
13254
|
-
const
|
|
13397
|
+
const event2 = {
|
|
13255
13398
|
t: "update",
|
|
13256
13399
|
ns: "networkPersonEvents",
|
|
13257
13400
|
id: `network_person_event:${mutationId}`,
|
|
@@ -13265,7 +13408,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
13265
13408
|
createdAt: now
|
|
13266
13409
|
}
|
|
13267
13410
|
};
|
|
13268
|
-
await db.transact([
|
|
13411
|
+
await db.transact([event2], {
|
|
13269
13412
|
mutationId,
|
|
13270
13413
|
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
13271
13414
|
});
|
|
@@ -13849,8 +13992,8 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13849
13992
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
13850
13993
|
}
|
|
13851
13994
|
try {
|
|
13852
|
-
const
|
|
13853
|
-
const fact = normalizeNetworkCommercialStripeEvent(
|
|
13995
|
+
const event2 = await constructStripeEvent(body, signature, secret);
|
|
13996
|
+
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
13854
13997
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
13855
13998
|
const result = await ingestNetworkCommercialFact(
|
|
13856
13999
|
db,
|
|
@@ -15449,7 +15592,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15449
15592
|
};
|
|
15450
15593
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
15451
15594
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
15452
|
-
const
|
|
15595
|
+
const checkout2 = {
|
|
15453
15596
|
...target,
|
|
15454
15597
|
priceId: peer.seatPriceId,
|
|
15455
15598
|
recipientName: String(input.recipientName),
|
|
@@ -15457,7 +15600,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15457
15600
|
purchaserEmail: String(input.purchaserEmail),
|
|
15458
15601
|
purchaserUserId: String(input.purchaserUserId)
|
|
15459
15602
|
};
|
|
15460
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
15603
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
15461
15604
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
15462
15605
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
15463
15606
|
if (action === "seat-decline") {
|
|
@@ -15473,7 +15616,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15473
15616
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
15474
15617
|
}
|
|
15475
15618
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
15476
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
15619
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
15477
15620
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
15478
15621
|
if (action === "seat-payments") {
|
|
15479
15622
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -15544,9 +15687,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15544
15687
|
const identities = /* @__PURE__ */ new Set();
|
|
15545
15688
|
for (const peer of peers) {
|
|
15546
15689
|
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE2.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");
|
|
15547
|
-
const
|
|
15548
|
-
if (identities.has(
|
|
15549
|
-
identities.add(
|
|
15690
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
15691
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
15692
|
+
identities.add(identity2);
|
|
15550
15693
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
15551
15694
|
}
|
|
15552
15695
|
return async (req) => {
|
|
@@ -16150,6 +16293,9 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
16150
16293
|
}
|
|
16151
16294
|
}
|
|
16152
16295
|
|
|
16296
|
+
// src/membership-chapter-delivery.ts
|
|
16297
|
+
import { validateEmailCopy as validateEmailCopy5 } from "@odla-ai/crm";
|
|
16298
|
+
|
|
16153
16299
|
// src/invitation-test-delivery.ts
|
|
16154
16300
|
function invitationDelivery(env, recipientEmail) {
|
|
16155
16301
|
const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
|
|
@@ -16212,16 +16358,13 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
16212
16358
|
const join = new URL(applicationPath, origin);
|
|
16213
16359
|
join.searchParams.set("seat", effect.seatId);
|
|
16214
16360
|
join.searchParams.set("tier", effect.skuId);
|
|
16215
|
-
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
|
|
16216
|
-
enabled: true,
|
|
16217
|
-
subject: "{{purchaserName}} invited you to {{chapterName}}",
|
|
16218
|
-
text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation here:\n{{joinUrl}}\n\nYou will complete your own application and membership requirements. You will not be asked to pay for this seat."
|
|
16219
|
-
} };
|
|
16220
16361
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
16221
|
-
if (!template.text.includes("{{purchaserName}}"))
|
|
16222
|
-
...
|
|
16223
|
-
|
|
16224
|
-
|
|
16362
|
+
if (template && !validateEmailCopy5(template).length && !template.text.includes("{{purchaserName}}")) {
|
|
16363
|
+
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
|
|
16364
|
+
...template,
|
|
16365
|
+
text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
|
|
16366
|
+
} };
|
|
16367
|
+
}
|
|
16225
16368
|
stage = "invitation_delivery";
|
|
16226
16369
|
const result = await sendTemplated({
|
|
16227
16370
|
db,
|
|
@@ -16241,7 +16384,8 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
16241
16384
|
joinUrl: join.href
|
|
16242
16385
|
},
|
|
16243
16386
|
dedupeKey: `membership-effect:${effect.effectId}`,
|
|
16244
|
-
applicationId: input.applicationId
|
|
16387
|
+
applicationId: input.applicationId,
|
|
16388
|
+
tierId: effect.skuId
|
|
16245
16389
|
});
|
|
16246
16390
|
if (!result.sent) throw new Error("invitation delivery unconfirmed");
|
|
16247
16391
|
} else {
|