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