@odla-ai/chapter 0.54.0 → 0.54.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/{chunk-NDZCR3OW.js → chunk-ZAE24YKY.js} +54 -21
- package/dist/{chunk-NDZCR3OW.js.map → chunk-ZAE24YKY.js.map} +1 -1
- package/dist/{copy-context-qvFo6eA7.d.ts → copy-context-CaUXIxUI.d.ts} +17 -2
- package/dist/index.cjs +281 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -17
- package/dist/index.d.ts +76 -17
- package/dist/index.js +266 -117
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +689 -544
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +26 -3
- package/dist/worker/index.d.ts +26 -3
- package/dist/worker/index.js +635 -490
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
package/dist/worker/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(worker_exports, {
|
|
|
37
37
|
deliverSignupControl: () => deliverSignupControl,
|
|
38
38
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
39
39
|
grantIncludedNamedSeat: () => grantIncludedNamedSeat,
|
|
40
|
+
liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
|
|
40
41
|
lowerSeatPrice: () => lowerSeatPrice,
|
|
41
42
|
namedSeatIncluded: () => namedSeatIncluded,
|
|
42
43
|
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
@@ -436,12 +437,12 @@ function canonical(value) {
|
|
|
436
437
|
return value;
|
|
437
438
|
}
|
|
438
439
|
async function stripeEventDigest(rawBody) {
|
|
439
|
-
const
|
|
440
|
-
if (!
|
|
440
|
+
const event2 = JSON.parse(rawBody);
|
|
441
|
+
if (!event2 || typeof event2 !== "object" || Array.isArray(event2)) throw new TypeError("invalid Stripe event");
|
|
441
442
|
const keys3 = ["id", "type", "created", "livemode", "account", "context", "api_version", "data"];
|
|
442
443
|
return stripePayloadDigest(JSON.stringify(canonical({
|
|
443
444
|
digestVersion: 1,
|
|
444
|
-
...Object.fromEntries(keys3.filter((key3) => Object.hasOwn(
|
|
445
|
+
...Object.fromEntries(keys3.filter((key3) => Object.hasOwn(event2, key3)).map((key3) => [key3, event2[key3]]))
|
|
445
446
|
})));
|
|
446
447
|
}
|
|
447
448
|
|
|
@@ -708,13 +709,13 @@ function firstPrice(object2) {
|
|
|
708
709
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
709
710
|
}
|
|
710
711
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
711
|
-
const
|
|
712
|
-
const eventId = providerId(
|
|
713
|
-
const eventType = text(
|
|
714
|
-
const object2 = raw(raw(
|
|
712
|
+
const event2 = raw(input);
|
|
713
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
714
|
+
const eventType = text(event2.type) ?? "";
|
|
715
|
+
const object2 = raw(raw(event2.data).object);
|
|
715
716
|
const objectId = providerId(object2.id) ?? "";
|
|
716
717
|
const candidates = metadataCandidates(object2);
|
|
717
|
-
const
|
|
718
|
+
const identity2 = {};
|
|
718
719
|
const identityKeys2 = {
|
|
719
720
|
appId: "odlaAppId",
|
|
720
721
|
chapterId: "chapterId",
|
|
@@ -724,7 +725,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
724
725
|
};
|
|
725
726
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
726
727
|
const value = metadataValue(candidates, source);
|
|
727
|
-
if (value)
|
|
728
|
+
if (value) identity2[target] = value;
|
|
728
729
|
}
|
|
729
730
|
let objectKind = "ignored";
|
|
730
731
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -746,11 +747,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
746
747
|
eventId,
|
|
747
748
|
digest,
|
|
748
749
|
eventType,
|
|
749
|
-
eventCreated: integer(
|
|
750
|
-
livemode:
|
|
750
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
751
|
+
livemode: event2.livemode === true,
|
|
751
752
|
objectKind,
|
|
752
753
|
objectId,
|
|
753
|
-
identity,
|
|
754
|
+
identity: identity2,
|
|
754
755
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
755
756
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
756
757
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -1227,35 +1228,35 @@ function findGiftRef(obj) {
|
|
|
1227
1228
|
...chapterId ? { chapterId } : {}
|
|
1228
1229
|
};
|
|
1229
1230
|
}
|
|
1230
|
-
function normalizeWebhookEvent(
|
|
1231
|
-
const obj =
|
|
1232
|
-
if (
|
|
1231
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
1232
|
+
const obj = event2.data?.object ?? {};
|
|
1233
|
+
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 };
|
|
1233
1234
|
const gift = findGiftRef(obj);
|
|
1234
1235
|
if (gift.giftIntentId) {
|
|
1235
|
-
const { giftIntentId, providerObjectId, ...
|
|
1236
|
-
if (
|
|
1237
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
1236
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
1237
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
1238
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
1238
1239
|
}
|
|
1239
|
-
if (
|
|
1240
|
-
if (
|
|
1241
|
-
return { kind: "ignored", type:
|
|
1240
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
1241
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
1242
|
+
return { kind: "ignored", type: event2.type };
|
|
1242
1243
|
}
|
|
1243
1244
|
const ref = findApplicationRef(obj);
|
|
1244
1245
|
const seatId = obj.metadata?.seatId;
|
|
1245
1246
|
if (typeof seatId === "string" && seatId) {
|
|
1246
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
1247
|
-
if (
|
|
1248
|
-
if (
|
|
1249
|
-
return { kind: "ignored", type:
|
|
1247
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
1248
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
1249
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
1250
|
+
return { kind: "ignored", type: event2.type };
|
|
1250
1251
|
}
|
|
1251
|
-
if (
|
|
1252
|
-
switch (
|
|
1252
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
1253
|
+
switch (event2.type) {
|
|
1253
1254
|
case "invoice.payment_failed":
|
|
1254
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
1255
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
1255
1256
|
case "invoice.paid":
|
|
1256
1257
|
case "invoice.payment_succeeded": {
|
|
1257
1258
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
1258
|
-
return { kind: "ignored", type:
|
|
1259
|
+
return { kind: "ignored", type: event2.type };
|
|
1259
1260
|
}
|
|
1260
1261
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
1261
1262
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -1269,11 +1270,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
1269
1270
|
case "charge.refunded":
|
|
1270
1271
|
return { kind: "refunded", ...ref };
|
|
1271
1272
|
case "customer.subscription.updated":
|
|
1272
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
1273
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
1273
1274
|
case "customer.subscription.deleted":
|
|
1274
1275
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
1275
1276
|
default:
|
|
1276
|
-
return { kind: "ignored", type:
|
|
1277
|
+
return { kind: "ignored", type: event2.type };
|
|
1277
1278
|
}
|
|
1278
1279
|
}
|
|
1279
1280
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -1632,6 +1633,41 @@ async function backfillCrm(deps) {
|
|
|
1632
1633
|
return { synced, errors };
|
|
1633
1634
|
}
|
|
1634
1635
|
|
|
1636
|
+
// src/email-retirement.ts
|
|
1637
|
+
function isRetiredEmailCopy(subject, text8) {
|
|
1638
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1639
|
+
const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1640
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/email.ts
|
|
1644
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
1645
|
+
|
|
1646
|
+
// src/email-events.ts
|
|
1647
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
1648
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
1649
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
1650
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
1651
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
1652
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
1653
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
1654
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
1655
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
1656
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
1657
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
1658
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
1659
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
1660
|
+
};
|
|
1661
|
+
function chapterEmailEvents(chapter) {
|
|
1662
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
1663
|
+
label: key3,
|
|
1664
|
+
trigger: "Administrator sends a message",
|
|
1665
|
+
audience: "Selected contact",
|
|
1666
|
+
variables: spec.vars
|
|
1667
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
1668
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1635
1671
|
// src/email.ts
|
|
1636
1672
|
function render(template, vars) {
|
|
1637
1673
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -1655,21 +1691,233 @@ function isAlreadySent(priorRows) {
|
|
|
1655
1691
|
function planDelivery(input) {
|
|
1656
1692
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
1657
1693
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
1658
|
-
if (tpl.enabled
|
|
1694
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
1695
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
1696
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
1659
1697
|
const vars = groupVars(input.group, input.vars);
|
|
1698
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
1699
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1700
|
+
const issues = (0, import_crm5.validateEmailCopy)(tpl, {
|
|
1701
|
+
allowedVariables: event2?.variables,
|
|
1702
|
+
optionalVariables: event2?.optionalVariables,
|
|
1703
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
1704
|
+
values: vars,
|
|
1705
|
+
tierId: vars.tierId
|
|
1706
|
+
});
|
|
1707
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
1660
1708
|
const isProd = input.envName === "prod";
|
|
1661
1709
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
1662
1710
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1663
1711
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1664
|
-
const
|
|
1712
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
1713
|
+
const renderedText = render(tpl.text, vars);
|
|
1714
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1715
|
+
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1716
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1665
1717
|
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1666
1718
|
|
|
1667
|
-
` +
|
|
1719
|
+
` + renderedText : renderedText;
|
|
1668
1720
|
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
1669
1721
|
}
|
|
1670
1722
|
|
|
1671
1723
|
// src/notify.ts
|
|
1672
|
-
var
|
|
1724
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
1725
|
+
|
|
1726
|
+
// src/email-event-values.ts
|
|
1727
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
1728
|
+
|
|
1729
|
+
// src/tiers.ts
|
|
1730
|
+
function tierIsFree(tier) {
|
|
1731
|
+
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
1732
|
+
}
|
|
1733
|
+
function tierPayable(tier, group, hasSecretKey) {
|
|
1734
|
+
if (tierIsFree(tier)) return true;
|
|
1735
|
+
return Boolean(group.stripePublishableKey && tier.stripePriceId && hasSecretKey);
|
|
1736
|
+
}
|
|
1737
|
+
function byOrder(a, b) {
|
|
1738
|
+
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1739
|
+
}
|
|
1740
|
+
function resolveTiers(group, rows5 = []) {
|
|
1741
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
1742
|
+
if (rows5.length) return [...stored].sort(byOrder);
|
|
1743
|
+
return [{
|
|
1744
|
+
id: "standard",
|
|
1745
|
+
groupId: String(group.id ?? ""),
|
|
1746
|
+
name: "Membership",
|
|
1747
|
+
priceCents: group.standardPriceCents ?? 0,
|
|
1748
|
+
currency: group.currency ?? "usd",
|
|
1749
|
+
interval: group.interval ?? "year",
|
|
1750
|
+
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
1751
|
+
sortOrder: 0,
|
|
1752
|
+
active: true
|
|
1753
|
+
}];
|
|
1754
|
+
}
|
|
1755
|
+
function offerableTiers(tiers, group, hasSecretKey) {
|
|
1756
|
+
return tiers.filter((tier) => tier.active && tierPayable(tier, group, hasSecretKey));
|
|
1757
|
+
}
|
|
1758
|
+
function findTier(tiers, tierId) {
|
|
1759
|
+
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1760
|
+
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1761
|
+
}
|
|
1762
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1763
|
+
return tiers.map((tier) => ({
|
|
1764
|
+
id: tier.id,
|
|
1765
|
+
name: tier.name,
|
|
1766
|
+
priceCents: tier.priceCents,
|
|
1767
|
+
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
1768
|
+
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1769
|
+
blurb: tier.blurb ?? "",
|
|
1770
|
+
free: tierIsFree(tier),
|
|
1771
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1772
|
+
}));
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/tiers-store.ts
|
|
1776
|
+
function rowToTier(row2, groupId) {
|
|
1777
|
+
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") {
|
|
1778
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
id: String(row2.id ?? ""),
|
|
1782
|
+
groupId: String(row2.groupId ?? groupId),
|
|
1783
|
+
name: String(row2.name ?? ""),
|
|
1784
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
1785
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
1786
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
1787
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
1788
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
1789
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
1790
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
1791
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
1792
|
+
active: row2.active !== false
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async function loadTiers(db, group) {
|
|
1796
|
+
const groupId = String(group.id ?? "");
|
|
1797
|
+
let rows5;
|
|
1798
|
+
try {
|
|
1799
|
+
rows5 = (await db.query({
|
|
1800
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
1801
|
+
})).tiers;
|
|
1802
|
+
} catch {
|
|
1803
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
1804
|
+
}
|
|
1805
|
+
if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
1806
|
+
const list = rows5.map((row2) => rowToTier(row2, groupId));
|
|
1807
|
+
return resolveTiers(group, list);
|
|
1808
|
+
}
|
|
1809
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
1810
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/notification-variables.ts
|
|
1814
|
+
function lifecycleNotifyDeps(db, env) {
|
|
1815
|
+
return {
|
|
1816
|
+
db,
|
|
1817
|
+
envName: env.ODLA_ENV,
|
|
1818
|
+
sender: env.SEND_EMAIL,
|
|
1819
|
+
from: env.EMAIL_FROM,
|
|
1820
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
1821
|
+
now: () => Date.now(),
|
|
1822
|
+
newId: () => crypto.randomUUID()
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
async function frozenCheckoutVariables(db, app) {
|
|
1826
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1827
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1828
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1829
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1830
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
1831
|
+
let formatted = "";
|
|
1832
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1833
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
paymentAmount: formatted,
|
|
1837
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1838
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
async function catalogTierName(db, group, app) {
|
|
1842
|
+
try {
|
|
1843
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
1844
|
+
} catch {
|
|
1845
|
+
return "";
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async function checkoutVariables(db, group, app) {
|
|
1849
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
1850
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
1851
|
+
}
|
|
1852
|
+
function formatInstant(at2, timeZone) {
|
|
1853
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
1854
|
+
weekday: "long",
|
|
1855
|
+
month: "long",
|
|
1856
|
+
day: "numeric",
|
|
1857
|
+
year: "numeric",
|
|
1858
|
+
hour: "numeric",
|
|
1859
|
+
minute: "2-digit",
|
|
1860
|
+
timeZoneName: "short",
|
|
1861
|
+
timeZone
|
|
1862
|
+
}).format(new Date(at2));
|
|
1863
|
+
}
|
|
1864
|
+
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1865
|
+
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1866
|
+
function applicantVariables(app) {
|
|
1867
|
+
const text8 = (v) => typeof v === "string" ? v : "";
|
|
1868
|
+
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
1869
|
+
}
|
|
1870
|
+
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1871
|
+
const startAt = formatInstant(now + 864e5, timeZone);
|
|
1872
|
+
return {
|
|
1873
|
+
firstName: "Sample",
|
|
1874
|
+
lastName: "Person",
|
|
1875
|
+
email: "sample@example.com",
|
|
1876
|
+
phone: "(555) 010-0100",
|
|
1877
|
+
state: "CA",
|
|
1878
|
+
tier: "Membership",
|
|
1879
|
+
tierName: "Membership",
|
|
1880
|
+
paymentAmount: "$100.00",
|
|
1881
|
+
paymentAmountSummary: " of $100.00",
|
|
1882
|
+
paymentNextStep: "Next, schedule your introduction call.",
|
|
1883
|
+
submitNextStep: "Next, schedule your introduction call.",
|
|
1884
|
+
startAt,
|
|
1885
|
+
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
1886
|
+
timezone: timeZone,
|
|
1887
|
+
meetUrl: "https://meet.google.com/sample",
|
|
1888
|
+
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
1889
|
+
meetingTime: startAt,
|
|
1890
|
+
meetingLink: "https://meet.google.com/sample",
|
|
1891
|
+
adminUrl: adminUrlFor(origin),
|
|
1892
|
+
membersUrl: membersUrlFor(origin),
|
|
1893
|
+
purchaserName: "Sample Purchaser",
|
|
1894
|
+
recipientName: "Sample Recipient",
|
|
1895
|
+
chapterName: "Sample Chapter",
|
|
1896
|
+
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
1897
|
+
joinUrl: `${origin}/join?seat=sample`
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/email-event-values.ts
|
|
1902
|
+
async function emailTierVariables(db, input) {
|
|
1903
|
+
try {
|
|
1904
|
+
const [apps, groups] = await Promise.all([
|
|
1905
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
1906
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
1907
|
+
]);
|
|
1908
|
+
const app = apps.applications?.[0], group = groups.groups?.[0];
|
|
1909
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
1910
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
1911
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
1912
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
1913
|
+
const tierName2 = (0, import_crm6.isEmailTierName)(name, tierId) ? name.trim() : "";
|
|
1914
|
+
return { tierName: tierName2, tier: tierName2, tierId };
|
|
1915
|
+
} catch {
|
|
1916
|
+
return { tierName: "", tier: "" };
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/notify.ts
|
|
1673
1921
|
function fromAddress(configured, name) {
|
|
1674
1922
|
const display = name?.trim();
|
|
1675
1923
|
if (!display) return configured;
|
|
@@ -1686,14 +1934,17 @@ async function sendTemplated(deps, input) {
|
|
|
1686
1934
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
1687
1935
|
const override = deps.debugEmail?.trim();
|
|
1688
1936
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
1937
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1938
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
1689
1939
|
const decision = planDelivery({
|
|
1690
1940
|
envName: deps.envName,
|
|
1691
1941
|
group,
|
|
1692
1942
|
template: input.template,
|
|
1693
1943
|
to: input.to,
|
|
1694
|
-
vars
|
|
1944
|
+
vars,
|
|
1695
1945
|
cloudflareReady,
|
|
1696
|
-
force: input.force
|
|
1946
|
+
force: input.force,
|
|
1947
|
+
event: event2
|
|
1697
1948
|
});
|
|
1698
1949
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
1699
1950
|
const previous = prior[0];
|
|
@@ -1718,6 +1969,8 @@ async function sendTemplated(deps, input) {
|
|
|
1718
1969
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
1719
1970
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
1720
1971
|
};
|
|
1972
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1973
|
+
if ((0, import_crm7.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1721
1974
|
try {
|
|
1722
1975
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
|
|
1723
1976
|
mutationId: `email-claim:${id6}:${attempt}`,
|
|
@@ -1764,17 +2017,17 @@ async function sendTemplated(deps, input) {
|
|
|
1764
2017
|
return { sent: true };
|
|
1765
2018
|
}
|
|
1766
2019
|
function emailGroupFrom(row2) {
|
|
1767
|
-
const
|
|
2020
|
+
const str4 = (v) => typeof v === "string" ? v : void 0;
|
|
1768
2021
|
const templates = row2.emailTemplates && typeof row2.emailTemplates === "object" ? row2.emailTemplates : {};
|
|
1769
2022
|
return {
|
|
1770
2023
|
id: String(row2.id),
|
|
1771
2024
|
name: String(row2.name ?? ""),
|
|
1772
|
-
replyTo:
|
|
1773
|
-
debugEmail:
|
|
1774
|
-
fromName:
|
|
1775
|
-
refundPolicyText:
|
|
1776
|
-
commitmentText:
|
|
1777
|
-
normsText:
|
|
2025
|
+
replyTo: str4(row2.replyTo) ?? "",
|
|
2026
|
+
debugEmail: str4(row2.debugEmail),
|
|
2027
|
+
fromName: str4(row2.fromName),
|
|
2028
|
+
refundPolicyText: str4(row2.refundPolicyText),
|
|
2029
|
+
commitmentText: str4(row2.commitmentText),
|
|
2030
|
+
normsText: str4(row2.normsText),
|
|
1778
2031
|
emailTemplates: templates
|
|
1779
2032
|
};
|
|
1780
2033
|
}
|
|
@@ -1887,15 +2140,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
1887
2140
|
// src/membership-offer.ts
|
|
1888
2141
|
function createMembershipCommercialOffer(input) {
|
|
1889
2142
|
const offering = membershipOfferingVersion(input.offering);
|
|
1890
|
-
const
|
|
2143
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
1891
2144
|
const now = input.now;
|
|
1892
2145
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
1893
2146
|
throw new TypeError("invalid offer clock or founding count");
|
|
1894
2147
|
}
|
|
1895
2148
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
1896
|
-
if (
|
|
1897
|
-
const founding = Boolean(
|
|
1898
|
-
const earned = founding ?
|
|
2149
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
2150
|
+
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);
|
|
2151
|
+
const earned = founding ? policy2 : null;
|
|
1899
2152
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
1900
2153
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
1901
2154
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -1987,15 +2240,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1987
2240
|
}
|
|
1988
2241
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
1989
2242
|
const state = await authoritativeState(db, input);
|
|
1990
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
1991
|
-
const approvedFoundingCount =
|
|
2243
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
2244
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
1992
2245
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1993
2246
|
const contract = createMembershipCommercialOffer({
|
|
1994
2247
|
offering,
|
|
1995
|
-
discountPolicy:
|
|
2248
|
+
discountPolicy: policy2,
|
|
1996
2249
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1997
2250
|
now: input.now,
|
|
1998
|
-
couponId: await membershipDiscountCouponId(input,
|
|
2251
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
1999
2252
|
});
|
|
2000
2253
|
const discount = contract.billingTerms.discount;
|
|
2001
2254
|
return {
|
|
@@ -2024,12 +2277,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
2024
2277
|
});
|
|
2025
2278
|
if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
2026
2279
|
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));
|
|
2027
|
-
const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
2280
|
+
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);
|
|
2028
2281
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
2029
2282
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
2030
|
-
const
|
|
2031
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
2032
|
-
return { offering, policy };
|
|
2283
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
2284
|
+
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");
|
|
2285
|
+
return { offering, policy: policy2 };
|
|
2033
2286
|
}
|
|
2034
2287
|
async function issueMembershipCommercialOffer(db, input) {
|
|
2035
2288
|
validate(input);
|
|
@@ -2037,15 +2290,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2037
2290
|
const state = await authoritativeState(db, input);
|
|
2038
2291
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
2039
2292
|
if (existing) return restore(existing, input);
|
|
2040
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
2041
|
-
const approvedFoundingCount =
|
|
2293
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
2294
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
2042
2295
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
2043
2296
|
const contract = createMembershipCommercialOffer({
|
|
2044
2297
|
offering,
|
|
2045
|
-
discountPolicy:
|
|
2298
|
+
discountPolicy: policy2,
|
|
2046
2299
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
2047
2300
|
now: input.now,
|
|
2048
|
-
couponId: await membershipDiscountCouponId(input,
|
|
2301
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
2049
2302
|
});
|
|
2050
2303
|
const value = {
|
|
2051
2304
|
id: input.offerId,
|
|
@@ -2312,90 +2565,6 @@ async function verifyMembershipPaymentReceipt(value, target) {
|
|
|
2312
2565
|
};
|
|
2313
2566
|
}
|
|
2314
2567
|
|
|
2315
|
-
// src/tiers.ts
|
|
2316
|
-
function tierIsFree(tier) {
|
|
2317
|
-
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
2318
|
-
}
|
|
2319
|
-
function tierPayable(tier, group, hasSecretKey) {
|
|
2320
|
-
if (tierIsFree(tier)) return true;
|
|
2321
|
-
return Boolean(group.stripePublishableKey && tier.stripePriceId && hasSecretKey);
|
|
2322
|
-
}
|
|
2323
|
-
function byOrder(a, b) {
|
|
2324
|
-
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
2325
|
-
}
|
|
2326
|
-
function resolveTiers(group, rows5 = []) {
|
|
2327
|
-
const stored = rows5.filter((tier) => tier.active);
|
|
2328
|
-
if (rows5.length) return [...stored].sort(byOrder);
|
|
2329
|
-
return [{
|
|
2330
|
-
id: "standard",
|
|
2331
|
-
groupId: String(group.id ?? ""),
|
|
2332
|
-
name: "Membership",
|
|
2333
|
-
priceCents: group.standardPriceCents ?? 0,
|
|
2334
|
-
currency: group.currency ?? "usd",
|
|
2335
|
-
interval: group.interval ?? "year",
|
|
2336
|
-
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
2337
|
-
sortOrder: 0,
|
|
2338
|
-
active: true
|
|
2339
|
-
}];
|
|
2340
|
-
}
|
|
2341
|
-
function offerableTiers(tiers, group, hasSecretKey) {
|
|
2342
|
-
return tiers.filter((tier) => tier.active && tierPayable(tier, group, hasSecretKey));
|
|
2343
|
-
}
|
|
2344
|
-
function findTier(tiers, tierId) {
|
|
2345
|
-
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
2346
|
-
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
2347
|
-
}
|
|
2348
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2349
|
-
return tiers.map((tier) => ({
|
|
2350
|
-
id: tier.id,
|
|
2351
|
-
name: tier.name,
|
|
2352
|
-
priceCents: tier.priceCents,
|
|
2353
|
-
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2354
|
-
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2355
|
-
blurb: tier.blurb ?? "",
|
|
2356
|
-
free: tierIsFree(tier),
|
|
2357
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2358
|
-
}));
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
// src/tiers-store.ts
|
|
2362
|
-
function rowToTier(row2, groupId) {
|
|
2363
|
-
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") {
|
|
2364
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
2365
|
-
}
|
|
2366
|
-
return {
|
|
2367
|
-
id: String(row2.id ?? ""),
|
|
2368
|
-
groupId: String(row2.groupId ?? groupId),
|
|
2369
|
-
name: String(row2.name ?? ""),
|
|
2370
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
2371
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
2372
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
2373
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
2374
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
2375
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
2376
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
2377
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
2378
|
-
active: row2.active !== false
|
|
2379
|
-
};
|
|
2380
|
-
}
|
|
2381
|
-
async function loadTiers(db, group) {
|
|
2382
|
-
const groupId = String(group.id ?? "");
|
|
2383
|
-
let rows5;
|
|
2384
|
-
try {
|
|
2385
|
-
rows5 = (await db.query({
|
|
2386
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2387
|
-
})).tiers;
|
|
2388
|
-
} catch {
|
|
2389
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
2390
|
-
}
|
|
2391
|
-
if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
2392
|
-
const list = rows5.map((row2) => rowToTier(row2, groupId));
|
|
2393
|
-
return resolveTiers(group, list);
|
|
2394
|
-
}
|
|
2395
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
2396
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
2568
|
// src/membership-restart-seat.ts
|
|
2400
2569
|
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
2401
2570
|
const ops = [], guards = [];
|
|
@@ -2902,89 +3071,6 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
2902
3071
|
}
|
|
2903
3072
|
}
|
|
2904
3073
|
|
|
2905
|
-
// src/notification-variables.ts
|
|
2906
|
-
function lifecycleNotifyDeps(db, env) {
|
|
2907
|
-
return {
|
|
2908
|
-
db,
|
|
2909
|
-
envName: env.ODLA_ENV,
|
|
2910
|
-
sender: env.SEND_EMAIL,
|
|
2911
|
-
from: env.EMAIL_FROM,
|
|
2912
|
-
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
2913
|
-
now: () => Date.now(),
|
|
2914
|
-
newId: () => crypto.randomUUID()
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
|
-
async function frozenCheckoutVariables(db, app) {
|
|
2918
|
-
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
2919
|
-
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
2920
|
-
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
2921
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
2922
|
-
const currency = offering?.currency ?? intent?.currency;
|
|
2923
|
-
let formatted = "";
|
|
2924
|
-
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
2925
|
-
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
2926
|
-
}
|
|
2927
|
-
return {
|
|
2928
|
-
paymentAmount: formatted,
|
|
2929
|
-
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
2930
|
-
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
2931
|
-
};
|
|
2932
|
-
}
|
|
2933
|
-
async function catalogTierName(db, group, app) {
|
|
2934
|
-
try {
|
|
2935
|
-
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
2936
|
-
} catch {
|
|
2937
|
-
return "";
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
async function checkoutVariables(db, group, app) {
|
|
2941
|
-
const frozen = await frozenCheckoutVariables(db, app);
|
|
2942
|
-
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2943
|
-
}
|
|
2944
|
-
function formatInstant(at2, timeZone) {
|
|
2945
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
2946
|
-
weekday: "long",
|
|
2947
|
-
month: "long",
|
|
2948
|
-
day: "numeric",
|
|
2949
|
-
year: "numeric",
|
|
2950
|
-
hour: "numeric",
|
|
2951
|
-
minute: "2-digit",
|
|
2952
|
-
timeZoneName: "short",
|
|
2953
|
-
timeZone
|
|
2954
|
-
}).format(new Date(at2));
|
|
2955
|
-
}
|
|
2956
|
-
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2957
|
-
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
2958
|
-
function applicantVariables(app) {
|
|
2959
|
-
const text8 = (v) => typeof v === "string" ? v : "";
|
|
2960
|
-
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
2961
|
-
}
|
|
2962
|
-
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
2963
|
-
const startAt = formatInstant(now + 864e5, timeZone);
|
|
2964
|
-
return {
|
|
2965
|
-
firstName: "Sample",
|
|
2966
|
-
lastName: "Person",
|
|
2967
|
-
email: "sample@example.com",
|
|
2968
|
-
phone: "(555) 010-0100",
|
|
2969
|
-
state: "CA",
|
|
2970
|
-
tier: "Membership",
|
|
2971
|
-
tierName: "Membership",
|
|
2972
|
-
paymentAmount: "$100.00",
|
|
2973
|
-
paymentAmountSummary: " of $100.00",
|
|
2974
|
-
paymentNextStep: "Next, schedule your introduction call.",
|
|
2975
|
-
submitNextStep: "Next, schedule your introduction call.",
|
|
2976
|
-
startAt,
|
|
2977
|
-
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
2978
|
-
timezone: timeZone,
|
|
2979
|
-
meetUrl: "https://meet.google.com/sample",
|
|
2980
|
-
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
2981
|
-
meetingTime: startAt,
|
|
2982
|
-
meetingLink: "https://meet.google.com/sample",
|
|
2983
|
-
adminUrl: adminUrlFor(origin),
|
|
2984
|
-
membersUrl: membersUrlFor(origin)
|
|
2985
|
-
};
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
3074
|
// src/payment-notifications.ts
|
|
2989
3075
|
var groupFor = async (db, app) => {
|
|
2990
3076
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3270,24 +3356,24 @@ async function revokeGiftClaim(db, claim, reason, now) {
|
|
|
3270
3356
|
}
|
|
3271
3357
|
|
|
3272
3358
|
// src/payments-route-support.ts
|
|
3273
|
-
async function findPaymentApplication(db,
|
|
3274
|
-
if ("applicationId" in
|
|
3275
|
-
const rows5 = await db.query({ applications: { $: { where: { id:
|
|
3359
|
+
async function findPaymentApplication(db, event2) {
|
|
3360
|
+
if ("applicationId" in event2 && event2.applicationId) {
|
|
3361
|
+
const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
|
|
3276
3362
|
return rows5.applications?.[0];
|
|
3277
3363
|
}
|
|
3278
|
-
if (
|
|
3279
|
-
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId:
|
|
3364
|
+
if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
|
|
3365
|
+
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
|
|
3280
3366
|
return Array.isArray(applications) && applications.length === 1 ? applications[0] : void 0;
|
|
3281
3367
|
}
|
|
3282
3368
|
return void 0;
|
|
3283
3369
|
}
|
|
3284
|
-
function paymentEventPatch(
|
|
3285
|
-
if ((status2 === "refunded" || status2 === "declined") && (
|
|
3286
|
-
switch (
|
|
3370
|
+
function paymentEventPatch(event2, status2) {
|
|
3371
|
+
if ((status2 === "refunded" || status2 === "declined") && (event2.kind === "first_payment" || event2.kind === "renewal")) return {};
|
|
3372
|
+
switch (event2.kind) {
|
|
3287
3373
|
case "first_payment":
|
|
3288
|
-
return firstPaymentPatch(status2,
|
|
3374
|
+
return firstPaymentPatch(status2, event2.renewalAt);
|
|
3289
3375
|
case "renewal":
|
|
3290
|
-
return
|
|
3376
|
+
return event2.renewalAt !== void 0 ? renewalPatch(event2.renewalAt) : {};
|
|
3291
3377
|
case "refunded":
|
|
3292
3378
|
return refundedPatch();
|
|
3293
3379
|
case "canceled":
|
|
@@ -3308,10 +3394,10 @@ function paymentLineItems(group, tier) {
|
|
|
3308
3394
|
}
|
|
3309
3395
|
|
|
3310
3396
|
// src/member-system-contract.ts
|
|
3311
|
-
function classifyMemberStripeEvent(
|
|
3397
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3312
3398
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
3313
|
-
const supplied = keys3.filter((key3) =>
|
|
3314
|
-
if (supplied.some((key3) =>
|
|
3399
|
+
const supplied = keys3.filter((key3) => identity2[key3] !== void 0);
|
|
3400
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
3315
3401
|
if (supplied.length === keys3.length) return "apply";
|
|
3316
3402
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
3317
3403
|
}
|
|
@@ -3321,12 +3407,12 @@ var firstRow = async (db, ns, q) => {
|
|
|
3321
3407
|
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
3322
3408
|
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3323
3409
|
};
|
|
3324
|
-
var stripeEventIdentity = (
|
|
3325
|
-
...
|
|
3326
|
-
...
|
|
3327
|
-
...
|
|
3328
|
-
...
|
|
3329
|
-
...
|
|
3410
|
+
var stripeEventIdentity = (event2) => ({
|
|
3411
|
+
...event2.appId ? { appId: event2.appId } : {},
|
|
3412
|
+
...event2.environment ? { environment: event2.environment } : {},
|
|
3413
|
+
...event2.dataEnvironment ? { dataEnvironment: event2.dataEnvironment } : {},
|
|
3414
|
+
...event2.runtime ? { runtime: event2.runtime } : {},
|
|
3415
|
+
...event2.chapterId ? { chapterId: event2.chapterId } : {}
|
|
3330
3416
|
});
|
|
3331
3417
|
var stripeEventTarget = (env, ctx) => ({
|
|
3332
3418
|
appId: env.ODLA_APP_ID,
|
|
@@ -3336,12 +3422,12 @@ var stripeEventTarget = (env, ctx) => ({
|
|
|
3336
3422
|
chapterId: ctx.chapter.id
|
|
3337
3423
|
});
|
|
3338
3424
|
var receiptFor = (db, eventId) => firstRow(db, "stripeEventReceipts", { where: { eventId }, limit: 1 });
|
|
3339
|
-
function reportStripePartition(level, eventId,
|
|
3425
|
+
function reportStripePartition(level, eventId, event2, disposition) {
|
|
3340
3426
|
console[level]("chapter stripe partition", JSON.stringify({
|
|
3341
3427
|
eventId,
|
|
3342
|
-
kind:
|
|
3428
|
+
kind: event2.kind,
|
|
3343
3429
|
disposition,
|
|
3344
|
-
...stripeEventIdentity(
|
|
3430
|
+
...stripeEventIdentity(event2)
|
|
3345
3431
|
}));
|
|
3346
3432
|
}
|
|
3347
3433
|
var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
|
|
@@ -3360,8 +3446,8 @@ async function alertStripeQuarantine(db, ctx, eventId, disposition) {
|
|
|
3360
3446
|
artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
|
|
3361
3447
|
});
|
|
3362
3448
|
}
|
|
3363
|
-
async function reportStripeCollision(db, ctx, eventId,
|
|
3364
|
-
reportStripePartition("error", eventId,
|
|
3449
|
+
async function reportStripeCollision(db, ctx, eventId, event2) {
|
|
3450
|
+
reportStripePartition("error", eventId, event2, "quarantine_collision");
|
|
3365
3451
|
await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
|
|
3366
3452
|
}
|
|
3367
3453
|
async function storeStripeReceipt(db, input) {
|
|
@@ -3396,18 +3482,18 @@ async function storeStripeReceipt(db, input) {
|
|
|
3396
3482
|
const raced = await receiptFor(db, input.eventId);
|
|
3397
3483
|
return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3398
3484
|
}
|
|
3399
|
-
async function quarantineStripeEvent(db, eventId, digest,
|
|
3485
|
+
async function quarantineStripeEvent(db, eventId, digest, event2, ctx) {
|
|
3400
3486
|
const disposition = "quarantine_unmatched";
|
|
3401
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
3487
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event: event2, disposition, matched: false });
|
|
3402
3488
|
if (stored === "collision") {
|
|
3403
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3489
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3404
3490
|
return json({ ok: true, matched: false, collision: true });
|
|
3405
3491
|
}
|
|
3406
|
-
reportStripePartition("warn", eventId,
|
|
3492
|
+
reportStripePartition("warn", eventId, event2, disposition);
|
|
3407
3493
|
if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
|
|
3408
3494
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
3409
3495
|
}
|
|
3410
|
-
async function replayedStripeReceipt(db, eventId, digest,
|
|
3496
|
+
async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx) {
|
|
3411
3497
|
const existing = await receiptFor(db, eventId);
|
|
3412
3498
|
if (!existing) return null;
|
|
3413
3499
|
if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
|
|
@@ -3417,7 +3503,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, c
|
|
|
3417
3503
|
collisionDigest: digest,
|
|
3418
3504
|
retryReady: false
|
|
3419
3505
|
} }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
|
|
3420
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3506
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3421
3507
|
return json({ ok: true, matched: false, collision: true });
|
|
3422
3508
|
}
|
|
3423
3509
|
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
@@ -3431,42 +3517,42 @@ async function firstRow2(db, ns, q) {
|
|
|
3431
3517
|
}
|
|
3432
3518
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3433
3519
|
const db = ctx.makeDb(env);
|
|
3434
|
-
const { rawBody, eventId, event } = input;
|
|
3435
|
-
if (
|
|
3520
|
+
const { rawBody, eventId, event: event2 } = input;
|
|
3521
|
+
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3436
3522
|
const digest = await stripeEventDigest(rawBody);
|
|
3437
3523
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
3438
|
-
const
|
|
3524
|
+
const identity2 = stripeEventIdentity(event2);
|
|
3439
3525
|
const target = stripeEventTarget(env, ctx);
|
|
3440
|
-
const preliminary = classifyMemberStripeEvent(
|
|
3526
|
+
const preliminary = classifyMemberStripeEvent(identity2, target, false);
|
|
3441
3527
|
if (preliminary === "ignore_sibling") {
|
|
3442
|
-
reportStripePartition("warn", eventId,
|
|
3528
|
+
reportStripePartition("warn", eventId, event2, preliminary);
|
|
3443
3529
|
return json({ ok: true, matched: false, disposition: preliminary });
|
|
3444
3530
|
}
|
|
3445
|
-
if (
|
|
3446
|
-
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id:
|
|
3531
|
+
if (event2.kind === "gift_paid" || event2.kind === "gift_revoked") {
|
|
3532
|
+
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event2.giftIntentId }, limit: 1 });
|
|
3447
3533
|
if (intent && typeof intent.runtime === "string" && intent.runtime !== target.runtime) {
|
|
3448
|
-
reportStripePartition("warn", eventId,
|
|
3534
|
+
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
3449
3535
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3450
3536
|
}
|
|
3451
|
-
const replay2 = await replayedStripeReceipt(db, eventId, digest,
|
|
3537
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx);
|
|
3452
3538
|
if (replay2) return replay2;
|
|
3453
3539
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
3454
|
-
const disposition2 = classifyMemberStripeEvent(
|
|
3540
|
+
const disposition2 = classifyMemberStripeEvent(identity2, target, localIntent);
|
|
3455
3541
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
3456
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3542
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3457
3543
|
}
|
|
3458
|
-
if (
|
|
3459
|
-
if (intent.providerObjectId !==
|
|
3460
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3544
|
+
if (event2.kind === "gift_paid") {
|
|
3545
|
+
if (intent.providerObjectId !== event2.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
3546
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3461
3547
|
}
|
|
3462
3548
|
try {
|
|
3463
3549
|
const issued = await issueGiftClaim(db, intent, {
|
|
3464
3550
|
eventId,
|
|
3465
|
-
objectId:
|
|
3551
|
+
objectId: event2.providerObjectId
|
|
3466
3552
|
}, Date.now());
|
|
3467
|
-
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
|
|
3553
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition: disposition2, matched: true });
|
|
3468
3554
|
if (stored3 === "collision") {
|
|
3469
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3555
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3470
3556
|
return json({ ok: true, matched: false, collision: true });
|
|
3471
3557
|
}
|
|
3472
3558
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
@@ -3474,57 +3560,57 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3474
3560
|
return json({ error: "gift issuance failed" }, 503);
|
|
3475
3561
|
}
|
|
3476
3562
|
}
|
|
3477
|
-
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id:
|
|
3478
|
-
status:
|
|
3563
|
+
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: event2.giftIntentId, attrs: {
|
|
3564
|
+
status: event2.reason,
|
|
3479
3565
|
updatedAt: Date.now()
|
|
3480
3566
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
3481
|
-
const claim = await firstRow2(db, "giftClaims", { where: { intentId:
|
|
3482
|
-
if (claim) await revokeGiftClaim(db, claim,
|
|
3483
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
3567
|
+
const claim = await firstRow2(db, "giftClaims", { where: { intentId: event2.giftIntentId }, limit: 1 });
|
|
3568
|
+
if (claim) await revokeGiftClaim(db, claim, event2.reason, Date.now());
|
|
3569
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition: disposition2, matched: Boolean(claim) });
|
|
3484
3570
|
if (stored2 === "collision") {
|
|
3485
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3571
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3486
3572
|
return json({ ok: true, matched: false, collision: true });
|
|
3487
3573
|
}
|
|
3488
3574
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3489
3575
|
}
|
|
3490
|
-
const app = await findPaymentApplication(db,
|
|
3576
|
+
const app = await findPaymentApplication(db, event2);
|
|
3491
3577
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3492
|
-
reportStripePartition("warn", eventId,
|
|
3578
|
+
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
3493
3579
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3494
3580
|
}
|
|
3495
|
-
const replay = await replayedStripeReceipt(db, eventId, digest,
|
|
3581
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event2, legacyDigest, ctx);
|
|
3496
3582
|
if (replay) return replay;
|
|
3497
3583
|
const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
3498
|
-
const disposition = classifyMemberStripeEvent(
|
|
3584
|
+
const disposition = classifyMemberStripeEvent(identity2, target, localApplication);
|
|
3499
3585
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
3500
|
-
return quarantineStripeEvent(db, eventId, digest,
|
|
3586
|
+
return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3501
3587
|
}
|
|
3502
3588
|
if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
|
|
3503
|
-
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3589
|
+
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3504
3590
|
return json({ ok: true, historical: true });
|
|
3505
3591
|
}
|
|
3506
|
-
if (
|
|
3592
|
+
if (event2.kind === "seat_paid" || event2.kind === "seat_refunded") {
|
|
3507
3593
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3508
3594
|
if (!seats) return json({ error: "named seat reconciliation is not configured" }, 503);
|
|
3509
3595
|
try {
|
|
3510
3596
|
const intent = await firstRow2(db, "subscriptionCheckoutIntents", { where: { id: String(app.id) }, limit: 1 });
|
|
3511
3597
|
if (!intent) return json({ error: "membership checkout reconciliation pending" }, 503);
|
|
3512
3598
|
const target2 = await membershipTargetForIntent(intent, app, env, ctx);
|
|
3513
|
-
const receipt = await seats[
|
|
3514
|
-
if (receipt.seatId !==
|
|
3515
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3599
|
+
const receipt = await seats[event2.kind === "seat_paid" ? "confirmPayment" : "confirmRefund"]({ ...target2, seatId: event2.seatId, eventId }, env);
|
|
3600
|
+
if (receipt.seatId !== event2.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
|
|
3601
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3516
3602
|
if (stored2 === "collision") {
|
|
3517
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3603
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3518
3604
|
return json({ ok: true, matched: false, collision: true });
|
|
3519
3605
|
}
|
|
3520
|
-
return json({ ok: true, seatId:
|
|
3606
|
+
return json({ ok: true, seatId: event2.seatId });
|
|
3521
3607
|
} catch {
|
|
3522
3608
|
return json({ error: "named seat reconciliation pending" }, 503);
|
|
3523
3609
|
}
|
|
3524
3610
|
}
|
|
3525
|
-
let authoritativeEvent =
|
|
3611
|
+
let authoritativeEvent = event2;
|
|
3526
3612
|
let membershipProjection = {};
|
|
3527
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(
|
|
3613
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
3528
3614
|
try {
|
|
3529
3615
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3530
3616
|
membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
|
|
@@ -3533,13 +3619,13 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3533
3619
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3534
3620
|
}
|
|
3535
3621
|
}
|
|
3536
|
-
if (
|
|
3622
|
+
if (event2.kind === "first_payment") {
|
|
3537
3623
|
const intent = await firstRow2(db, "subscriptionCheckoutIntents", { where: { id: String(app.id) }, limit: 1 });
|
|
3538
3624
|
if (intent) {
|
|
3539
|
-
const evidence = verifyInitialMembershipInvoice(intent,
|
|
3625
|
+
const evidence = verifyInitialMembershipInvoice(intent, event2.invoice);
|
|
3540
3626
|
if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
|
|
3541
|
-
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest,
|
|
3542
|
-
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...
|
|
3627
|
+
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event2, ctx);
|
|
3628
|
+
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event2, renewalAt: evidence.renewalAt };
|
|
3543
3629
|
}
|
|
3544
3630
|
if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
|
|
3545
3631
|
try {
|
|
@@ -3549,7 +3635,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3549
3635
|
}
|
|
3550
3636
|
}
|
|
3551
3637
|
}
|
|
3552
|
-
const invoiceEvent =
|
|
3638
|
+
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
3553
3639
|
const patch = { ...invoiceEvent && app.canceled === true ? {} : paymentEventPatch(authoritativeEvent, String(app.status ?? "")), ...membershipProjection };
|
|
3554
3640
|
if (invoiceEvent && typeof patch.renewalAt === "number" && patch.renewalAt > 0 && typeof app.renewalAt === "number" && patch.renewalAt < app.renewalAt) delete patch.renewalAt;
|
|
3555
3641
|
let next = app;
|
|
@@ -3583,7 +3669,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3583
3669
|
return json({ error: "refund account access reconciliation pending" }, 503);
|
|
3584
3670
|
}
|
|
3585
3671
|
}
|
|
3586
|
-
if (
|
|
3672
|
+
if (event2.kind === "first_payment" && next.status !== "refunded" && next.status !== "declined" && next.canceled !== true) {
|
|
3587
3673
|
try {
|
|
3588
3674
|
await notifyPaymentConfirmed(db, env, eventId, next);
|
|
3589
3675
|
} catch {
|
|
@@ -3602,9 +3688,9 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3602
3688
|
if (!approval.ok) return json({ error: "membership approval reconciliation pending" }, 503);
|
|
3603
3689
|
}
|
|
3604
3690
|
}
|
|
3605
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3691
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event: event2, disposition, matched: true });
|
|
3606
3692
|
if (stored === "collision") {
|
|
3607
|
-
await reportStripeCollision(db, ctx, eventId,
|
|
3693
|
+
await reportStripeCollision(db, ctx, eventId, event2);
|
|
3608
3694
|
return json({ ok: true, matched: false, collision: true });
|
|
3609
3695
|
}
|
|
3610
3696
|
return json({ ok: true });
|
|
@@ -3762,8 +3848,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3762
3848
|
if (!String(row2.disposition).startsWith("quarantine_") && row2.retryReady !== true) return json({ error: "event is already reconciled" }, 409);
|
|
3763
3849
|
try {
|
|
3764
3850
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3765
|
-
const
|
|
3766
|
-
if (provider.event.livemode !== (env.ODLA_ENV === "prod") || classifyMemberStripeEvent(stripeEventIdentity(
|
|
3851
|
+
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3852
|
+
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);
|
|
3767
3853
|
const id6 = crypto.randomUUID();
|
|
3768
3854
|
await db.transact([
|
|
3769
3855
|
{ t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
|
|
@@ -3784,7 +3870,7 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3784
3870
|
equals: { digest: row2.digest, disposition: row2.disposition, ...row2.reviewId ? { reviewId: row2.reviewId } : {} },
|
|
3785
3871
|
...!row2.reviewId ? { absent: ["reviewId"] } : {}
|
|
3786
3872
|
}] });
|
|
3787
|
-
return reconcileChapterStripeEvent({ rawBody: provider.rawBody, eventId, event, origin: url.origin }, env, ctx);
|
|
3873
|
+
return reconcileChapterStripeEvent({ rawBody: provider.rawBody, eventId, event: event2, origin: url.origin }, env, ctx);
|
|
3788
3874
|
} catch {
|
|
3789
3875
|
return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
|
|
3790
3876
|
}
|
|
@@ -4344,7 +4430,7 @@ function stateOf(item, all, now, window, rivals) {
|
|
|
4344
4430
|
if (closed) dates.push(mine.until);
|
|
4345
4431
|
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
4346
4432
|
}
|
|
4347
|
-
var mechanism = (
|
|
4433
|
+
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";
|
|
4348
4434
|
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
4349
4435
|
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
4350
4436
|
function normalize(rows5, collection, parse, invalid2) {
|
|
@@ -4456,16 +4542,16 @@ var couponOf = (discount) => {
|
|
|
4456
4542
|
async function retermPolicy(db, scope2, id6) {
|
|
4457
4543
|
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4458
4544
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4459
|
-
let
|
|
4545
|
+
let policy2;
|
|
4460
4546
|
try {
|
|
4461
|
-
|
|
4547
|
+
policy2 = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4462
4548
|
} catch {
|
|
4463
4549
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4464
4550
|
}
|
|
4465
|
-
if (
|
|
4466
|
-
return
|
|
4551
|
+
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");
|
|
4552
|
+
return policy2;
|
|
4467
4553
|
}
|
|
4468
|
-
async function attachProviderDiscount(sk, scope2, member,
|
|
4554
|
+
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4469
4555
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4470
4556
|
const read2 = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4471
4557
|
const sub = read2.body, items = sub?.items?.data;
|
|
@@ -4481,8 +4567,8 @@ async function attachProviderDiscount(sk, scope2, member, policy, couponId, prod
|
|
|
4481
4567
|
const written = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
4482
4568
|
"discounts[0][coupon]": couponId,
|
|
4483
4569
|
"expand[0]": "discounts",
|
|
4484
|
-
"metadata[discountPolicyVersionId]":
|
|
4485
|
-
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${
|
|
4570
|
+
"metadata[discountPolicyVersionId]": policy2.id
|
|
4571
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
|
|
4486
4572
|
const after = written.body?.discounts;
|
|
4487
4573
|
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4488
4574
|
membershipFail("membership_reterm_provider_unconfirmed");
|
|
@@ -4496,8 +4582,8 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4496
4582
|
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4497
4583
|
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");
|
|
4498
4584
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4499
|
-
const
|
|
4500
|
-
const receipt = { policyVersionId:
|
|
4585
|
+
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4586
|
+
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4501
4587
|
const skip = (membershipId, reason) => {
|
|
4502
4588
|
receipt.skipped.push({ membershipId, reason });
|
|
4503
4589
|
};
|
|
@@ -4518,7 +4604,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4518
4604
|
}
|
|
4519
4605
|
for (const member of rows5) {
|
|
4520
4606
|
const id6 = String(member.id);
|
|
4521
|
-
if (member.discountPolicyVersionId ===
|
|
4607
|
+
if (member.discountPolicyVersionId === policy2.id) {
|
|
4522
4608
|
receipt.unchanged.push(id6);
|
|
4523
4609
|
continue;
|
|
4524
4610
|
}
|
|
@@ -4530,22 +4616,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4530
4616
|
skip(id6, "not_founding");
|
|
4531
4617
|
continue;
|
|
4532
4618
|
}
|
|
4533
|
-
if (member.cohort !==
|
|
4619
|
+
if (member.cohort !== policy2.cohortLabel) {
|
|
4534
4620
|
skip(id6, "cohort_mismatch");
|
|
4535
4621
|
continue;
|
|
4536
4622
|
}
|
|
4537
4623
|
const skuId = String(member.baseSkuId);
|
|
4538
|
-
if (!
|
|
4624
|
+
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4539
4625
|
skip(id6, "sku_not_discounted");
|
|
4540
4626
|
continue;
|
|
4541
4627
|
}
|
|
4542
4628
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4543
|
-
const renewal = Math.round(base * (1e4 -
|
|
4629
|
+
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4544
4630
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4545
4631
|
skip(id6, "would_not_widen");
|
|
4546
4632
|
continue;
|
|
4547
4633
|
}
|
|
4548
|
-
const couponId = await membershipDiscountCouponId({ ...scope2, skuId },
|
|
4634
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
4549
4635
|
let providerUpdated;
|
|
4550
4636
|
try {
|
|
4551
4637
|
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
@@ -4563,10 +4649,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4563
4649
|
discountProductId: productId,
|
|
4564
4650
|
billingTerms: {
|
|
4565
4651
|
offeringVersionId: String(member.offeringVersionId),
|
|
4566
|
-
discount: { policyVersionId:
|
|
4652
|
+
discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
|
|
4567
4653
|
}
|
|
4568
4654
|
}, false);
|
|
4569
|
-
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member,
|
|
4655
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
|
|
4570
4656
|
} catch (error) {
|
|
4571
4657
|
const code = error.code;
|
|
4572
4658
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
@@ -4577,17 +4663,17 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4577
4663
|
}
|
|
4578
4664
|
try {
|
|
4579
4665
|
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4580
|
-
discountPolicyVersionId:
|
|
4581
|
-
discountSnapshot:
|
|
4666
|
+
discountPolicyVersionId: policy2.id,
|
|
4667
|
+
discountSnapshot: policy2,
|
|
4582
4668
|
renewalAmountCents: renewal,
|
|
4583
4669
|
revision: Number(member.revision) + 1,
|
|
4584
4670
|
updatedAt: now
|
|
4585
4671
|
})], {
|
|
4586
|
-
mutationId: `membership-reterm:${id6}:${
|
|
4672
|
+
mutationId: `membership-reterm:${id6}:${policy2.id}:${member.revision}`,
|
|
4587
4673
|
guards: [
|
|
4588
4674
|
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4589
4675
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4590
|
-
{ ns: "discountPolicyVersion", id:
|
|
4676
|
+
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4591
4677
|
]
|
|
4592
4678
|
});
|
|
4593
4679
|
} catch (error) {
|
|
@@ -4833,18 +4919,18 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
4833
4919
|
|
|
4834
4920
|
// src/member-session-application.ts
|
|
4835
4921
|
var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
|
|
4836
|
-
async function memberSessionApplication(db, chapterId, runtime,
|
|
4922
|
+
async function memberSessionApplication(db, chapterId, runtime, identity2) {
|
|
4837
4923
|
const lookup = async (where) => {
|
|
4838
4924
|
const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
|
|
4839
4925
|
if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
|
|
4840
4926
|
return rows5.filter((row2) => local(row2, chapterId, runtime));
|
|
4841
4927
|
};
|
|
4842
|
-
let apps = await lookup({ clerkUserId:
|
|
4928
|
+
let apps = await lookup({ clerkUserId: identity2.userId });
|
|
4843
4929
|
let emailFallback = false;
|
|
4844
|
-
if (!apps.length &&
|
|
4845
|
-
const normalized = normalizeChapterEmail(
|
|
4930
|
+
if (!apps.length && identity2.email) {
|
|
4931
|
+
const normalized = normalizeChapterEmail(identity2.email);
|
|
4846
4932
|
const normalizedRows = await lookup({ emailNormalized: normalized });
|
|
4847
|
-
const legacyRows = await lookup({ email:
|
|
4933
|
+
const legacyRows = await lookup({ email: identity2.email });
|
|
4848
4934
|
apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
|
|
4849
4935
|
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
|
|
4850
4936
|
emailFallback = true;
|
|
@@ -4854,8 +4940,8 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
4854
4940
|
if (emailFallback) {
|
|
4855
4941
|
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
|
|
4856
4942
|
try {
|
|
4857
|
-
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId:
|
|
4858
|
-
mutationId: `member-account-link:${app.id}:${
|
|
4943
|
+
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
|
|
4944
|
+
mutationId: `member-account-link:${app.id}:${identity2.userId}`,
|
|
4859
4945
|
guards: [{
|
|
4860
4946
|
ns: "applications",
|
|
4861
4947
|
id: String(app.id),
|
|
@@ -4868,7 +4954,7 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
4868
4954
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4869
4955
|
}
|
|
4870
4956
|
const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
|
|
4871
|
-
if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !==
|
|
4957
|
+
if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
|
|
4872
4958
|
app = saved;
|
|
4873
4959
|
}
|
|
4874
4960
|
const meeting = (await db.query({ meetings: { $: {
|
|
@@ -5000,7 +5086,7 @@ function belongsToSignup(account, applicationId) {
|
|
|
5000
5086
|
}
|
|
5001
5087
|
|
|
5002
5088
|
// src/member.ts
|
|
5003
|
-
var
|
|
5089
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
5004
5090
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
5005
5091
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
5006
5092
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -5034,7 +5120,7 @@ function resolveApplication(a) {
|
|
|
5034
5120
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
5035
5121
|
throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
|
|
5036
5122
|
}
|
|
5037
|
-
(0,
|
|
5123
|
+
(0, import_crm8.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
|
|
5038
5124
|
}
|
|
5039
5125
|
}
|
|
5040
5126
|
return {
|
|
@@ -5077,7 +5163,7 @@ function applicantProfile(chapter, fields) {
|
|
|
5077
5163
|
}
|
|
5078
5164
|
async function submitApplication(db, chapter, fields, opts) {
|
|
5079
5165
|
const app = chapter.application;
|
|
5080
|
-
const fieldStates = (0,
|
|
5166
|
+
const fieldStates = (0, import_crm8.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
5081
5167
|
for (const [f, state] of Object.entries(fieldStates)) {
|
|
5082
5168
|
if (!state.required) continue;
|
|
5083
5169
|
const v = fields[f];
|
|
@@ -5207,7 +5293,7 @@ async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
|
5207
5293
|
}
|
|
5208
5294
|
|
|
5209
5295
|
// src/worker-routes.ts
|
|
5210
|
-
var
|
|
5296
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
5211
5297
|
|
|
5212
5298
|
// src/discussion-reference-security.ts
|
|
5213
5299
|
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
@@ -5808,7 +5894,7 @@ function foundingOf(offer) {
|
|
|
5808
5894
|
return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
|
|
5809
5895
|
}
|
|
5810
5896
|
async function createPaymentQuote(input) {
|
|
5811
|
-
const { group, tier, policy } = input;
|
|
5897
|
+
const { group, tier, policy: policy2 } = input;
|
|
5812
5898
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
5813
5899
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5814
5900
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
@@ -5831,9 +5917,9 @@ async function createPaymentQuote(input) {
|
|
|
5831
5917
|
dueTodayCents: lines.dueTodayCents
|
|
5832
5918
|
} : lines,
|
|
5833
5919
|
...presentation ? { presentation } : {},
|
|
5834
|
-
refundPolicyText:
|
|
5920
|
+
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5835
5921
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
5836
|
-
trustCopy:
|
|
5922
|
+
trustCopy: policy2 ? policy2.trustCopy ?? "" : String(group.trustCopy ?? ""),
|
|
5837
5923
|
...terms2 ? { commercial: {
|
|
5838
5924
|
offeringVersionId: terms2.offeringVersionId,
|
|
5839
5925
|
...terms2.discount ? {
|
|
@@ -5905,13 +5991,13 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5905
5991
|
if (error.code !== "membership_quote_expired") throw error;
|
|
5906
5992
|
}
|
|
5907
5993
|
}
|
|
5908
|
-
const
|
|
5994
|
+
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5909
5995
|
const guards = [{
|
|
5910
5996
|
ns: "applications",
|
|
5911
5997
|
id: id6,
|
|
5912
5998
|
exists: true,
|
|
5913
|
-
equals: Object.fromEntries(
|
|
5914
|
-
absent:
|
|
5999
|
+
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
6000
|
+
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
5915
6001
|
}, { ns: "subscriptionCheckoutIntents", id: id6, exists: false }];
|
|
5916
6002
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
5917
6003
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
@@ -6047,11 +6133,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6047
6133
|
const tier = findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null);
|
|
6048
6134
|
if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
|
|
6049
6135
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
6050
|
-
const
|
|
6136
|
+
const policy2 = control3?.document.policy?.tierPolicies?.[tier.id];
|
|
6051
6137
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6052
6138
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6053
6139
|
db,
|
|
6054
|
-
{ ...target, app, group, tier, policy, presentation, now: Date.now() },
|
|
6140
|
+
{ ...target, app, group, tier, policy: policy2, presentation, now: Date.now() },
|
|
6055
6141
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6056
6142
|
);
|
|
6057
6143
|
const quote = await createPaymentQuote({
|
|
@@ -6062,7 +6148,7 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6062
6148
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6063
6149
|
group,
|
|
6064
6150
|
tier,
|
|
6065
|
-
policy,
|
|
6151
|
+
policy: policy2,
|
|
6066
6152
|
presentation
|
|
6067
6153
|
});
|
|
6068
6154
|
return { tier, quote, billingTerms: void 0 };
|
|
@@ -6705,7 +6791,7 @@ var handleCrm = async (req, url, env, ctx) => {
|
|
|
6705
6791
|
const crmBase = ctx.crmBase;
|
|
6706
6792
|
if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
|
|
6707
6793
|
const db = ctx.makeDb(env);
|
|
6708
|
-
const routes = (0,
|
|
6794
|
+
const routes = (0, import_crm9.createCrmRoutes)({
|
|
6709
6795
|
crm: ctx.chapter.crm,
|
|
6710
6796
|
db,
|
|
6711
6797
|
authorize: async (r) => {
|
|
@@ -7209,15 +7295,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7209
7295
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
7210
7296
|
else for (const [id6, raw2] of Object.entries(overrides)) {
|
|
7211
7297
|
const path = `document.policy.tierPolicies.${id6}`;
|
|
7212
|
-
const
|
|
7213
|
-
if (!tiers.some((tier) => tier.id === id6) || !
|
|
7298
|
+
const policy2 = recordOf(raw2);
|
|
7299
|
+
if (!tiers.some((tier) => tier.id === id6) || !policy2) {
|
|
7214
7300
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7215
7301
|
continue;
|
|
7216
7302
|
}
|
|
7217
|
-
unknownKeys(
|
|
7303
|
+
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7218
7304
|
tierPolicies[id6] = {
|
|
7219
|
-
refundPolicyText: text4(
|
|
7220
|
-
...
|
|
7305
|
+
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7306
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7221
7307
|
};
|
|
7222
7308
|
}
|
|
7223
7309
|
}
|
|
@@ -7272,7 +7358,7 @@ function validateSignupControlDocument(input) {
|
|
|
7272
7358
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
7273
7359
|
}
|
|
7274
7360
|
const application = applicationOf(root.application, errors);
|
|
7275
|
-
const
|
|
7361
|
+
const policy2 = policyOf(root.policy, errors, tiers);
|
|
7276
7362
|
let scheduling;
|
|
7277
7363
|
if (root.scheduling !== void 0) {
|
|
7278
7364
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -7292,7 +7378,7 @@ function validateSignupControlDocument(input) {
|
|
|
7292
7378
|
tiers,
|
|
7293
7379
|
membershipPage: page.value,
|
|
7294
7380
|
...application ? { application } : {},
|
|
7295
|
-
...
|
|
7381
|
+
...policy2 ? { policy: policy2 } : {},
|
|
7296
7382
|
...scheduling ? { scheduling } : {},
|
|
7297
7383
|
...theme ? { theme } : {},
|
|
7298
7384
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -8109,7 +8195,7 @@ async function networkWriteAllowed(req, db, user, ctx, sensitiveRead = false) {
|
|
|
8109
8195
|
|
|
8110
8196
|
// src/network-person-reference.ts
|
|
8111
8197
|
var import_db6 = require("@odla-ai/db");
|
|
8112
|
-
var
|
|
8198
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
8113
8199
|
|
|
8114
8200
|
// src/worker-network-fetch.ts
|
|
8115
8201
|
var NetworkTargetTransportError = class extends Error {
|
|
@@ -8145,7 +8231,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
8145
8231
|
}
|
|
8146
8232
|
|
|
8147
8233
|
// src/worker-network-admission-state.ts
|
|
8148
|
-
var
|
|
8234
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
8149
8235
|
var MAX_ASSIGNMENTS = 2e3;
|
|
8150
8236
|
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
8151
8237
|
function assignmentId(targetId, personId) {
|
|
@@ -8172,7 +8258,7 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
8172
8258
|
return isValidEmail(normalized) ? normalized : null;
|
|
8173
8259
|
}
|
|
8174
8260
|
async function grantInput(db, ctx, assignment, resolveEmail) {
|
|
8175
|
-
const person = resolveEmail ? null : await (0,
|
|
8261
|
+
const person = resolveEmail ? null : await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
8176
8262
|
const email = resolveEmail ? await resolveEmail(assignment.personId) : person ? personAdmissionEmail(ctx, person) : null;
|
|
8177
8263
|
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
8178
8264
|
return {
|
|
@@ -8378,7 +8464,7 @@ function chapterPersonReference(targetId, recordId) {
|
|
|
8378
8464
|
}
|
|
8379
8465
|
async function resolveAdmissionPersonEmail(db, env, ctx, target, personId) {
|
|
8380
8466
|
if (!personId.startsWith("chapter-person:")) {
|
|
8381
|
-
const person2 = await (0,
|
|
8467
|
+
const person2 = await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, personId);
|
|
8382
8468
|
return person2 ? personAdmissionEmail(ctx, person2) : null;
|
|
8383
8469
|
}
|
|
8384
8470
|
const prefix = `chapter-person:${target.id}:`;
|
|
@@ -9036,8 +9122,8 @@ function createStripeProvider(config) {
|
|
|
9036
9122
|
if (!webhookSecret || !await (0, import_stripe11.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
9037
9123
|
return { ok: false, reason: "bad_signature" };
|
|
9038
9124
|
}
|
|
9039
|
-
const
|
|
9040
|
-
return { ok: true, eventId:
|
|
9125
|
+
const event2 = JSON.parse(rawBody);
|
|
9126
|
+
return { ok: true, eventId: event2.id, event: normalizeWebhookEvent(event2, { requireProviderPayment: config.requireProviderPayment }) };
|
|
9041
9127
|
},
|
|
9042
9128
|
async refund(input) {
|
|
9043
9129
|
return refundSubscriptionPayment(requireSecret(secretKey), input);
|
|
@@ -9151,7 +9237,7 @@ async function startGiftIntent(db, input) {
|
|
|
9151
9237
|
throw new Error("gift purchase intent does not match this checkout");
|
|
9152
9238
|
}
|
|
9153
9239
|
if (stored.status === "paid") throw new Error("gift purchase is already paid");
|
|
9154
|
-
const
|
|
9240
|
+
const checkout2 = await startGiftProviderCheckout(
|
|
9155
9241
|
input.stripeSecret,
|
|
9156
9242
|
stored,
|
|
9157
9243
|
input.purchaserEmail,
|
|
@@ -9161,15 +9247,15 @@ async function startGiftIntent(db, input) {
|
|
|
9161
9247
|
);
|
|
9162
9248
|
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
|
|
9163
9249
|
status: "checkout_ready",
|
|
9164
|
-
providerObjectId:
|
|
9165
|
-
...
|
|
9250
|
+
providerObjectId: checkout2.providerObjectId,
|
|
9251
|
+
...checkout2.customerId ? { providerCustomerId: checkout2.customerId } : {},
|
|
9166
9252
|
checkoutStartedAt: input.now,
|
|
9167
9253
|
updatedAt: input.now
|
|
9168
|
-
} }], { mutationId: `gift-checkout-ready:${input.id}:${
|
|
9254
|
+
} }], { mutationId: `gift-checkout-ready:${input.id}:${checkout2.providerObjectId}` });
|
|
9169
9255
|
return {
|
|
9170
9256
|
gift: true,
|
|
9171
9257
|
intentId: input.id,
|
|
9172
|
-
clientSecret:
|
|
9258
|
+
clientSecret: checkout2.clientSecret,
|
|
9173
9259
|
publishableKey: input.publishableKey,
|
|
9174
9260
|
lineItems: { standardCents: input.offer.priceCents, discountCents: 0, dueTodayCents: input.offer.priceCents },
|
|
9175
9261
|
...giftCode ? { giftCode } : {}
|
|
@@ -9427,15 +9513,15 @@ async function startSubscription(req, env, ctx) {
|
|
|
9427
9513
|
return json({ error: error instanceof Error ? error.message : "gift checkout failed" }, 502);
|
|
9428
9514
|
}
|
|
9429
9515
|
}
|
|
9430
|
-
let
|
|
9516
|
+
let checkout2;
|
|
9431
9517
|
try {
|
|
9432
|
-
|
|
9518
|
+
checkout2 = await checkoutQuoteForApplication(db, app, group, env, ctx);
|
|
9433
9519
|
} catch (error) {
|
|
9434
9520
|
const expired = error.code === "membership_quote_expired";
|
|
9435
9521
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9436
9522
|
}
|
|
9437
|
-
const { tier, quote, billingTerms } =
|
|
9438
|
-
const membershipOffer = "membershipOffer" in
|
|
9523
|
+
const { tier, quote, billingTerms } = checkout2;
|
|
9524
|
+
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9439
9525
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9440
9526
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
9441
9527
|
if (tierIsFree(tier)) return json({ error: "tier is free", code: "tier_not_payable" }, 409);
|
|
@@ -9857,13 +9943,6 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9857
9943
|
}
|
|
9858
9944
|
};
|
|
9859
9945
|
|
|
9860
|
-
// src/named-seat-accepted-template.ts
|
|
9861
|
-
var SEAT_ACCEPTED_TEMPLATE = {
|
|
9862
|
-
enabled: true,
|
|
9863
|
-
subject: "{{recipientName}} accepted your {{chapterName}} seat",
|
|
9864
|
-
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}}"
|
|
9865
|
-
};
|
|
9866
|
-
|
|
9867
9946
|
// src/named-seat-accepted-notice.ts
|
|
9868
9947
|
var text5 = (value) => typeof value === "string" ? value : "";
|
|
9869
9948
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
@@ -9874,7 +9953,6 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
9874
9953
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9875
9954
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
9876
9955
|
const config = emailGroupFrom(group);
|
|
9877
|
-
config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
|
|
9878
9956
|
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
|
|
9879
9957
|
return sendTemplated({
|
|
9880
9958
|
db,
|
|
@@ -9890,6 +9968,7 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
9890
9968
|
to: text5(purchaser.email),
|
|
9891
9969
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
9892
9970
|
applicationId: input.primaryApplicationId,
|
|
9971
|
+
tierId: input.skuId,
|
|
9893
9972
|
vars: {
|
|
9894
9973
|
firstName: text5(purchaser.firstName),
|
|
9895
9974
|
purchaserName,
|
|
@@ -9906,7 +9985,7 @@ async function tierName(db, group, skuId) {
|
|
|
9906
9985
|
if (tier) return tier.name;
|
|
9907
9986
|
} catch {
|
|
9908
9987
|
}
|
|
9909
|
-
return
|
|
9988
|
+
return "";
|
|
9910
9989
|
}
|
|
9911
9990
|
|
|
9912
9991
|
// src/worker-routes-named-seat-recipient.ts
|
|
@@ -10103,10 +10182,10 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
10103
10182
|
};
|
|
10104
10183
|
|
|
10105
10184
|
// src/worker-routes-people-profile.ts
|
|
10106
|
-
var
|
|
10185
|
+
var import_crm13 = require("@odla-ai/crm");
|
|
10107
10186
|
|
|
10108
10187
|
// src/people-profile.ts
|
|
10109
|
-
var
|
|
10188
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
10110
10189
|
var sameValue = (a, b) => (a == null || a === "") && (b == null || b === "") || JSON.stringify(a) === JSON.stringify(b);
|
|
10111
10190
|
function profileReadOnlyFields(record4, ctx) {
|
|
10112
10191
|
return [
|
|
@@ -10121,17 +10200,17 @@ async function updateChapterPerson(opts) {
|
|
|
10121
10200
|
const { db, env, ctx, record: record4 } = opts;
|
|
10122
10201
|
const input = Object.fromEntries(Object.entries(opts.input).filter(([key3, value]) => !sameValue(value, record4.fields[key3])));
|
|
10123
10202
|
const forbidden = profileReadOnlyFields(record4, ctx).filter((key3) => key3 in input);
|
|
10124
|
-
if (forbidden.length) throw new
|
|
10203
|
+
if (forbidden.length) throw new import_crm12.CrmInputError(
|
|
10125
10204
|
"Use the verified identity or application workflow for protected fields",
|
|
10126
10205
|
Object.fromEntries(forbidden.map((key3) => [key3, "This field is managed by its authoritative workflow"]))
|
|
10127
10206
|
);
|
|
10128
10207
|
const appId = record4.fields.applicationId;
|
|
10129
10208
|
const app = typeof appId === "string" && appId ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : null;
|
|
10130
10209
|
if (appId && (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || String(app.email).trim().toLowerCase() !== String(record4.primaryEmail).trim().toLowerCase())) {
|
|
10131
|
-
throw new
|
|
10210
|
+
throw new import_crm12.CrmInputError("The application identity must be repaired before editing this profile");
|
|
10132
10211
|
}
|
|
10133
10212
|
const prepared = ctx.chapter.crm.prepare(record4.type, input, record4);
|
|
10134
|
-
if (!prepared.ok) throw new
|
|
10213
|
+
if (!prepared.ok) throw new import_crm12.CrmInputError("invalid input", prepared.errors);
|
|
10135
10214
|
const appAttrs = {};
|
|
10136
10215
|
if (app) {
|
|
10137
10216
|
const mirrored = /* @__PURE__ */ new Set(["firstName", "lastName", "phone", "linkedin", ...ctx.chapter.application.crmFields]);
|
|
@@ -10160,7 +10239,7 @@ async function updateChapterPerson(opts) {
|
|
|
10160
10239
|
}] });
|
|
10161
10240
|
}
|
|
10162
10241
|
};
|
|
10163
|
-
return (0,
|
|
10242
|
+
return (0, import_crm12.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
|
|
10164
10243
|
id: record4.id,
|
|
10165
10244
|
input,
|
|
10166
10245
|
expectedUpdatedAt: opts.expectedRevision ?? record4.updatedAt,
|
|
@@ -10204,7 +10283,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10204
10283
|
} catch {
|
|
10205
10284
|
return json({ error: "invalid record id" }, 422);
|
|
10206
10285
|
}
|
|
10207
|
-
const record4 = await (0,
|
|
10286
|
+
const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db }, id6);
|
|
10208
10287
|
if (!record4) return json({ error: "not found" }, 404);
|
|
10209
10288
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
10210
10289
|
if (match?.[2]) {
|
|
@@ -10256,7 +10335,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10256
10335
|
});
|
|
10257
10336
|
return json(result);
|
|
10258
10337
|
} catch (error) {
|
|
10259
|
-
if (error instanceof
|
|
10338
|
+
if (error instanceof import_crm13.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
|
|
10260
10339
|
if (error.code === "transact_guard_failed") return json({ error: "profile changed; reload before saving" }, 409);
|
|
10261
10340
|
return json({ error: "profile update failed" }, 503);
|
|
10262
10341
|
}
|
|
@@ -10413,10 +10492,10 @@ async function repairAccountPeoplePage(deps, after) {
|
|
|
10413
10492
|
}
|
|
10414
10493
|
|
|
10415
10494
|
// src/crm-application-repair.ts
|
|
10416
|
-
var
|
|
10495
|
+
var import_crm14 = require("@odla-ai/crm");
|
|
10417
10496
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
10418
10497
|
const deps = { crm: chapter.crm, db };
|
|
10419
|
-
const record4 = await (0,
|
|
10498
|
+
const record4 = await (0, import_crm14.getRecord)(deps, recordId);
|
|
10420
10499
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
10421
10500
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
10422
10501
|
const fields = record4.fields;
|
|
@@ -10434,7 +10513,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
10434
10513
|
}
|
|
10435
10514
|
const app = [...applications.values()][0];
|
|
10436
10515
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
10437
|
-
await (0,
|
|
10516
|
+
await (0, import_crm14.updateRecord)(deps, {
|
|
10438
10517
|
id: recordId,
|
|
10439
10518
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
10440
10519
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -11121,7 +11200,122 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11121
11200
|
return patchApplication(db, env, ctx, app, body);
|
|
11122
11201
|
};
|
|
11123
11202
|
|
|
11203
|
+
// src/email-admin-config.ts
|
|
11204
|
+
var import_crm16 = require("@odla-ai/crm");
|
|
11205
|
+
|
|
11206
|
+
// src/email-readiness.ts
|
|
11207
|
+
var import_crm15 = require("@odla-ai/crm");
|
|
11208
|
+
function validateEmailReadiness(input) {
|
|
11209
|
+
const issues = [];
|
|
11210
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
11211
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
11212
|
+
}
|
|
11213
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
11214
|
+
for (const tier of input.tiers ?? []) {
|
|
11215
|
+
if (!(0, import_crm15.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
11216
|
+
}
|
|
11217
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm15.isEmailTierName)(tier.name, tier.id));
|
|
11218
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
11219
|
+
const events = Object.entries(input.events).map(([key3, event2]) => {
|
|
11220
|
+
const template = input.templates[key3];
|
|
11221
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm15.validateEmailCopy)(template, { allowedVariables: event2.variables });
|
|
11222
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
11223
|
+
if (template) {
|
|
11224
|
+
const used = [...`${template.subject ?? ""}
|
|
11225
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
11226
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
11227
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
11231
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
11232
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
11233
|
+
});
|
|
11234
|
+
return { checkedAt: input.now ?? Date.now(), events, issues };
|
|
11235
|
+
}
|
|
11236
|
+
|
|
11237
|
+
// src/email-admin-config.ts
|
|
11238
|
+
var str2 = (value) => typeof value === "string" ? value : "";
|
|
11239
|
+
async function emailSettings(db, ctx, env) {
|
|
11240
|
+
const hub = ctx.chapter.mode === "hub";
|
|
11241
|
+
const crmDeps3 = { db, crm: ctx.chapter.crm };
|
|
11242
|
+
const group = hub ? await (0, import_crm16.getConfig)(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
11243
|
+
const raw2 = group?.emailTemplates;
|
|
11244
|
+
const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
|
|
11245
|
+
const events = chapterEmailEvents(ctx.chapter);
|
|
11246
|
+
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
11247
|
+
subject: str2(stored[key3]?.subject),
|
|
11248
|
+
text: str2(stored[key3]?.text),
|
|
11249
|
+
enabled: stored[key3]?.enabled
|
|
11250
|
+
}]));
|
|
11251
|
+
let tiers = [], tiersUnavailable = false;
|
|
11252
|
+
if (!hub) {
|
|
11253
|
+
try {
|
|
11254
|
+
tiers = await loadTiers(db, group ?? { id: ctx.chapter.id });
|
|
11255
|
+
} catch {
|
|
11256
|
+
tiersUnavailable = true;
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
const groupValues = Object.fromEntries(["refundPolicyText", "commitmentText", "normsText"].map((key3) => [key3, str2(group?.[key3])]));
|
|
11260
|
+
const validation = validateEmailReadiness({ events, templates: stored, groupValues, tiers: hub ? void 0 : tiers, tiersUnavailable });
|
|
11261
|
+
return { group, events, data: {
|
|
11262
|
+
groupId: ctx.chapter.id,
|
|
11263
|
+
name: ctx.chapter.name,
|
|
11264
|
+
emailTemplates,
|
|
11265
|
+
validation,
|
|
11266
|
+
replyTo: str2(group?.replyTo),
|
|
11267
|
+
fromName: str2(group?.fromName),
|
|
11268
|
+
notificationEmail: str2(group?.notificationEmail),
|
|
11269
|
+
debugEmail: str2(group?.debugEmail),
|
|
11270
|
+
...groupValues,
|
|
11271
|
+
envName: env.ODLA_ENV,
|
|
11272
|
+
testAvailable: !hub,
|
|
11273
|
+
transport: env.SEND_EMAIL && env.EMAIL_FROM ? "cloudflare" : "log-only",
|
|
11274
|
+
fromEmail: env.EMAIL_FROM ?? null
|
|
11275
|
+
} };
|
|
11276
|
+
}
|
|
11277
|
+
async function updateEmailSettings(db, ctx, env, body) {
|
|
11278
|
+
const { group, events } = await emailSettings(db, ctx, env);
|
|
11279
|
+
if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
|
|
11280
|
+
const templates = body.emailTemplates;
|
|
11281
|
+
if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
|
|
11282
|
+
const clean = {};
|
|
11283
|
+
for (const [key3, value] of Object.entries(templates)) {
|
|
11284
|
+
if (!events[key3]) continue;
|
|
11285
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
11286
|
+
const t = value;
|
|
11287
|
+
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
11288
|
+
const subject = str2(t.subject).trim(), text8 = str2(t.text);
|
|
11289
|
+
const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text8 }, { allowedVariables: events[key3].variables });
|
|
11290
|
+
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
11291
|
+
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
11292
|
+
}
|
|
11293
|
+
clean[key3] = { subject, text: text8, enabled: t.enabled };
|
|
11294
|
+
}
|
|
11295
|
+
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
11296
|
+
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
11297
|
+
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
11298
|
+
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
11299
|
+
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
11300
|
+
const fromName = str2(body.fromName).trim();
|
|
11301
|
+
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);
|
|
11302
|
+
const commitmentText = str2(body.commitmentText), normsText = str2(body.normsText);
|
|
11303
|
+
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
11304
|
+
if (ctx.chapter.mode === "hub") {
|
|
11305
|
+
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11306
|
+
await (0, import_crm16.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11307
|
+
} else {
|
|
11308
|
+
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11309
|
+
await db.transact([
|
|
11310
|
+
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
11311
|
+
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11312
|
+
]);
|
|
11313
|
+
}
|
|
11314
|
+
return json({ ok: true });
|
|
11315
|
+
}
|
|
11316
|
+
|
|
11124
11317
|
// src/worker-routes-admin-comms.ts
|
|
11318
|
+
var import_crm17 = require("@odla-ai/crm");
|
|
11125
11319
|
async function gate5(req, env, ctx) {
|
|
11126
11320
|
const rawDb = ctx.makeDb(env);
|
|
11127
11321
|
const u = await ctx.verifyUser(req, env);
|
|
@@ -11133,39 +11327,13 @@ async function loadGroup2(db, id6) {
|
|
|
11133
11327
|
const { groups } = await db.query({ groups: { $: { where: { id: id6 }, limit: 1 } } });
|
|
11134
11328
|
return groups?.[0] ?? null;
|
|
11135
11329
|
}
|
|
11136
|
-
var
|
|
11330
|
+
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
11137
11331
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
11138
11332
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
11139
11333
|
const got = await gate5(req, env, ctx);
|
|
11140
11334
|
if (got instanceof Response) return got;
|
|
11141
11335
|
const db = got;
|
|
11142
|
-
|
|
11143
|
-
if (!group) return json({ error: "not found" }, 404);
|
|
11144
|
-
if (req.method === "GET") {
|
|
11145
|
-
const stored = group.emailTemplates ?? {};
|
|
11146
|
-
const emailTemplates = {};
|
|
11147
|
-
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
11148
|
-
const t = stored[key3];
|
|
11149
|
-
if (t) emailTemplates[key3] = { subject: str2(t.subject), text: str2(t.text), enabled: t.enabled !== false };
|
|
11150
|
-
}
|
|
11151
|
-
return json({
|
|
11152
|
-
groupId: group.id,
|
|
11153
|
-
name: group.name,
|
|
11154
|
-
replyTo: str2(group.replyTo),
|
|
11155
|
-
fromName: str2(group.fromName),
|
|
11156
|
-
notificationEmail: str2(group.notificationEmail),
|
|
11157
|
-
debugEmail: str2(group.debugEmail),
|
|
11158
|
-
emailTemplates,
|
|
11159
|
-
commitmentText: str2(group.commitmentText),
|
|
11160
|
-
normsText: str2(group.normsText),
|
|
11161
|
-
refundPolicyText: str2(group.refundPolicyText),
|
|
11162
|
-
// Read-only delivery wiring — surfaced so "why did this not send?" is
|
|
11163
|
-
// answerable without logs.
|
|
11164
|
-
envName: env.ODLA_ENV,
|
|
11165
|
-
transport: env.SEND_EMAIL && env.EMAIL_FROM ? "cloudflare" : "log-only",
|
|
11166
|
-
fromEmail: env.EMAIL_FROM ?? null
|
|
11167
|
-
});
|
|
11168
|
-
}
|
|
11336
|
+
if (req.method === "GET") return json((await emailSettings(db, ctx, env)).data);
|
|
11169
11337
|
let body;
|
|
11170
11338
|
try {
|
|
11171
11339
|
body = await req.json();
|
|
@@ -11173,41 +11341,16 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
11173
11341
|
return json({ error: "invalid JSON body" }, 400);
|
|
11174
11342
|
}
|
|
11175
11343
|
if (!body || typeof body !== "object" || Array.isArray(body)) return json({ error: "object body required" }, 400);
|
|
11176
|
-
|
|
11177
|
-
if (!templates || typeof templates !== "object") return json({ error: "emailTemplates object required" }, 400);
|
|
11178
|
-
const clean = {};
|
|
11179
|
-
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
11180
|
-
const t = templates[key3];
|
|
11181
|
-
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
11182
|
-
const text8 = typeof t?.text === "string" ? t.text : "";
|
|
11183
|
-
if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
|
|
11184
|
-
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
|
|
11185
|
-
if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
|
|
11186
|
-
clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
|
|
11187
|
-
}
|
|
11188
|
-
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11189
|
-
const notificationEmail = str2(body.notificationEmail).trim();
|
|
11190
|
-
const replyTo = str2(body.replyTo).trim();
|
|
11191
|
-
const debugEmail = str2(body.debugEmail).trim();
|
|
11192
|
-
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
11193
|
-
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
11194
|
-
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
11195
|
-
const fromName = str2(body.fromName).trim();
|
|
11196
|
-
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);
|
|
11197
|
-
const commitmentText = str2(body.commitmentText);
|
|
11198
|
-
const normsText = str2(body.normsText);
|
|
11199
|
-
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
11200
|
-
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11201
|
-
await db.transact([
|
|
11202
|
-
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
11203
|
-
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11204
|
-
]);
|
|
11205
|
-
return json({ ok: true });
|
|
11344
|
+
return updateEmailSettings(db, ctx, env, body);
|
|
11206
11345
|
};
|
|
11207
11346
|
var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
11208
11347
|
if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
|
|
11209
11348
|
const got = await gate5(req, env, ctx);
|
|
11210
11349
|
if (got instanceof Response) return got;
|
|
11350
|
+
if (ctx.chapter.mode === "hub") {
|
|
11351
|
+
const rows5 = await (0, import_crm17.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
11352
|
+
return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
11353
|
+
}
|
|
11211
11354
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
11212
11355
|
const visible = [];
|
|
11213
11356
|
for (const row2 of emailLog ?? []) {
|
|
@@ -11243,21 +11386,22 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
11243
11386
|
} catch {
|
|
11244
11387
|
return json({ error: "invalid JSON body" }, 400);
|
|
11245
11388
|
}
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
}
|
|
11389
|
+
if (ctx.chapter.mode === "hub") return json({ error: "Send CRM mail from a selected contact." }, 400);
|
|
11390
|
+
const template = str3(body.template);
|
|
11391
|
+
const events = chapterEmailEvents(ctx.chapter);
|
|
11392
|
+
if (!events[template]) return json({ error: "Unknown email event." }, 400);
|
|
11250
11393
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
11251
11394
|
if (!group) return json({ error: "not found" }, 404);
|
|
11252
11395
|
const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
11253
11396
|
group: emailGroupFrom(group),
|
|
11254
11397
|
template,
|
|
11255
|
-
to:
|
|
11398
|
+
to: str3(group.notificationEmail),
|
|
11256
11399
|
vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
|
|
11257
11400
|
dedupeKey: `test:${template}:${Date.now()}`,
|
|
11258
|
-
force: true
|
|
11401
|
+
force: true,
|
|
11402
|
+
event: events[template]
|
|
11259
11403
|
});
|
|
11260
|
-
return json({ ok: res.sent, reason: res.reason ?? null, to:
|
|
11404
|
+
return json({ ok: res.sent, reason: res.reason ?? null, to: str3(group.notificationEmail), redirected: env.ODLA_ENV !== "prod" && !!group.debugEmail });
|
|
11261
11405
|
};
|
|
11262
11406
|
var handleAdminComms = async (req, url, env, ctx) => {
|
|
11263
11407
|
const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
|
|
@@ -11279,13 +11423,13 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
11279
11423
|
});
|
|
11280
11424
|
const app = data.applications?.[0] ?? null;
|
|
11281
11425
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
11282
|
-
const group = await loadGroup2(db,
|
|
11283
|
-
const vars = app ? { firstName:
|
|
11426
|
+
const group = await loadGroup2(db, str3(app?.groupId, ctx.chapter.id));
|
|
11427
|
+
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;
|
|
11284
11428
|
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
11285
11429
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
11286
11430
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
11287
11431
|
const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
|
|
11288
|
-
return { kind: "email", channel, label: String(r.template), subject:
|
|
11432
|
+
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 };
|
|
11289
11433
|
});
|
|
11290
11434
|
const calendar = (data.meetings ?? []).map((m2) => ({
|
|
11291
11435
|
kind: "calendar",
|
|
@@ -11301,7 +11445,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
11301
11445
|
};
|
|
11302
11446
|
|
|
11303
11447
|
// src/worker-routes-network.ts
|
|
11304
|
-
var
|
|
11448
|
+
var import_crm19 = require("@odla-ai/crm");
|
|
11305
11449
|
var import_db9 = require("@odla-ai/db");
|
|
11306
11450
|
|
|
11307
11451
|
// src/network-edge.ts
|
|
@@ -11327,7 +11471,7 @@ function unpairedResponse(unpaired) {
|
|
|
11327
11471
|
}
|
|
11328
11472
|
|
|
11329
11473
|
// src/worker-network-push.ts
|
|
11330
|
-
var
|
|
11474
|
+
var import_crm18 = require("@odla-ai/crm");
|
|
11331
11475
|
var import_db8 = require("@odla-ai/db");
|
|
11332
11476
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
11333
11477
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -11339,7 +11483,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
11339
11483
|
};
|
|
11340
11484
|
if (!secret) {
|
|
11341
11485
|
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
11342
|
-
await (0,
|
|
11486
|
+
await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
|
|
11343
11487
|
recordId: record4.id,
|
|
11344
11488
|
targetId: target.id,
|
|
11345
11489
|
status: "failed",
|
|
@@ -11378,7 +11522,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
11378
11522
|
const responseBody = await res.json().catch(() => ({}));
|
|
11379
11523
|
if (!res.ok) {
|
|
11380
11524
|
const error = responseBody.error ?? "follower rejected the record";
|
|
11381
|
-
await (0,
|
|
11525
|
+
await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
|
|
11382
11526
|
recordId: record4.id,
|
|
11383
11527
|
targetId: target.id,
|
|
11384
11528
|
status: "failed",
|
|
@@ -11393,7 +11537,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
11393
11537
|
error
|
|
11394
11538
|
};
|
|
11395
11539
|
}
|
|
11396
|
-
await (0,
|
|
11540
|
+
await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
|
|
11397
11541
|
recordId: record4.id,
|
|
11398
11542
|
targetId: target.id,
|
|
11399
11543
|
status: "delivered",
|
|
@@ -11409,7 +11553,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
11409
11553
|
};
|
|
11410
11554
|
} catch (err) {
|
|
11411
11555
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
11412
|
-
await (0,
|
|
11556
|
+
await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
|
|
11413
11557
|
recordId: record4.id,
|
|
11414
11558
|
targetId: target.id,
|
|
11415
11559
|
status: "failed",
|
|
@@ -11462,7 +11606,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
11462
11606
|
const verified = await (0, import_db9.verifyFederatedRequest)(req, { secret });
|
|
11463
11607
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
11464
11608
|
const types = await Promise.all(
|
|
11465
|
-
Object.keys(ctx.chapter.crm.config.types).map((type) => (0,
|
|
11609
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm19.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
11466
11610
|
);
|
|
11467
11611
|
const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
|
|
11468
11612
|
const snapshot = {
|
|
@@ -11609,7 +11753,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11609
11753
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
11610
11754
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
11611
11755
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
11612
|
-
const record4 = await (0,
|
|
11756
|
+
const record4 = await (0, import_crm19.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
11613
11757
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
11614
11758
|
const results = await Promise.all(
|
|
11615
11759
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -11618,11 +11762,11 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11618
11762
|
};
|
|
11619
11763
|
|
|
11620
11764
|
// src/worker-routes-network-records-follower.ts
|
|
11621
|
-
var
|
|
11765
|
+
var import_crm23 = require("@odla-ai/crm");
|
|
11622
11766
|
var import_db10 = require("@odla-ai/db");
|
|
11623
11767
|
|
|
11624
11768
|
// src/network-application-action.ts
|
|
11625
|
-
var
|
|
11769
|
+
var import_crm20 = require("@odla-ai/crm");
|
|
11626
11770
|
var ACTION_META = "chapterNetworkMutation";
|
|
11627
11771
|
async function networkApplicationAction(opts) {
|
|
11628
11772
|
const { db, env, ctx, record: record4, input, sourceId, activityId, fingerprint } = opts;
|
|
@@ -11711,7 +11855,7 @@ async function networkApplicationAction(opts) {
|
|
|
11711
11855
|
const current = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
|
|
11712
11856
|
if (!current || current.status !== input.stage) return json({ error: "application reconciliation pending", code: "action_pending" }, 503);
|
|
11713
11857
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) });
|
|
11714
|
-
const projected = await (0,
|
|
11858
|
+
const projected = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
11715
11859
|
if (!projected || projected.stage !== input.stage) return json({ error: "CRM reconciliation pending", code: "action_pending" }, 503);
|
|
11716
11860
|
const audit = (await db.query({ crm_activity: { $: { where: { id: activityId }, limit: 1 } } })).crm_activity?.[0];
|
|
11717
11861
|
const priorMeta = audit?.meta;
|
|
@@ -11721,7 +11865,7 @@ async function networkApplicationAction(opts) {
|
|
|
11721
11865
|
} },
|
|
11722
11866
|
{ t: "update", ns: "crm_activity", id: lockId, attrs: { meta: { state: "complete", activityId } } }
|
|
11723
11867
|
], { guards: [{ ns: "crm_activity", id: lockId, exists: true, equals: { meta: { state: "pending", activityId } } }] });
|
|
11724
|
-
const updated = await (0,
|
|
11868
|
+
const updated = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
11725
11869
|
if (!updated) return json({ error: "record not found" }, 404);
|
|
11726
11870
|
return { record: updated, duplicate: Boolean(receipt) };
|
|
11727
11871
|
}
|
|
@@ -11757,7 +11901,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
11757
11901
|
}
|
|
11758
11902
|
|
|
11759
11903
|
// src/worker-routes-network-records-search.ts
|
|
11760
|
-
var
|
|
11904
|
+
var import_crm21 = require("@odla-ai/crm");
|
|
11761
11905
|
var SEARCH_BATCH_SIZE = 1e3;
|
|
11762
11906
|
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
11763
11907
|
var ReaderSearchPopulationError = class extends Error {
|
|
@@ -11798,19 +11942,19 @@ async function listVisibleSearchPopulation(db, where, window, count) {
|
|
|
11798
11942
|
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
11799
11943
|
);
|
|
11800
11944
|
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
11801
|
-
[
|
|
11945
|
+
[import_crm21.CRM_NS.record]: { $: {
|
|
11802
11946
|
where,
|
|
11803
11947
|
order: { [window.sort.field]: window.sort.dir },
|
|
11804
11948
|
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
11805
11949
|
offset
|
|
11806
11950
|
} }
|
|
11807
11951
|
})));
|
|
11808
|
-
return pages.flatMap((page) => (page[
|
|
11952
|
+
return pages.flatMap((page) => (page[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord));
|
|
11809
11953
|
}
|
|
11810
11954
|
async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
11811
11955
|
const where = baseWhere(type, window.stage);
|
|
11812
11956
|
const crmDb = db;
|
|
11813
|
-
const aggregate = await crmDb.aggregate(
|
|
11957
|
+
const aggregate = await crmDb.aggregate(import_crm21.CRM_NS.record, { count: true }, { where });
|
|
11814
11958
|
const candidateCount = aggregate.count ?? 0;
|
|
11815
11959
|
if (window.search) {
|
|
11816
11960
|
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
@@ -11822,7 +11966,7 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11822
11966
|
};
|
|
11823
11967
|
}
|
|
11824
11968
|
const result = await crmDb.query({
|
|
11825
|
-
[
|
|
11969
|
+
[import_crm21.CRM_NS.record]: { $: {
|
|
11826
11970
|
where,
|
|
11827
11971
|
order: { [window.sort.field]: window.sort.dir },
|
|
11828
11972
|
limit: window.limit,
|
|
@@ -11830,19 +11974,19 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11830
11974
|
} }
|
|
11831
11975
|
});
|
|
11832
11976
|
return {
|
|
11833
|
-
records: (result[
|
|
11977
|
+
records: (result[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord),
|
|
11834
11978
|
total: candidateCount
|
|
11835
11979
|
};
|
|
11836
11980
|
}
|
|
11837
11981
|
|
|
11838
11982
|
// src/network-mutation-receipt.ts
|
|
11839
|
-
var
|
|
11983
|
+
var import_crm22 = require("@odla-ai/crm");
|
|
11840
11984
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
11841
11985
|
async function priorMutation(db, id6) {
|
|
11842
11986
|
const result = await db.query({
|
|
11843
|
-
[
|
|
11987
|
+
[import_crm22.CRM_NS.activity]: { $: { where: { id: id6 }, limit: 1 } }
|
|
11844
11988
|
});
|
|
11845
|
-
const row2 = (result[
|
|
11989
|
+
const row2 = (result[import_crm22.CRM_NS.activity] ?? [])[0];
|
|
11846
11990
|
const meta = row2?.meta;
|
|
11847
11991
|
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
11848
11992
|
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint, pending: receipt.state === "pending", failed: receipt.state === "failed" } : null;
|
|
@@ -11866,7 +12010,7 @@ function mutationError(error) {
|
|
|
11866
12010
|
if (error?.code === "transact_guard_failed") {
|
|
11867
12011
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
11868
12012
|
}
|
|
11869
|
-
if (error instanceof
|
|
12013
|
+
if (error instanceof import_crm23.CrmInputError) {
|
|
11870
12014
|
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
11871
12015
|
}
|
|
11872
12016
|
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
@@ -11921,7 +12065,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11921
12065
|
}
|
|
11922
12066
|
if (prior?.failed) return json({ error: "this operation was not applied; reload before choosing a new action", code: "action_failed" }, 409);
|
|
11923
12067
|
if (prior && !prior.pending) {
|
|
11924
|
-
const replayed = await (0,
|
|
12068
|
+
const replayed = await (0, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
11925
12069
|
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
11926
12070
|
const result = {
|
|
11927
12071
|
version: 1,
|
|
@@ -11931,7 +12075,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11931
12075
|
};
|
|
11932
12076
|
return json(result);
|
|
11933
12077
|
}
|
|
11934
|
-
const record4 = await (0,
|
|
12078
|
+
const record4 = await (0, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
11935
12079
|
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
11936
12080
|
if (!prior?.pending && record4.updatedAt !== input.expectedRevision) {
|
|
11937
12081
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
@@ -11994,7 +12138,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11994
12138
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
11995
12139
|
});
|
|
11996
12140
|
}
|
|
11997
|
-
const updated = hasFields ? await (0,
|
|
12141
|
+
const updated = hasFields ? await (0, import_crm23.updateRecord)(deps, {
|
|
11998
12142
|
id: record4.id,
|
|
11999
12143
|
input: fields,
|
|
12000
12144
|
mutationId: activityId,
|
|
@@ -12005,7 +12149,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12005
12149
|
authorId: `network:${verified.sender}`,
|
|
12006
12150
|
meta: auditMeta
|
|
12007
12151
|
}
|
|
12008
|
-
}) : await (0,
|
|
12152
|
+
}) : await (0, import_crm23.setStage)(deps, {
|
|
12009
12153
|
id: record4.id,
|
|
12010
12154
|
to: input.stage,
|
|
12011
12155
|
authorId: `network:${verified.sender}`,
|
|
@@ -12032,7 +12176,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12032
12176
|
const recordId = url.searchParams.get("recordId");
|
|
12033
12177
|
if (recordId !== null) {
|
|
12034
12178
|
if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
|
|
12035
|
-
const record4 = await (0,
|
|
12179
|
+
const record4 = await (0, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
12036
12180
|
if (!record4 || record4.type !== type || record4.archived) return json({ error: "record not found" }, 404);
|
|
12037
12181
|
return json({
|
|
12038
12182
|
version: 1,
|
|
@@ -12282,13 +12426,13 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12282
12426
|
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
12283
12427
|
const got = await gate8(req, env, ctx);
|
|
12284
12428
|
if ("response" in got) return got.response;
|
|
12285
|
-
const
|
|
12286
|
-
if (!
|
|
12429
|
+
const identity2 = noteIdentity(url, ctx);
|
|
12430
|
+
if (!identity2) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
12287
12431
|
if (req.method === "GET") {
|
|
12288
12432
|
const result = await got.db.query({
|
|
12289
12433
|
networkNotes: {
|
|
12290
12434
|
$: {
|
|
12291
|
-
where: { recordKey:
|
|
12435
|
+
where: { recordKey: identity2.recordKey },
|
|
12292
12436
|
order: { createdAt: "desc" },
|
|
12293
12437
|
limit: 100
|
|
12294
12438
|
}
|
|
@@ -12309,9 +12453,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12309
12453
|
const id6 = crypto.randomUUID();
|
|
12310
12454
|
const note = {
|
|
12311
12455
|
id: id6,
|
|
12312
|
-
targetId:
|
|
12313
|
-
recordType:
|
|
12314
|
-
recordId:
|
|
12456
|
+
targetId: identity2.targetId,
|
|
12457
|
+
recordType: identity2.recordType,
|
|
12458
|
+
recordId: identity2.recordId,
|
|
12315
12459
|
body,
|
|
12316
12460
|
authorId: got.user.userId,
|
|
12317
12461
|
...got.user.email ? { authorEmail: got.user.email } : {},
|
|
@@ -12321,13 +12465,13 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12321
12465
|
t: "update",
|
|
12322
12466
|
ns: "networkNotes",
|
|
12323
12467
|
id: id6,
|
|
12324
|
-
attrs: { ...note, recordKey:
|
|
12468
|
+
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12325
12469
|
}]);
|
|
12326
12470
|
return json({ note }, 201);
|
|
12327
12471
|
};
|
|
12328
12472
|
|
|
12329
12473
|
// src/worker-routes-network-shared-notes.ts
|
|
12330
|
-
var
|
|
12474
|
+
var import_crm24 = require("@odla-ai/crm");
|
|
12331
12475
|
var import_db12 = require("@odla-ai/db");
|
|
12332
12476
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
12333
12477
|
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
@@ -12358,7 +12502,7 @@ function sharedNote(activity, sourceId) {
|
|
|
12358
12502
|
}
|
|
12359
12503
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
12360
12504
|
const result = await db.query({
|
|
12361
|
-
[
|
|
12505
|
+
[import_crm24.CRM_NS.activity]: {
|
|
12362
12506
|
$: {
|
|
12363
12507
|
where: {
|
|
12364
12508
|
and: [
|
|
@@ -12371,7 +12515,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
12371
12515
|
}
|
|
12372
12516
|
}
|
|
12373
12517
|
});
|
|
12374
|
-
return (result[
|
|
12518
|
+
return (result[import_crm24.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
12375
12519
|
const note = sharedNote(activity, sourceId);
|
|
12376
12520
|
return note ? [note] : [];
|
|
12377
12521
|
});
|
|
@@ -12399,7 +12543,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12399
12543
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
12400
12544
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
12401
12545
|
}
|
|
12402
|
-
const record4 = await (0,
|
|
12546
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
12403
12547
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
12404
12548
|
if (req.method === "GET") {
|
|
12405
12549
|
return json({
|
|
@@ -12421,7 +12565,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12421
12565
|
}
|
|
12422
12566
|
const createdAt = Date.now();
|
|
12423
12567
|
const id6 = `network:${verified.sender}:${mutationId}`;
|
|
12424
|
-
const result = await (0,
|
|
12568
|
+
const result = await (0, import_crm24.addActivity)(
|
|
12425
12569
|
{
|
|
12426
12570
|
crm: ctx.chapter.crm,
|
|
12427
12571
|
db,
|
|
@@ -12583,8 +12727,8 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
12583
12727
|
}
|
|
12584
12728
|
}
|
|
12585
12729
|
const seen = /* @__PURE__ */ new Set();
|
|
12586
|
-
for (const
|
|
12587
|
-
const key3 = `${
|
|
12730
|
+
for (const identity2 of identities) {
|
|
12731
|
+
const key3 = `${identity2.kind}:${identity2.value}`;
|
|
12588
12732
|
if (seen.has(key3)) errors.push("duplicate identity in import");
|
|
12589
12733
|
seen.add(key3);
|
|
12590
12734
|
}
|
|
@@ -12640,8 +12784,8 @@ async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
|
12640
12784
|
async function networkChapterPersonSourceKey(chapterId, chapterPersonId) {
|
|
12641
12785
|
return `chapter_source_${await sha256(`${chapterId}\0${chapterPersonId}`)}`;
|
|
12642
12786
|
}
|
|
12643
|
-
function projectNetworkContactPolicy(
|
|
12644
|
-
return links.map((link) => ({ ...link, contactPolicy: { ...
|
|
12787
|
+
function projectNetworkContactPolicy(policy2, links) {
|
|
12788
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy2 } }));
|
|
12645
12789
|
}
|
|
12646
12790
|
function canonicalNetworkPersonId(personId, persons) {
|
|
12647
12791
|
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
@@ -12947,11 +13091,11 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
12947
13091
|
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
12948
13092
|
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
12949
13093
|
}
|
|
12950
|
-
const
|
|
12951
|
-
const next = { ...person, contactPolicy:
|
|
13094
|
+
const policy2 = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
13095
|
+
const next = { ...person, contactPolicy: policy2, revision: person.revision + 1, updatedAt: now };
|
|
12952
13096
|
await db.transact([
|
|
12953
13097
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
12954
|
-
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed:
|
|
13098
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy2.marketingSuppressed, discretionarySuppressed: policy2.discretionarySuppressed }, { personId: person.id })
|
|
12955
13099
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
12956
13100
|
return next;
|
|
12957
13101
|
}
|
|
@@ -13076,13 +13220,13 @@ async function derivedMutationId(batchMutationId, part) {
|
|
|
13076
13220
|
return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
|
|
13077
13221
|
}
|
|
13078
13222
|
function matchingEvent(events, mutationId, kind) {
|
|
13079
|
-
return events.find((
|
|
13223
|
+
return events.find((event2) => event2.mutationId === mutationId && event2.kind === kind);
|
|
13080
13224
|
}
|
|
13081
|
-
function eventString(
|
|
13082
|
-
return typeof
|
|
13225
|
+
function eventString(event2, key3) {
|
|
13226
|
+
return typeof event2.detail[key3] === "string" ? String(event2.detail[key3]) : "";
|
|
13083
13227
|
}
|
|
13084
|
-
function eventCounts(
|
|
13085
|
-
const value =
|
|
13228
|
+
function eventCounts(event2) {
|
|
13229
|
+
const value = event2.detail.counts;
|
|
13086
13230
|
return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
|
|
13087
13231
|
}
|
|
13088
13232
|
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
@@ -13209,11 +13353,11 @@ var redactedNetworkPersonLink = async (link) => ({
|
|
|
13209
13353
|
state: link.state,
|
|
13210
13354
|
createdAt: link.createdAt
|
|
13211
13355
|
});
|
|
13212
|
-
var safeNetworkContactPolicy = (
|
|
13213
|
-
marketingSuppressed:
|
|
13214
|
-
discretionarySuppressed:
|
|
13215
|
-
hasReason: Boolean(
|
|
13216
|
-
updatedAt:
|
|
13356
|
+
var safeNetworkContactPolicy = (policy2) => ({
|
|
13357
|
+
marketingSuppressed: policy2.marketingSuppressed,
|
|
13358
|
+
discretionarySuppressed: policy2.discretionarySuppressed,
|
|
13359
|
+
hasReason: Boolean(policy2.reason),
|
|
13360
|
+
updatedAt: policy2.updatedAt
|
|
13217
13361
|
});
|
|
13218
13362
|
var safeNetworkPersonAlias = async (alias) => ({
|
|
13219
13363
|
id: alias.id,
|
|
@@ -13268,14 +13412,14 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13268
13412
|
aliases,
|
|
13269
13413
|
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
13270
13414
|
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
13271
|
-
recentEvents: state.events.slice(0, 100).map((
|
|
13272
|
-
id:
|
|
13273
|
-
kind:
|
|
13274
|
-
personId:
|
|
13275
|
-
aliasId:
|
|
13276
|
-
actorId:
|
|
13277
|
-
mutationId:
|
|
13278
|
-
createdAt:
|
|
13415
|
+
recentEvents: state.events.slice(0, 100).map((event2) => ({
|
|
13416
|
+
id: event2.id,
|
|
13417
|
+
kind: event2.kind,
|
|
13418
|
+
personId: event2.personId ?? null,
|
|
13419
|
+
aliasId: event2.aliasId ?? null,
|
|
13420
|
+
actorId: event2.actorId,
|
|
13421
|
+
mutationId: event2.mutationId,
|
|
13422
|
+
createdAt: event2.createdAt
|
|
13279
13423
|
}))
|
|
13280
13424
|
};
|
|
13281
13425
|
}
|
|
@@ -13290,7 +13434,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
13290
13434
|
const target = input.kind === "person" ? state.persons.find((row2) => row2.id === input.id) : state.aliases.find((row2) => row2.id === input.id);
|
|
13291
13435
|
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
13292
13436
|
const ids2 = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
13293
|
-
const
|
|
13437
|
+
const event2 = {
|
|
13294
13438
|
t: "update",
|
|
13295
13439
|
ns: "networkPersonEvents",
|
|
13296
13440
|
id: `network_person_event:${mutationId}`,
|
|
@@ -13304,7 +13448,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
13304
13448
|
createdAt: now
|
|
13305
13449
|
}
|
|
13306
13450
|
};
|
|
13307
|
-
await db.transact([
|
|
13451
|
+
await db.transact([event2], {
|
|
13308
13452
|
mutationId,
|
|
13309
13453
|
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
13310
13454
|
});
|
|
@@ -13888,8 +14032,8 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13888
14032
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
13889
14033
|
}
|
|
13890
14034
|
try {
|
|
13891
|
-
const
|
|
13892
|
-
const fact = normalizeNetworkCommercialStripeEvent(
|
|
14035
|
+
const event2 = await (0, import_stripe12.constructStripeEvent)(body, signature, secret);
|
|
14036
|
+
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
13893
14037
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
13894
14038
|
const result = await ingestNetworkCommercialFact(
|
|
13895
14039
|
db,
|
|
@@ -14000,7 +14144,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
14000
14144
|
};
|
|
14001
14145
|
|
|
14002
14146
|
// src/worker-routes-formation.ts
|
|
14003
|
-
var
|
|
14147
|
+
var import_crm25 = require("@odla-ai/crm");
|
|
14004
14148
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
14005
14149
|
var handleFormation = async (req, url, env, ctx) => {
|
|
14006
14150
|
const formation = ctx.chapter.formation;
|
|
@@ -14052,7 +14196,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14052
14196
|
const db = ctx.makeDb(env);
|
|
14053
14197
|
const id6 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
14054
14198
|
try {
|
|
14055
|
-
const created = await (0,
|
|
14199
|
+
const created = await (0, import_crm25.createRecord)(
|
|
14056
14200
|
{
|
|
14057
14201
|
crm: ctx.chapter.crm,
|
|
14058
14202
|
db,
|
|
@@ -15488,7 +15632,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15488
15632
|
};
|
|
15489
15633
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
15490
15634
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
15491
|
-
const
|
|
15635
|
+
const checkout2 = {
|
|
15492
15636
|
...target,
|
|
15493
15637
|
priceId: peer.seatPriceId,
|
|
15494
15638
|
recipientName: String(input.recipientName),
|
|
@@ -15496,7 +15640,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15496
15640
|
purchaserEmail: String(input.purchaserEmail),
|
|
15497
15641
|
purchaserUserId: String(input.purchaserUserId)
|
|
15498
15642
|
};
|
|
15499
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
15643
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
15500
15644
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
15501
15645
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
15502
15646
|
if (action === "seat-decline") {
|
|
@@ -15512,7 +15656,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15512
15656
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
15513
15657
|
}
|
|
15514
15658
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
15515
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
15659
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
15516
15660
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
15517
15661
|
if (action === "seat-payments") {
|
|
15518
15662
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -15583,9 +15727,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15583
15727
|
const identities = /* @__PURE__ */ new Set();
|
|
15584
15728
|
for (const peer of peers) {
|
|
15585
15729
|
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");
|
|
15586
|
-
const
|
|
15587
|
-
if (identities.has(
|
|
15588
|
-
identities.add(
|
|
15730
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
15731
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
15732
|
+
identities.add(identity2);
|
|
15589
15733
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
15590
15734
|
}
|
|
15591
15735
|
return async (req) => {
|
|
@@ -16189,6 +16333,9 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
16189
16333
|
}
|
|
16190
16334
|
}
|
|
16191
16335
|
|
|
16336
|
+
// src/membership-chapter-delivery.ts
|
|
16337
|
+
var import_crm26 = require("@odla-ai/crm");
|
|
16338
|
+
|
|
16192
16339
|
// src/invitation-test-delivery.ts
|
|
16193
16340
|
function invitationDelivery(env, recipientEmail) {
|
|
16194
16341
|
const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
|
|
@@ -16251,16 +16398,13 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
16251
16398
|
const join = new URL(applicationPath, origin);
|
|
16252
16399
|
join.searchParams.set("seat", effect.seatId);
|
|
16253
16400
|
join.searchParams.set("tier", effect.skuId);
|
|
16254
|
-
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
|
|
16255
|
-
enabled: true,
|
|
16256
|
-
subject: "{{purchaserName}} invited you to {{chapterName}}",
|
|
16257
|
-
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."
|
|
16258
|
-
} };
|
|
16259
16401
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
16260
|
-
if (!template.text.includes("{{purchaserName}}"))
|
|
16261
|
-
...
|
|
16262
|
-
|
|
16263
|
-
|
|
16402
|
+
if (template && !(0, import_crm26.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
|
|
16403
|
+
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
|
|
16404
|
+
...template,
|
|
16405
|
+
text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
|
|
16406
|
+
} };
|
|
16407
|
+
}
|
|
16264
16408
|
stage = "invitation_delivery";
|
|
16265
16409
|
const result = await sendTemplated({
|
|
16266
16410
|
db,
|
|
@@ -16280,7 +16424,8 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
16280
16424
|
joinUrl: join.href
|
|
16281
16425
|
},
|
|
16282
16426
|
dedupeKey: `membership-effect:${effect.effectId}`,
|
|
16283
|
-
applicationId: input.applicationId
|
|
16427
|
+
applicationId: input.applicationId,
|
|
16428
|
+
tierId: effect.skuId
|
|
16284
16429
|
});
|
|
16285
16430
|
if (!result.sent) throw new Error("invitation delivery unconfirmed");
|
|
16286
16431
|
} else {
|