@odla-ai/chapter 0.54.1 → 0.55.0
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 +46 -3
- package/dist/{chunk-SPEL4OGE.js → chunk-VEOGTSVX.js} +507 -359
- package/dist/chunk-VEOGTSVX.js.map +1 -0
- 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 +795 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -19
- package/dist/index.d.ts +97 -19
- package/dist/index.js +776 -374
- 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 +2 -2
- package/dist/ui/member/index.d.ts +9 -2
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +1680 -1145
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +37 -3
- package/dist/worker/index.d.ts +37 -3
- package/dist/worker/index.js +1638 -1103
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-SPEL4OGE.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(src_exports, {
|
|
|
27
27
|
BUILDER_LIBRARY_MANIFEST_PATH: () => BUILDER_LIBRARY_MANIFEST_PATH,
|
|
28
28
|
BUILDER_LIBRARY_PROMOTE_PATH: () => BUILDER_LIBRARY_PROMOTE_PATH,
|
|
29
29
|
BUILDER_LIBRARY_SCHEMA_VERSION: () => BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
30
|
+
CHAPTER_EMAIL_EVENTS: () => CHAPTER_EMAIL_EVENTS,
|
|
30
31
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
31
32
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
32
33
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
@@ -83,6 +84,7 @@ __export(src_exports, {
|
|
|
83
84
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
84
85
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
85
86
|
chapterDb: () => chapterDb,
|
|
87
|
+
chapterEmailEvents: () => chapterEmailEvents,
|
|
86
88
|
chapterFlowFieldContract: () => chapterFlowFieldContract,
|
|
87
89
|
clampArray: () => clampArray,
|
|
88
90
|
classifyMemberStripeEvent: () => classifyMemberStripeEvent,
|
|
@@ -251,7 +253,7 @@ __export(src_exports, {
|
|
|
251
253
|
slotWindow: () => slotWindow,
|
|
252
254
|
stageIndex: () => stageIndex,
|
|
253
255
|
stripeCall: () => stripeCall,
|
|
254
|
-
stripeForm: () =>
|
|
256
|
+
stripeForm: () => import_stripe11.stripeForm,
|
|
255
257
|
subAnnualCents: () => subAnnualCents,
|
|
256
258
|
submitApplication: () => submitApplication,
|
|
257
259
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
@@ -262,6 +264,7 @@ __export(src_exports, {
|
|
|
262
264
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
263
265
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
264
266
|
validateBuilderLibraryDocument: () => validateBuilderLibraryDocument,
|
|
267
|
+
validateEmailReadiness: () => validateEmailReadiness,
|
|
265
268
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
266
269
|
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
267
270
|
validateMemberSystemAuthority: () => validateMemberSystemAuthority,
|
|
@@ -272,7 +275,7 @@ __export(src_exports, {
|
|
|
272
275
|
verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
|
|
273
276
|
verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
|
|
274
277
|
verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
|
|
275
|
-
verifyStripeSignature: () =>
|
|
278
|
+
verifyStripeSignature: () => import_stripe11.verifyStripeSignature,
|
|
276
279
|
webhookMutationId: () => webhookMutationId
|
|
277
280
|
});
|
|
278
281
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -401,8 +404,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
401
404
|
if (app.requireDisclaimerAck && !acked) {
|
|
402
405
|
return { ok: false, error: "disclaimerAck is required" };
|
|
403
406
|
}
|
|
404
|
-
const
|
|
405
|
-
const row2 = { id:
|
|
407
|
+
const id11 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
408
|
+
const row2 = { id: id11, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
406
409
|
for (const f of [...app.required, ...app.optional]) {
|
|
407
410
|
if (fieldStates[f]?.visible === false) continue;
|
|
408
411
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -436,12 +439,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
436
439
|
}
|
|
437
440
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
438
441
|
const ops = [
|
|
439
|
-
{ t: "update", ns: "applications", id:
|
|
442
|
+
{ t: "update", ns: "applications", id: id11, attrs: row2 }
|
|
440
443
|
];
|
|
441
444
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
442
445
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
443
446
|
consumedAt: opts.now,
|
|
444
|
-
consumedApplicationId:
|
|
447
|
+
consumedApplicationId: id11
|
|
445
448
|
} });
|
|
446
449
|
}
|
|
447
450
|
try {
|
|
@@ -449,7 +452,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
449
452
|
ops,
|
|
450
453
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
451
454
|
);
|
|
452
|
-
return { ok: true, id:
|
|
455
|
+
return { ok: true, id: id11, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
453
456
|
} catch (error) {
|
|
454
457
|
if (error.code === "unique_violation") return {
|
|
455
458
|
ok: false,
|
|
@@ -1380,6 +1383,7 @@ Warmly,
|
|
|
1380
1383
|
${name}`;
|
|
1381
1384
|
return {
|
|
1382
1385
|
adminNotification: {
|
|
1386
|
+
enabled: true,
|
|
1383
1387
|
subject: `New application: {{firstName}} {{lastName}}`,
|
|
1384
1388
|
text: `A new application came in for ${name}.
|
|
1385
1389
|
|
|
@@ -1390,18 +1394,21 @@ Tier: {{tier}}
|
|
|
1390
1394
|
Review it: {{adminUrl}}`
|
|
1391
1395
|
},
|
|
1392
1396
|
submitConfirmation: {
|
|
1397
|
+
enabled: true,
|
|
1393
1398
|
subject: `We received your application to ${name}`,
|
|
1394
1399
|
text: `Hi {{firstName}},
|
|
1395
1400
|
|
|
1396
1401
|
Thanks for applying to ${name}. {{submitNextStep}}${sign}`
|
|
1397
1402
|
},
|
|
1398
1403
|
paymentConfirmation: {
|
|
1404
|
+
enabled: true,
|
|
1399
1405
|
subject: `Welcome to ${name}`,
|
|
1400
1406
|
text: `Hi {{firstName}},
|
|
1401
1407
|
|
|
1402
1408
|
Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}}${sign}`
|
|
1403
1409
|
},
|
|
1404
1410
|
prepEmail: {
|
|
1411
|
+
enabled: true,
|
|
1405
1412
|
subject: `Your ${name} intro call`,
|
|
1406
1413
|
text: `Hi {{firstName}},
|
|
1407
1414
|
|
|
@@ -1410,18 +1417,19 @@ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startA
|
|
|
1410
1417
|
Join here: {{meetingLink}}${sign}`
|
|
1411
1418
|
},
|
|
1412
1419
|
onboardingInvite: {
|
|
1420
|
+
enabled: true,
|
|
1413
1421
|
subject: `You're in, ${name}`,
|
|
1414
1422
|
text: `Hi {{firstName}},
|
|
1415
1423
|
|
|
1416
1424
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
1417
1425
|
},
|
|
1418
|
-
seatAccepted: SEAT_ACCEPTED_TEMPLATE
|
|
1426
|
+
seatAccepted: { ...SEAT_ACCEPTED_TEMPLATE, enabled: true }
|
|
1419
1427
|
};
|
|
1420
1428
|
}
|
|
1421
1429
|
function buildGroupSeed(config) {
|
|
1422
1430
|
const prices = config.prices;
|
|
1423
1431
|
const emails = config.emails;
|
|
1424
|
-
const
|
|
1432
|
+
const policy2 = config.policy ?? {};
|
|
1425
1433
|
const scheduling = {
|
|
1426
1434
|
...DEFAULT_SCHEDULING,
|
|
1427
1435
|
...config.scheduling ?? {},
|
|
@@ -1436,17 +1444,17 @@ function buildGroupSeed(config) {
|
|
|
1436
1444
|
interval: prices?.interval ?? "year",
|
|
1437
1445
|
notificationEmail: emails?.notificationEmail ?? "",
|
|
1438
1446
|
replyTo: emails?.replyTo ?? emails?.notificationEmail ?? "",
|
|
1439
|
-
disclaimerText:
|
|
1440
|
-
refundPolicyText:
|
|
1441
|
-
merchantDisclosureText:
|
|
1442
|
-
trustCopy:
|
|
1447
|
+
disclaimerText: policy2.disclaimerText ?? "",
|
|
1448
|
+
refundPolicyText: policy2.refundPolicyText ?? "",
|
|
1449
|
+
merchantDisclosureText: policy2.merchantDisclosureText ?? "",
|
|
1450
|
+
trustCopy: policy2.trustCopy ?? "",
|
|
1443
1451
|
emailTemplates: emails?.templates ?? defaultEmailTemplates(config.name),
|
|
1444
1452
|
schedulingJson: scheduling
|
|
1445
1453
|
};
|
|
1446
1454
|
if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
|
|
1447
1455
|
if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
|
|
1448
|
-
if (
|
|
1449
|
-
if (
|
|
1456
|
+
if (policy2.commitmentText) row2.commitmentText = policy2.commitmentText;
|
|
1457
|
+
if (policy2.normsText) row2.normsText = policy2.normsText;
|
|
1450
1458
|
return row2;
|
|
1451
1459
|
}
|
|
1452
1460
|
|
|
@@ -1979,9 +1987,9 @@ var APPLICATION_LABELS = {
|
|
|
1979
1987
|
phone: "Phone",
|
|
1980
1988
|
state: "State"
|
|
1981
1989
|
};
|
|
1982
|
-
function humanize(
|
|
1983
|
-
const text6 =
|
|
1984
|
-
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` :
|
|
1990
|
+
function humanize(id11) {
|
|
1991
|
+
const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1992
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id11;
|
|
1985
1993
|
}
|
|
1986
1994
|
function applicationDefs(chapter) {
|
|
1987
1995
|
try {
|
|
@@ -1990,23 +1998,23 @@ function applicationDefs(chapter) {
|
|
|
1990
1998
|
return {};
|
|
1991
1999
|
}
|
|
1992
2000
|
}
|
|
1993
|
-
function applicationType(
|
|
2001
|
+
function applicationType(id11, def) {
|
|
1994
2002
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1995
|
-
return
|
|
2003
|
+
return id11 === "email" ? "email" : id11 === "linkedin" ? "url" : "string";
|
|
1996
2004
|
}
|
|
1997
2005
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1998
2006
|
const defs = applicationDefs(chapter);
|
|
1999
|
-
return [...application.required, ...application.optional].map((
|
|
2000
|
-
const def = defs[
|
|
2001
|
-
const conditions = application.conditions[
|
|
2002
|
-
const type = applicationType(
|
|
2007
|
+
return [...application.required, ...application.optional].map((id11, order2) => {
|
|
2008
|
+
const def = defs[id11];
|
|
2009
|
+
const conditions = application.conditions[id11];
|
|
2010
|
+
const type = applicationType(id11, def);
|
|
2003
2011
|
return {
|
|
2004
|
-
id:
|
|
2005
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
2012
|
+
id: id11,
|
|
2013
|
+
label: def?.label ?? APPLICATION_LABELS[id11] ?? humanize(id11),
|
|
2006
2014
|
type,
|
|
2007
|
-
required: application.required.includes(
|
|
2015
|
+
required: application.required.includes(id11),
|
|
2008
2016
|
order: order2,
|
|
2009
|
-
maxLength: application.maxLen[
|
|
2017
|
+
maxLength: application.maxLen[id11] ?? application.defaultMaxLen,
|
|
2010
2018
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
2011
2019
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
2012
2020
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -2016,17 +2024,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
2016
2024
|
function resolveFormationFlowFields(crm, formation) {
|
|
2017
2025
|
if (!formation.enabled) return [];
|
|
2018
2026
|
const def = crm.type(formation.type);
|
|
2019
|
-
return [...formation.required, ...formation.optional].map((
|
|
2020
|
-
const field2 = def.fields[
|
|
2027
|
+
return [...formation.required, ...formation.optional].map((id11, order2) => {
|
|
2028
|
+
const field2 = def.fields[id11];
|
|
2021
2029
|
return {
|
|
2022
|
-
id:
|
|
2023
|
-
label: field2.label ?? humanize(
|
|
2030
|
+
id: id11,
|
|
2031
|
+
label: field2.label ?? humanize(id11),
|
|
2024
2032
|
type: field2.type,
|
|
2025
|
-
required: formation.required.includes(
|
|
2033
|
+
required: formation.required.includes(id11),
|
|
2026
2034
|
order: order2,
|
|
2027
|
-
maxLength: formation.maxLen[
|
|
2035
|
+
maxLength: formation.maxLen[id11] ?? formation.defaultMaxLen,
|
|
2028
2036
|
...field2.options ? { options: [...field2.options] } : {},
|
|
2029
|
-
...
|
|
2037
|
+
...id11 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
2030
2038
|
};
|
|
2031
2039
|
});
|
|
2032
2040
|
}
|
|
@@ -2330,12 +2338,12 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
|
|
|
2330
2338
|
if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
|
|
2331
2339
|
const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
|
|
2332
2340
|
if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
|
|
2333
|
-
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((
|
|
2341
|
+
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id11) => !SLUG2.test(id11))) {
|
|
2334
2342
|
throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
|
|
2335
2343
|
}
|
|
2336
|
-
const unknown = chapterIds.filter((
|
|
2344
|
+
const unknown = chapterIds.filter((id11) => !targetIds.includes(id11));
|
|
2337
2345
|
if (unknown.length) {
|
|
2338
|
-
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((
|
|
2346
|
+
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id11) => `"${id11}"`).join(", ")} are not configured network targets`);
|
|
2339
2347
|
}
|
|
2340
2348
|
return { mode: "shadow", chapterIds };
|
|
2341
2349
|
}
|
|
@@ -2458,9 +2466,9 @@ function isResolvedCrm(x) {
|
|
|
2458
2466
|
}
|
|
2459
2467
|
function defineChapter(config) {
|
|
2460
2468
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
2461
|
-
const { id:
|
|
2462
|
-
if (typeof
|
|
2463
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
2469
|
+
const { id: id11, name } = config;
|
|
2470
|
+
if (typeof id11 !== "string" || !SLUG7.test(id11)) {
|
|
2471
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id11)}`);
|
|
2464
2472
|
}
|
|
2465
2473
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
2466
2474
|
const mode = config.mode ?? "chapter";
|
|
@@ -2567,7 +2575,7 @@ function defineChapter(config) {
|
|
|
2567
2575
|
};
|
|
2568
2576
|
const chapter = {
|
|
2569
2577
|
config,
|
|
2570
|
-
id:
|
|
2578
|
+
id: id11,
|
|
2571
2579
|
name,
|
|
2572
2580
|
mode,
|
|
2573
2581
|
brand,
|
|
@@ -2755,13 +2763,13 @@ function firstPrice(object2) {
|
|
|
2755
2763
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2756
2764
|
}
|
|
2757
2765
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2758
|
-
const
|
|
2759
|
-
const eventId = providerId(
|
|
2760
|
-
const eventType = text(
|
|
2761
|
-
const object2 = raw(raw(
|
|
2766
|
+
const event2 = raw(input);
|
|
2767
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
2768
|
+
const eventType = text(event2.type) ?? "";
|
|
2769
|
+
const object2 = raw(raw(event2.data).object);
|
|
2762
2770
|
const objectId = providerId(object2.id) ?? "";
|
|
2763
2771
|
const candidates = metadataCandidates(object2);
|
|
2764
|
-
const
|
|
2772
|
+
const identity2 = {};
|
|
2765
2773
|
const identityKeys2 = {
|
|
2766
2774
|
appId: "odlaAppId",
|
|
2767
2775
|
chapterId: "chapterId",
|
|
@@ -2771,7 +2779,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2771
2779
|
};
|
|
2772
2780
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
2773
2781
|
const value = metadataValue(candidates, source);
|
|
2774
|
-
if (value)
|
|
2782
|
+
if (value) identity2[target] = value;
|
|
2775
2783
|
}
|
|
2776
2784
|
let objectKind = "ignored";
|
|
2777
2785
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -2793,11 +2801,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2793
2801
|
eventId,
|
|
2794
2802
|
digest,
|
|
2795
2803
|
eventType,
|
|
2796
|
-
eventCreated: integer(
|
|
2797
|
-
livemode:
|
|
2804
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
2805
|
+
livemode: event2.livemode === true,
|
|
2798
2806
|
objectKind,
|
|
2799
2807
|
objectId,
|
|
2800
|
-
identity,
|
|
2808
|
+
identity: identity2,
|
|
2801
2809
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2802
2810
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2803
2811
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -2982,14 +2990,14 @@ function facts(snapshots) {
|
|
|
2982
2990
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2983
2991
|
];
|
|
2984
2992
|
return groups2.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2985
|
-
const
|
|
2993
|
+
const id11 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2986
2994
|
return normalizeNetworkCommercialStripeEvent({
|
|
2987
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
2995
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id11}`,
|
|
2988
2996
|
type: eventType,
|
|
2989
2997
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2990
2998
|
livemode: object2.livemode === true,
|
|
2991
2999
|
data: { object: object2 }
|
|
2992
|
-
}, `stripe-snapshot:${eventType}:${
|
|
3000
|
+
}, `stripe-snapshot:${eventType}:${id11}`);
|
|
2993
3001
|
}));
|
|
2994
3002
|
}
|
|
2995
3003
|
var selected = (row2) => ({
|
|
@@ -3644,15 +3652,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
3644
3652
|
// src/membership-offer.ts
|
|
3645
3653
|
function createMembershipCommercialOffer(input) {
|
|
3646
3654
|
const offering = membershipOfferingVersion(input.offering);
|
|
3647
|
-
const
|
|
3655
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
3648
3656
|
const now = input.now;
|
|
3649
3657
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
3650
3658
|
throw new TypeError("invalid offer clock or founding count");
|
|
3651
3659
|
}
|
|
3652
3660
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
3653
|
-
if (
|
|
3654
|
-
const founding = Boolean(
|
|
3655
|
-
const earned = founding ?
|
|
3661
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
3662
|
+
const founding = Boolean(policy2 && policy2.status === "published" && offering.basePriceCents > 0 && policy2.cohortEligibleSkuIds.includes(offering.baseSkuId) && now >= policy2.qualifyingStartsAt && now < policy2.qualifyingEndsAt && input.approvedFoundingCount < policy2.softTarget);
|
|
3663
|
+
const earned = founding ? policy2 : null;
|
|
3656
3664
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
3657
3665
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
3658
3666
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -3698,8 +3706,8 @@ async function hash(value) {
|
|
|
3698
3706
|
async function membershipDiscountCouponId(input, policyId) {
|
|
3699
3707
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
3700
3708
|
}
|
|
3701
|
-
async function load(db, ns,
|
|
3702
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
3709
|
+
async function load(db, ns, id11) {
|
|
3710
|
+
return (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3703
3711
|
}
|
|
3704
3712
|
async function membershipOfferIdForRequest(input) {
|
|
3705
3713
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -3744,15 +3752,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
3744
3752
|
}
|
|
3745
3753
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3746
3754
|
const state = await authoritativeState(db, input);
|
|
3747
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3748
|
-
const approvedFoundingCount =
|
|
3755
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
3756
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3749
3757
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3750
3758
|
const contract = createMembershipCommercialOffer({
|
|
3751
3759
|
offering,
|
|
3752
|
-
discountPolicy:
|
|
3760
|
+
discountPolicy: policy2,
|
|
3753
3761
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3754
3762
|
now: input.now,
|
|
3755
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3763
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3756
3764
|
});
|
|
3757
3765
|
const discount = contract.billingTerms.discount;
|
|
3758
3766
|
return {
|
|
@@ -3781,12 +3789,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
3781
3789
|
});
|
|
3782
3790
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3783
3791
|
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3784
|
-
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
3792
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
|
|
3785
3793
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3786
3794
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3787
|
-
const
|
|
3788
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
3789
|
-
return { offering, policy };
|
|
3795
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3796
|
+
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy2 && policies.filter((item) => item.version === policy2.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3797
|
+
return { offering, policy: policy2 };
|
|
3790
3798
|
}
|
|
3791
3799
|
async function issueMembershipCommercialOffer(db, input) {
|
|
3792
3800
|
validate(input);
|
|
@@ -3794,15 +3802,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3794
3802
|
const state = await authoritativeState(db, input);
|
|
3795
3803
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3796
3804
|
if (existing) return restore(existing, input);
|
|
3797
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3798
|
-
const approvedFoundingCount =
|
|
3805
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
3806
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3799
3807
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3800
3808
|
const contract = createMembershipCommercialOffer({
|
|
3801
3809
|
offering,
|
|
3802
|
-
discountPolicy:
|
|
3810
|
+
discountPolicy: policy2,
|
|
3803
3811
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3804
3812
|
now: input.now,
|
|
3805
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3813
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3806
3814
|
});
|
|
3807
3815
|
const value = {
|
|
3808
3816
|
id: input.offerId,
|
|
@@ -3836,20 +3844,20 @@ var import_stripe2 = require("@odla-ai/stripe");
|
|
|
3836
3844
|
|
|
3837
3845
|
// src/commercial-evidence-guard.ts
|
|
3838
3846
|
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3839
|
-
const ns = "networkCommercialExceptions",
|
|
3840
|
-
const row2 = (await db.query({ [ns]: { $: { where: { id:
|
|
3841
|
-
if (!row2) return { ns, id:
|
|
3847
|
+
const ns = "networkCommercialExceptions", id11 = `network-commercial-exception:${eventId}:collision`;
|
|
3848
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3849
|
+
if (!row2) return { ns, id: id11, exists: false };
|
|
3842
3850
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3843
3851
|
throw Object.assign(new Error(code), { code });
|
|
3844
3852
|
}
|
|
3845
|
-
return { ns, id:
|
|
3853
|
+
return { ns, id: id11, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3846
3854
|
}
|
|
3847
3855
|
|
|
3848
3856
|
// src/membership-authority-state.ts
|
|
3849
3857
|
var membershipFail = (code) => {
|
|
3850
3858
|
throw Object.assign(new Error(code), { code });
|
|
3851
3859
|
};
|
|
3852
|
-
var membershipLoad = async (db, ns,
|
|
3860
|
+
var membershipLoad = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3853
3861
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3854
3862
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3855
3863
|
function requireMembershipScope(row2, input) {
|
|
@@ -3860,18 +3868,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
3860
3868
|
if (!row2 || row2.mode !== "authoritative" || row2.chapterId !== input.chapterId || row2.environment !== input.environment || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1) return membershipFail("membership_authority_unavailable");
|
|
3861
3869
|
return row2;
|
|
3862
3870
|
}
|
|
3863
|
-
var membershipUpdate = (ns,
|
|
3871
|
+
var membershipUpdate = (ns, id11, attrs) => ({ t: "update", ns, id: id11, attrs });
|
|
3864
3872
|
var membershipClock = (now) => {
|
|
3865
3873
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3866
3874
|
};
|
|
3867
3875
|
|
|
3868
3876
|
// src/membership-restart-payment.ts
|
|
3869
3877
|
async function membershipRestartPayment(db, target, member, now) {
|
|
3870
|
-
const
|
|
3878
|
+
const id11 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id11);
|
|
3871
3879
|
requireMembershipScope(restart, target);
|
|
3872
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
3880
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
3873
3881
|
requireMembershipScope(entitlement, target);
|
|
3874
|
-
if (entitlement.membershipId !==
|
|
3882
|
+
if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3875
3883
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3876
3884
|
return {
|
|
3877
3885
|
restartId: String(restart.restartId),
|
|
@@ -3888,49 +3896,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
3888
3896
|
graceEndsAt: 0
|
|
3889
3897
|
},
|
|
3890
3898
|
ops: [
|
|
3891
|
-
membershipUpdate("membershipRestart",
|
|
3892
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
3899
|
+
membershipUpdate("membershipRestart", id11, { status: "complete" }),
|
|
3900
|
+
{ t: "retract", ns: "networkMembership", id: id11, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3893
3901
|
],
|
|
3894
3902
|
guards: [
|
|
3895
|
-
{ ns: "networkMembership", id:
|
|
3896
|
-
{ ns: "membershipRestart", id:
|
|
3903
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3904
|
+
{ ns: "membershipRestart", id: id11, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3897
3905
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3898
3906
|
]
|
|
3899
3907
|
};
|
|
3900
3908
|
}
|
|
3901
3909
|
|
|
3910
|
+
// src/checkout-named-seat.ts
|
|
3911
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
3912
|
+
const row2 = value;
|
|
3913
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
3914
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
3915
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
3916
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
|
|
3917
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
3918
|
+
}
|
|
3919
|
+
return { recipientName: name, recipientEmail: email };
|
|
3920
|
+
}
|
|
3921
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
3922
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
3923
|
+
target.appId,
|
|
3924
|
+
target.chapterId,
|
|
3925
|
+
target.environment,
|
|
3926
|
+
target.runtime,
|
|
3927
|
+
email
|
|
3928
|
+
])));
|
|
3929
|
+
return {
|
|
3930
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
3931
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
3932
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3902
3936
|
// src/membership-checkout-binding.ts
|
|
3903
3937
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
3904
3938
|
var reject2 = (code) => {
|
|
3905
3939
|
throw Object.assign(new Error(code), { code });
|
|
3906
3940
|
};
|
|
3907
|
-
var load2 = async (db, ns,
|
|
3941
|
+
var load2 = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3908
3942
|
async function membershipCheckoutBindingId(input) {
|
|
3909
3943
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
3910
3944
|
}
|
|
3911
3945
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
3912
3946
|
const row2 = value;
|
|
3913
3947
|
if (!row2 || typeof row2 !== "object" || row2.id !== await membershipCheckoutBindingId(input) || keys.some((key3) => row2[key3] !== input[key3]) || !Number.isSafeInteger(row2.boundAt) || row2.boundAt < 0) return reject2("membership_checkout_conflict");
|
|
3914
|
-
|
|
3948
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
3949
|
+
if (input.namedSeat) {
|
|
3950
|
+
const expected = {
|
|
3951
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
3952
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
3953
|
+
};
|
|
3954
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
3955
|
+
if (!row2.namedSeat || Object.entries({ ...expected, ...identity2 }).some(([key3, value2]) => row2.namedSeat[key3] !== value2) || !/^price_[A-Za-z0-9_]+$/.test(row2.namedSeat.priceId) || row2.namedSeat.priceId === input.priceId || !Number.isSafeInteger(row2.namedSeat.amountCents) || row2.namedSeat.amountCents < 0) return reject2("membership_checkout_conflict");
|
|
3956
|
+
}
|
|
3957
|
+
return {
|
|
3958
|
+
id: row2.id,
|
|
3959
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
3960
|
+
boundAt: row2.boundAt,
|
|
3961
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
3962
|
+
};
|
|
3915
3963
|
}
|
|
3916
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
3964
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
3917
3965
|
if (!Number.isSafeInteger(now) || now < 0 || !/^[a-f0-9]{64}$/.test(input.offerDigest) || !/^[a-f0-9]{64}$/.test(input.quoteDigest) || !/^price_[A-Za-z0-9_]+$/.test(input.priceId)) return reject2("invalid_membership_checkout");
|
|
3918
3966
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3919
3967
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
3920
3968
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1) return reject2("membership_authority_unavailable");
|
|
3921
|
-
const
|
|
3922
|
-
const existing = await load2(db, "membershipOfferCheckout",
|
|
3969
|
+
const id11 = await membershipCheckoutBindingId(input);
|
|
3970
|
+
const existing = await load2(db, "membershipOfferCheckout", id11);
|
|
3923
3971
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
3924
3972
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3925
3973
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3926
3974
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3927
|
-
const restart = await load2(db, "membershipRestart",
|
|
3928
|
-
|
|
3975
|
+
const restart = await load2(db, "membershipRestart", id11.replace(/^checkout_/, "membership_"));
|
|
3976
|
+
let namedSeat;
|
|
3977
|
+
if (input.namedSeat) {
|
|
3978
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
3979
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
3980
|
+
namedSeat = {
|
|
3981
|
+
...selection,
|
|
3982
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
3983
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
3984
|
+
priceId: seatPriceId,
|
|
3985
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
const receipt = { id: id11, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
3929
3989
|
try {
|
|
3930
|
-
await db.transact([
|
|
3931
|
-
|
|
3990
|
+
await db.transact([
|
|
3991
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id11, attrs: receipt },
|
|
3992
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
3993
|
+
id: namedSeat.recipientClaimId,
|
|
3994
|
+
appId: input.appId,
|
|
3995
|
+
chapterId: input.chapterId,
|
|
3996
|
+
environment: input.environment,
|
|
3997
|
+
runtime: input.runtime,
|
|
3998
|
+
seatId: namedSeat.seatId,
|
|
3999
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
4000
|
+
createdAt: now
|
|
4001
|
+
} }] : []
|
|
4002
|
+
], {
|
|
4003
|
+
mutationId: `membership-checkout:${id11}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3932
4004
|
guards: [
|
|
3933
|
-
{ ns: "membershipOfferCheckout", id:
|
|
4005
|
+
{ ns: "membershipOfferCheckout", id: id11, exists: false },
|
|
4006
|
+
...namedSeat ? [
|
|
4007
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
4008
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
4009
|
+
] : [],
|
|
3934
4010
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
3935
4011
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
3936
4012
|
]
|
|
@@ -3938,11 +4014,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3938
4014
|
} catch (error) {
|
|
3939
4015
|
if (error.code !== "transact_guard_failed") throw error;
|
|
3940
4016
|
}
|
|
3941
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
4017
|
+
const saved = await load2(db, "membershipOfferCheckout", id11);
|
|
3942
4018
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
3943
4019
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3944
4020
|
}
|
|
3945
4021
|
|
|
4022
|
+
// src/membership-checkout-seat-payment.ts
|
|
4023
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
4024
|
+
const { now, paidAt, endsAt } = input;
|
|
4025
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
4026
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
4027
|
+
return [
|
|
4028
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
4029
|
+
id: seat.seatId,
|
|
4030
|
+
...membershipScope(target),
|
|
4031
|
+
primaryMembershipId: seat.membershipId,
|
|
4032
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
4033
|
+
recipientName: seat.recipientName,
|
|
4034
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
4035
|
+
recipientClaimId: seat.recipientClaimId,
|
|
4036
|
+
priceId: seat.priceId,
|
|
4037
|
+
primaryPriceId: input.primaryPriceId,
|
|
4038
|
+
amountCents: seat.amountCents,
|
|
4039
|
+
currency: input.offering.currency,
|
|
4040
|
+
interval: input.offering.interval,
|
|
4041
|
+
providerCustomerId: input.customerId,
|
|
4042
|
+
providerSubscriptionId: input.subscriptionId,
|
|
4043
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
4044
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
4045
|
+
preparedAt: input.boundAt,
|
|
4046
|
+
purchasedAt: paidAt,
|
|
4047
|
+
termStartedAt: paidAt,
|
|
4048
|
+
termEndsAt: endsAt,
|
|
4049
|
+
acceptanceEndsAt: endsAt,
|
|
4050
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
4051
|
+
offeringSnapshot: input.offering,
|
|
4052
|
+
paymentEventId: input.eventId,
|
|
4053
|
+
...included ? { entitlementId } : {},
|
|
4054
|
+
revision: 1,
|
|
4055
|
+
createdAt: now,
|
|
4056
|
+
updatedAt: now
|
|
4057
|
+
}),
|
|
4058
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
4059
|
+
id: entitlementId,
|
|
4060
|
+
...membershipScope(target),
|
|
4061
|
+
membershipId: seat.membershipId,
|
|
4062
|
+
seatId: seat.seatId,
|
|
4063
|
+
kind: "named_seat",
|
|
4064
|
+
status: "pending_acceptance",
|
|
4065
|
+
termStartedAt: paidAt,
|
|
4066
|
+
termEndsAt: endsAt,
|
|
4067
|
+
offeringSnapshot: input.offering,
|
|
4068
|
+
sourceEventId: input.eventId,
|
|
4069
|
+
revision: 1,
|
|
4070
|
+
createdAt: now,
|
|
4071
|
+
updatedAt: now
|
|
4072
|
+
})] : [],
|
|
4073
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
4074
|
+
id: effectId,
|
|
4075
|
+
chapterId: target.chapterId,
|
|
4076
|
+
environment: target.environment,
|
|
4077
|
+
runtime: target.runtime,
|
|
4078
|
+
membershipId: seat.membershipId,
|
|
4079
|
+
seatId: seat.seatId,
|
|
4080
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
4081
|
+
status: "pending",
|
|
4082
|
+
attempts: 0,
|
|
4083
|
+
createdAt: now,
|
|
4084
|
+
updatedAt: now
|
|
4085
|
+
})
|
|
4086
|
+
];
|
|
4087
|
+
}
|
|
4088
|
+
|
|
3946
4089
|
// src/membership-payment-authority.ts
|
|
3947
4090
|
async function networkMembershipId(input) {
|
|
3948
4091
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3958,11 +4101,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3958
4101
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
3959
4102
|
);
|
|
3960
4103
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
4104
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
4105
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
4106
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
3961
4107
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3962
4108
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3963
4109
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3964
4110
|
const fact = eventRow.fact;
|
|
3965
|
-
if (!fact || !fact.identity || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.eventType !== "invoice.payment_succeeded" || fact.objectKind !== "invoice" || fact.billingReason !== "subscription_create" || fact.livemode !== (input.environment === "prod") || fact.status !== "paid" || fact.applicationId !== input.applicationId || fact.subscriptionId !== input.providerSubscriptionId || fact.customerId !== input.providerCustomerId || !/^in_[A-Za-z0-9_]+$/.test(fact.invoiceId ?? "") || fact.currency !== offer.contract.offering.currency || fact.invoiceTotalCents !==
|
|
4111
|
+
if (!fact || !fact.identity || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.eventType !== "invoice.payment_succeeded" || fact.objectKind !== "invoice" || fact.billingReason !== "subscription_create" || fact.livemode !== (input.environment === "prod") || fact.status !== "paid" || fact.applicationId !== input.applicationId || fact.subscriptionId !== input.providerSubscriptionId || fact.customerId !== input.providerCustomerId || !/^in_[A-Za-z0-9_]+$/.test(fact.invoiceId ?? "") || fact.currency !== offer.contract.offering.currency || fact.invoiceTotalCents !== totalCents || fact.amountPaidCents !== totalCents || Object.entries({
|
|
3966
4112
|
appId: input.appId,
|
|
3967
4113
|
chapterId: input.chapterId,
|
|
3968
4114
|
environment: input.environment,
|
|
@@ -3974,14 +4120,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3974
4120
|
const paidAt = fact.invoicePaidAt;
|
|
3975
4121
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
3976
4122
|
if (!Number.isSafeInteger(paidAt) || Number(paidAt) < Math.floor(Number(binding.boundAt) / 1e3) * 1e3 || Number(paidAt) > now + 3e4 || !Number.isSafeInteger(endsAt) || Number(endsAt) <= Number(paidAt)) membershipFail("membership_payment_period_mismatch");
|
|
3977
|
-
const
|
|
3978
|
-
let member = await membershipLoad(db, "networkMembership",
|
|
4123
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
4124
|
+
let member = await membershipLoad(db, "networkMembership", id11);
|
|
3979
4125
|
if (!member || member.status === "restarting") {
|
|
3980
4126
|
const offering = offer.contract.offering;
|
|
3981
4127
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3982
4128
|
const attrs = {
|
|
3983
|
-
id:
|
|
3984
|
-
membershipId:
|
|
4129
|
+
id: id11,
|
|
4130
|
+
membershipId: id11,
|
|
3985
4131
|
...membershipScope(input),
|
|
3986
4132
|
baseSkuId: input.skuId,
|
|
3987
4133
|
status: "paid_pending_approval",
|
|
@@ -3997,8 +4143,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3997
4143
|
founding: offer.contract.founding,
|
|
3998
4144
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
3999
4145
|
baseAmountCents: offering.basePriceCents,
|
|
4000
|
-
amountCents:
|
|
4146
|
+
amountCents: totalCents,
|
|
4001
4147
|
renewalAmountCents: offer.contract.renewalCents,
|
|
4148
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
4002
4149
|
currency: offering.currency,
|
|
4003
4150
|
interval: offering.interval,
|
|
4004
4151
|
initialTermStartedAt: paidAt,
|
|
@@ -4015,11 +4162,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
4015
4162
|
};
|
|
4016
4163
|
try {
|
|
4017
4164
|
await db.transact([
|
|
4018
|
-
membershipUpdate("networkMembership",
|
|
4165
|
+
membershipUpdate("networkMembership", id11, attrs),
|
|
4019
4166
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4020
4167
|
id: entitlementId,
|
|
4021
4168
|
...membershipScope(input),
|
|
4022
|
-
membershipId:
|
|
4169
|
+
membershipId: id11,
|
|
4023
4170
|
kind: "primary",
|
|
4024
4171
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
4025
4172
|
termStartedAt: paidAt,
|
|
@@ -4031,23 +4178,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
4031
4178
|
updatedAt: now,
|
|
4032
4179
|
...restart?.entitlementAttrs
|
|
4033
4180
|
}),
|
|
4034
|
-
...restart?.ops ?? []
|
|
4035
|
-
|
|
4036
|
-
|
|
4181
|
+
...restart?.ops ?? [],
|
|
4182
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
4183
|
+
offering,
|
|
4184
|
+
primaryPriceId: String(binding.priceId),
|
|
4185
|
+
invoiceId: fact.invoiceId,
|
|
4186
|
+
invoiceTotalCents: totalCents,
|
|
4187
|
+
subscriptionId: input.providerSubscriptionId,
|
|
4188
|
+
customerId: input.providerCustomerId,
|
|
4189
|
+
eventId: input.eventId,
|
|
4190
|
+
paidAt: Number(paidAt),
|
|
4191
|
+
endsAt: Number(endsAt),
|
|
4192
|
+
boundAt: Number(binding.boundAt),
|
|
4193
|
+
now
|
|
4194
|
+
}) : []
|
|
4195
|
+
], { mutationId: `membership-paid:${id11}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
4196
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id11, exists: false }],
|
|
4197
|
+
...namedSeat ? [
|
|
4198
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
4199
|
+
{
|
|
4200
|
+
ns: "networkSeatRecipient",
|
|
4201
|
+
id: namedSeat.recipientClaimId,
|
|
4202
|
+
exists: true,
|
|
4203
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
4204
|
+
}
|
|
4205
|
+
] : [],
|
|
4037
4206
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4038
|
-
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
4207
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
4039
4208
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
4040
4209
|
evidenceGuard
|
|
4041
4210
|
] });
|
|
4042
4211
|
} catch (error) {
|
|
4043
4212
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4044
4213
|
}
|
|
4045
|
-
member = await membershipLoad(db, "networkMembership",
|
|
4214
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
4046
4215
|
}
|
|
4047
4216
|
requireMembershipScope(member, input);
|
|
4048
4217
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
4049
4218
|
return {
|
|
4050
|
-
membershipId:
|
|
4219
|
+
membershipId: id11,
|
|
4051
4220
|
status: String(member.status),
|
|
4052
4221
|
cohort: String(member.cohort),
|
|
4053
4222
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -4059,9 +4228,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
4059
4228
|
// src/membership-cancel-authority.ts
|
|
4060
4229
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
4061
4230
|
membershipClock(now);
|
|
4062
|
-
const
|
|
4231
|
+
const id11 = await networkMembershipId(target);
|
|
4063
4232
|
const state = await loadMembershipAuthorityState(db, target);
|
|
4064
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
4233
|
+
const member = await membershipLoad(db, "networkMembership", id11);
|
|
4065
4234
|
requireMembershipScope(member, target);
|
|
4066
4235
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
4067
4236
|
membershipFail("membership_cancellation_unavailable");
|
|
@@ -4079,7 +4248,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
4079
4248
|
"POST",
|
|
4080
4249
|
path,
|
|
4081
4250
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
4082
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
4251
|
+
{ idempotencyKey: `membership-cancel-at-term:${id11}:${member.providerSubscriptionId}` }
|
|
4083
4252
|
);
|
|
4084
4253
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
4085
4254
|
membershipFail("membership_cancellation_pending");
|
|
@@ -4087,7 +4256,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
4087
4256
|
}
|
|
4088
4257
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
4089
4258
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
4090
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
4259
|
+
await db.transact([membershipUpdate("networkMembership", id11, {
|
|
4091
4260
|
status: status2,
|
|
4092
4261
|
autoRenew: false,
|
|
4093
4262
|
graceEndsAt: 0,
|
|
@@ -4096,12 +4265,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
4096
4265
|
updatedAt: now
|
|
4097
4266
|
})], {
|
|
4098
4267
|
guards: [
|
|
4099
|
-
{ ns: "networkMembership", id:
|
|
4268
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4100
4269
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
4101
4270
|
]
|
|
4102
4271
|
});
|
|
4103
4272
|
}
|
|
4104
|
-
return { membershipId:
|
|
4273
|
+
return { membershipId: id11, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
4105
4274
|
}
|
|
4106
4275
|
|
|
4107
4276
|
// src/membership-authority-handler.ts
|
|
@@ -4140,17 +4309,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
4140
4309
|
// src/membership-approval-authority.ts
|
|
4141
4310
|
async function approveNetworkMembership(db, input, now) {
|
|
4142
4311
|
membershipClock(now);
|
|
4143
|
-
const
|
|
4312
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
4144
4313
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4145
4314
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4146
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
4315
|
+
const member = await membershipLoad(db, "networkMembership", id11);
|
|
4147
4316
|
requireMembershipScope(member, input);
|
|
4148
4317
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
4149
4318
|
requireMembershipScope(entitlement, input);
|
|
4150
|
-
if (entitlement.membershipId !==
|
|
4319
|
+
if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
4151
4320
|
if (["active", "cancel_at_term_end"].includes(String(member.status)) && entitlement.status === "active" && Number(member.refundedCents) === 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && Number(member.termEndsAt) > now) {
|
|
4152
4321
|
return {
|
|
4153
|
-
membershipId:
|
|
4322
|
+
membershipId: id11,
|
|
4154
4323
|
status: String(member.status),
|
|
4155
4324
|
cohort: String(member.cohort),
|
|
4156
4325
|
memberNumber: Number(member.memberNumber),
|
|
@@ -4170,7 +4339,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
4170
4339
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
4171
4340
|
try {
|
|
4172
4341
|
await db.transact([
|
|
4173
|
-
membershipUpdate("networkMembership",
|
|
4342
|
+
membershipUpdate("networkMembership", id11, {
|
|
4174
4343
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
4175
4344
|
memberNumber,
|
|
4176
4345
|
journeyApprovedAt: now,
|
|
@@ -4185,8 +4354,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
4185
4354
|
updatedAt: now
|
|
4186
4355
|
}),
|
|
4187
4356
|
...seatActivation.ops
|
|
4188
|
-
], { mutationId: `membership-approve:${
|
|
4189
|
-
{ ns: "networkMembership", id:
|
|
4357
|
+
], { mutationId: `membership-approve:${id11}`, guards: [
|
|
4358
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
4190
4359
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
4191
4360
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
4192
4361
|
...seatActivation.guards
|
|
@@ -4285,41 +4454,41 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
4285
4454
|
// src/membership-refund-authority.ts
|
|
4286
4455
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
4287
4456
|
membershipClock(now);
|
|
4288
|
-
const
|
|
4289
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4457
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
4458
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4290
4459
|
requireMembershipScope(member, input);
|
|
4291
|
-
if (member.status === "refunded") return { membershipId:
|
|
4460
|
+
if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
4292
4461
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
4293
4462
|
if (member.status !== "refund_pending") {
|
|
4294
4463
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
4295
4464
|
requireMembershipScope(entitlement, input);
|
|
4296
4465
|
await db.transact([
|
|
4297
|
-
membershipUpdate("networkMembership",
|
|
4466
|
+
membershipUpdate("networkMembership", id11, {
|
|
4298
4467
|
status: "refund_pending",
|
|
4299
4468
|
refundRequestedAt: now,
|
|
4300
4469
|
revision: Number(member.revision) + 1,
|
|
4301
4470
|
updatedAt: now
|
|
4302
4471
|
}),
|
|
4303
4472
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
4304
|
-
membershipUpdate("membershipEffect", `${
|
|
4305
|
-
id: `${
|
|
4473
|
+
membershipUpdate("membershipEffect", `${id11}:refund`, {
|
|
4474
|
+
id: `${id11}:refund`,
|
|
4306
4475
|
chapterId: input.chapterId,
|
|
4307
4476
|
environment: input.environment,
|
|
4308
4477
|
runtime: input.runtime,
|
|
4309
|
-
membershipId:
|
|
4478
|
+
membershipId: id11,
|
|
4310
4479
|
kind: "refund_membership",
|
|
4311
4480
|
status: "pending",
|
|
4312
4481
|
attempts: 0,
|
|
4313
4482
|
createdAt: now,
|
|
4314
4483
|
updatedAt: now
|
|
4315
4484
|
})
|
|
4316
|
-
], { mutationId: `membership-refund-request:${
|
|
4317
|
-
{ ns: "networkMembership", id:
|
|
4485
|
+
], { mutationId: `membership-refund-request:${id11}`, guards: [
|
|
4486
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4318
4487
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
4319
4488
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
4320
4489
|
] });
|
|
4321
4490
|
}
|
|
4322
|
-
member = await membershipLoad(db, "networkMembership",
|
|
4491
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
4323
4492
|
requireMembershipScope(member, input);
|
|
4324
4493
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
4325
4494
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -4333,23 +4502,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4333
4502
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
4334
4503
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4335
4504
|
state = await loadMembershipAuthorityState(db, input);
|
|
4336
|
-
member = await membershipLoad(db, "networkMembership",
|
|
4505
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
4337
4506
|
requireMembershipScope(member, input);
|
|
4338
|
-
if (member.status === "refunded") return { membershipId:
|
|
4507
|
+
if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
4339
4508
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
4340
4509
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
4341
4510
|
const counts = state.cohortCounts;
|
|
4342
4511
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
4343
4512
|
try {
|
|
4344
4513
|
await db.transact([
|
|
4345
|
-
membershipUpdate("networkMembership",
|
|
4514
|
+
membershipUpdate("networkMembership", id11, {
|
|
4346
4515
|
status: "refunded",
|
|
4347
4516
|
refundedCents: result.refundedCents,
|
|
4348
4517
|
autoRenew: false,
|
|
4349
4518
|
revision: Number(member.revision) + 1,
|
|
4350
4519
|
updatedAt: now
|
|
4351
4520
|
}),
|
|
4352
|
-
membershipUpdate("membershipEffect", `${
|
|
4521
|
+
membershipUpdate("membershipEffect", `${id11}:refund`, { status: "complete", updatedAt: now }),
|
|
4353
4522
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
4354
4523
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
4355
4524
|
revision: Number(state.revision) + 1,
|
|
@@ -4360,7 +4529,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4360
4529
|
chapterId: input.chapterId,
|
|
4361
4530
|
environment: input.environment,
|
|
4362
4531
|
runtime: input.runtime,
|
|
4363
|
-
membershipId:
|
|
4532
|
+
membershipId: id11,
|
|
4364
4533
|
seatId: member.namedSeatId,
|
|
4365
4534
|
kind: "refund_seat_with_primary",
|
|
4366
4535
|
status: "pending",
|
|
@@ -4368,8 +4537,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4368
4537
|
createdAt: now,
|
|
4369
4538
|
updatedAt: now
|
|
4370
4539
|
})] : []
|
|
4371
|
-
], { mutationId: `membership-refunded:${
|
|
4372
|
-
{ ns: "networkMembership", id:
|
|
4540
|
+
], { mutationId: `membership-refunded:${id11}`, guards: [
|
|
4541
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
4373
4542
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4374
4543
|
] });
|
|
4375
4544
|
} catch (error) {
|
|
@@ -4397,8 +4566,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4397
4566
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4398
4567
|
requireMembershipScope(member, input);
|
|
4399
4568
|
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4400
|
-
const
|
|
4401
|
-
let seat = await membershipLoad(db, "networkSeat",
|
|
4569
|
+
const id11 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4570
|
+
let seat = await membershipLoad(db, "networkSeat", id11);
|
|
4402
4571
|
if (!seat) {
|
|
4403
4572
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4404
4573
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -4406,7 +4575,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4406
4575
|
membershipFail("named_seat_purchase_unavailable");
|
|
4407
4576
|
}
|
|
4408
4577
|
const attrs = {
|
|
4409
|
-
id:
|
|
4578
|
+
id: id11,
|
|
4410
4579
|
...membershipScope(input),
|
|
4411
4580
|
primaryMembershipId: memberId,
|
|
4412
4581
|
recipientName: name,
|
|
@@ -4431,20 +4600,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4431
4600
|
};
|
|
4432
4601
|
try {
|
|
4433
4602
|
await db.transact([
|
|
4434
|
-
membershipUpdate("networkSeat",
|
|
4603
|
+
membershipUpdate("networkSeat", id11, attrs),
|
|
4435
4604
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4436
4605
|
id: claimId,
|
|
4437
4606
|
appId: input.appId,
|
|
4438
4607
|
chapterId: input.chapterId,
|
|
4439
4608
|
environment: input.environment,
|
|
4440
4609
|
runtime: input.runtime,
|
|
4441
|
-
seatId:
|
|
4610
|
+
seatId: id11,
|
|
4442
4611
|
recipientEmailNormalized: email,
|
|
4443
4612
|
createdAt: now
|
|
4444
4613
|
}),
|
|
4445
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4446
|
-
], { mutationId: `named-seat-reserve:${
|
|
4447
|
-
{ ns: "networkSeat", id:
|
|
4614
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id11, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4615
|
+
], { mutationId: `named-seat-reserve:${id11}`, guards: [
|
|
4616
|
+
{ ns: "networkSeat", id: id11, exists: false },
|
|
4448
4617
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4449
4618
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4450
4619
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4452,7 +4621,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4452
4621
|
} catch (error) {
|
|
4453
4622
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4454
4623
|
}
|
|
4455
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4624
|
+
seat = await membershipLoad(db, "networkSeat", id11);
|
|
4456
4625
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4457
4626
|
}
|
|
4458
4627
|
requireMembershipScope(seat, input);
|
|
@@ -4462,7 +4631,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4462
4631
|
|
|
4463
4632
|
// src/membership-seat-included.ts
|
|
4464
4633
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4465
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4634
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4466
4635
|
function namedSeatRecipientState(seat) {
|
|
4467
4636
|
switch (String(seat.status)) {
|
|
4468
4637
|
case "renewal_pending":
|
|
@@ -4560,7 +4729,7 @@ async function grantIncludedNamedSeat(db, input, now) {
|
|
|
4560
4729
|
// src/membership-renewal-authority.ts
|
|
4561
4730
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4562
4731
|
membershipClock(now);
|
|
4563
|
-
const
|
|
4732
|
+
const id11 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4564
4733
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
4565
4734
|
const fact = eventRow?.fact;
|
|
4566
4735
|
if (!fact || !/^evt_[A-Za-z0-9_]+$/.test(input.eventId) || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.objectKind !== "invoice" || !["invoice.payment_succeeded", "invoice.payment_failed"].includes(fact.eventType) || fact.billingReason !== "subscription_cycle" || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || Object.entries({
|
|
@@ -4573,10 +4742,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4573
4742
|
return membershipFail("membership_renewal_evidence_pending");
|
|
4574
4743
|
}
|
|
4575
4744
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4576
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4745
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4577
4746
|
requireMembershipScope(member, input);
|
|
4578
4747
|
const receipt = () => ({
|
|
4579
|
-
membershipId:
|
|
4748
|
+
membershipId: id11,
|
|
4580
4749
|
status: String(member.status),
|
|
4581
4750
|
termEndsAt: Number(member.termEndsAt),
|
|
4582
4751
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -4601,13 +4770,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4601
4770
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
4602
4771
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
4603
4772
|
if (paid && (fact.status !== "paid" || fact.amountPaidCents !== amount || !Number.isSafeInteger(fact.invoicePaidAt) || Number(fact.invoicePaidAt) < start || Number(fact.invoicePaidAt) > now + 3e4)) membershipFail("membership_renewal_unpaid");
|
|
4604
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
4773
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4605
4774
|
requireMembershipScope(entitlement, input);
|
|
4606
4775
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
4607
4776
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
4608
4777
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
4609
4778
|
const ops = [
|
|
4610
|
-
membershipUpdate("networkMembership",
|
|
4779
|
+
membershipUpdate("networkMembership", id11, {
|
|
4611
4780
|
status: status2,
|
|
4612
4781
|
graceEndsAt,
|
|
4613
4782
|
...paid ? {
|
|
@@ -4618,7 +4787,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4618
4787
|
revision: Number(member.revision) + 1,
|
|
4619
4788
|
updatedAt: now
|
|
4620
4789
|
}),
|
|
4621
|
-
membershipUpdate("networkEntitlement", `${
|
|
4790
|
+
membershipUpdate("networkEntitlement", `${id11}:primary`, {
|
|
4622
4791
|
status: entitlementStatus,
|
|
4623
4792
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4624
4793
|
sourceEventId: input.eventId,
|
|
@@ -4627,8 +4796,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4627
4796
|
})
|
|
4628
4797
|
];
|
|
4629
4798
|
const guards = [
|
|
4630
|
-
{ ns: "networkMembership", id:
|
|
4631
|
-
{ ns: "networkEntitlement", id: `${
|
|
4799
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4800
|
+
{ ns: "networkEntitlement", id: `${id11}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
4632
4801
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
4633
4802
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
4634
4803
|
evidenceGuard
|
|
@@ -4653,15 +4822,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4653
4822
|
);
|
|
4654
4823
|
}
|
|
4655
4824
|
try {
|
|
4656
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
4825
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id11}:${input.eventId}`, guards });
|
|
4657
4826
|
} catch (error) {
|
|
4658
4827
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4659
4828
|
continue;
|
|
4660
4829
|
}
|
|
4661
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
4830
|
+
const saved = await membershipLoad(db, "networkMembership", id11);
|
|
4662
4831
|
requireMembershipScope(saved, input);
|
|
4663
4832
|
return {
|
|
4664
|
-
membershipId:
|
|
4833
|
+
membershipId: id11,
|
|
4665
4834
|
status: String(saved.status),
|
|
4666
4835
|
termEndsAt: Number(saved.termEndsAt),
|
|
4667
4836
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -4687,12 +4856,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4687
4856
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
4688
4857
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
4689
4858
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
4690
|
-
const
|
|
4859
|
+
const id11 = await networkMembershipId(input);
|
|
4691
4860
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4692
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4861
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4693
4862
|
requireMembershipScope(member, input);
|
|
4694
4863
|
const receipt = (value = member) => ({
|
|
4695
|
-
membershipId:
|
|
4864
|
+
membershipId: id11,
|
|
4696
4865
|
status: String(value.status),
|
|
4697
4866
|
termEndsAt: Number(value.termEndsAt),
|
|
4698
4867
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -4713,7 +4882,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4713
4882
|
});
|
|
4714
4883
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4715
4884
|
await refundNetworkMembership(db, input, sk, now);
|
|
4716
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
4885
|
+
const saved2 = await membershipLoad(db, "networkMembership", id11);
|
|
4717
4886
|
requireMembershipScope(saved2, input);
|
|
4718
4887
|
return receipt(saved2);
|
|
4719
4888
|
}
|
|
@@ -4728,18 +4897,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4728
4897
|
const status2 = autoRenew ? member.status === "cancel_at_term_end" ? "active" : String(member.status) : now >= Number(member.termEndsAt) ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
4729
4898
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
4730
4899
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
4731
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
4900
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4732
4901
|
requireMembershipScope(entitlement, input);
|
|
4733
4902
|
try {
|
|
4734
4903
|
await db.transact([
|
|
4735
|
-
membershipUpdate("networkMembership",
|
|
4904
|
+
membershipUpdate("networkMembership", id11, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
4736
4905
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
4737
4906
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
4738
4907
|
revision: Number(entitlement.revision) + 1,
|
|
4739
4908
|
updatedAt: now
|
|
4740
4909
|
})
|
|
4741
4910
|
], { guards: [
|
|
4742
|
-
{ ns: "networkMembership", id:
|
|
4911
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4743
4912
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
4744
4913
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4745
4914
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -4749,7 +4918,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4749
4918
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4750
4919
|
continue;
|
|
4751
4920
|
}
|
|
4752
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
4921
|
+
const saved = await membershipLoad(db, "networkMembership", id11);
|
|
4753
4922
|
requireMembershipScope(saved, input);
|
|
4754
4923
|
return receipt(saved);
|
|
4755
4924
|
}
|
|
@@ -4810,19 +4979,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
4810
4979
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4811
4980
|
membershipClock(now);
|
|
4812
4981
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4813
|
-
const
|
|
4814
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
4982
|
+
const id11 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4983
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id11, input.previousSubscriptionId]))}`;
|
|
4815
4984
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4816
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
4985
|
+
const previous = await membershipLoad(db, "networkMembership", id11);
|
|
4817
4986
|
requireMembershipScope(previous, input);
|
|
4818
4987
|
const receipt = {
|
|
4819
4988
|
applicationId: input.applicationId,
|
|
4820
|
-
membershipId:
|
|
4989
|
+
membershipId: id11,
|
|
4821
4990
|
restartId,
|
|
4822
4991
|
memberNumber: Number(previous.memberNumber),
|
|
4823
4992
|
status: "ready"
|
|
4824
4993
|
};
|
|
4825
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
4994
|
+
const existing = await membershipLoad(db, "membershipRestart", id11);
|
|
4826
4995
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4827
4996
|
requireMembershipScope(existing, input);
|
|
4828
4997
|
return receipt;
|
|
@@ -4838,33 +5007,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
4838
5007
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4839
5008
|
id: restartId,
|
|
4840
5009
|
...membershipScope(input),
|
|
4841
|
-
membershipId:
|
|
5010
|
+
membershipId: id11,
|
|
4842
5011
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
4843
5012
|
memberSnapshot: previous,
|
|
4844
5013
|
bindingSnapshot: binding2,
|
|
4845
5014
|
archivedAt: now
|
|
4846
5015
|
}),
|
|
4847
|
-
membershipUpdate("networkMembership",
|
|
5016
|
+
membershipUpdate("networkMembership", id11, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4848
5017
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4849
5018
|
...seats.guards,
|
|
4850
|
-
{ ns: "networkMembership", id:
|
|
5019
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4851
5020
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4852
5021
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4853
5022
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4854
5023
|
] });
|
|
4855
5024
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4856
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
5025
|
+
const reserved = await membershipLoad(db, "networkMembership", id11);
|
|
4857
5026
|
requireMembershipScope(reserved, input);
|
|
4858
5027
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4859
5028
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4860
5029
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4861
5030
|
requireMembershipScope(binding, input);
|
|
4862
5031
|
await db.transact([
|
|
4863
|
-
membershipUpdate("membershipRestart",
|
|
5032
|
+
membershipUpdate("membershipRestart", id11, { id: id11, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4864
5033
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4865
5034
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4866
|
-
{ ns: "membershipRestart", id:
|
|
4867
|
-
{ ns: "networkMembership", id:
|
|
5035
|
+
{ ns: "membershipRestart", id: id11, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
5036
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4868
5037
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4869
5038
|
] });
|
|
4870
5039
|
return receipt;
|
|
@@ -4874,15 +5043,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
4874
5043
|
async function readMembershipAccess(db, input, now) {
|
|
4875
5044
|
membershipClock(now);
|
|
4876
5045
|
await loadMembershipAuthorityState(db, input);
|
|
4877
|
-
const
|
|
4878
|
-
if (!member) return { membershipId:
|
|
5046
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
5047
|
+
if (!member) return { membershipId: id11, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
4879
5048
|
requireMembershipScope(member, input);
|
|
4880
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
5049
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4881
5050
|
requireMembershipScope(entitlement, input);
|
|
4882
5051
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4883
|
-
const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId ===
|
|
5052
|
+
const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id11 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
|
|
4884
5053
|
return {
|
|
4885
|
-
membershipId:
|
|
5054
|
+
membershipId: id11,
|
|
4886
5055
|
allowed: allowed2,
|
|
4887
5056
|
status: String(member.status),
|
|
4888
5057
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -4937,7 +5106,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
4937
5106
|
currency: seat.currency,
|
|
4938
5107
|
interval: seat.interval,
|
|
4939
5108
|
environment: seat.environment
|
|
4940
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
5109
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
4941
5110
|
if (!productId) reject3();
|
|
4942
5111
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
4943
5112
|
const items = sub.items?.data;
|
|
@@ -4981,10 +5150,14 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4981
5150
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
4982
5151
|
}
|
|
4983
5152
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
4984
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
|
|
5153
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
|
|
4985
5154
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4986
5155
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4987
|
-
if (existing)
|
|
5156
|
+
if (existing) {
|
|
5157
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject3();
|
|
5158
|
+
return String(existing.id);
|
|
5159
|
+
}
|
|
5160
|
+
if (seat.combinedInvoice) reject3();
|
|
4988
5161
|
const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4989
5162
|
subscription: seat.providerSubscriptionId,
|
|
4990
5163
|
price: seat.priceId,
|
|
@@ -5202,7 +5375,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
5202
5375
|
const state = await loadMembershipAuthorityState(db, input);
|
|
5203
5376
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5204
5377
|
requireMembershipScope(seat, input);
|
|
5205
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
5378
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
5206
5379
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5207
5380
|
requireMembershipScope(member, input);
|
|
5208
5381
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -5260,43 +5433,121 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
5260
5433
|
}
|
|
5261
5434
|
|
|
5262
5435
|
// src/membership-seat-refund-stripe.ts
|
|
5436
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5437
|
+
|
|
5438
|
+
// src/membership-checkout-seat-refund.ts
|
|
5263
5439
|
var import_stripe8 = require("@odla-ai/stripe");
|
|
5264
5440
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
5265
5441
|
throw Object.assign(new Error(code), { code });
|
|
5266
5442
|
};
|
|
5443
|
+
var id8 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
5444
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
5445
|
+
const invoiceRef = seat.combinedInvoice;
|
|
5446
|
+
if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
|
|
5447
|
+
const call = async (method, path, params, key3) => {
|
|
5448
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
5449
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
5450
|
+
return result.body;
|
|
5451
|
+
};
|
|
5452
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
5453
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
5454
|
+
if (sub.id !== seat.providerSubscriptionId || id8(sub.customer) !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(sub.items?.data) || sub.items.has_more !== false) fail4();
|
|
5455
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
5456
|
+
if (invoice.id !== invoiceRef.invoiceId || id8(invoice.subscription ?? invoice.parent?.subscription_details?.subscription) !== sub.id || id8(invoice.customer) !== seat.providerCustomerId || invoice.status !== "paid" || invoice.billing_reason !== "subscription_create" || invoice.total !== invoiceRef.totalCents || invoice.amount_paid !== invoiceRef.totalCents || invoice.currency !== seat.currency || invoice.livemode !== (seat.environment === "prod")) fail4();
|
|
5457
|
+
let chargeId = id8(invoice.charge), paymentId = id8(invoice.payment_intent);
|
|
5458
|
+
if (!chargeId && !paymentId) {
|
|
5459
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
5460
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
5461
|
+
const payment = payments.data[0];
|
|
5462
|
+
if (id8(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
5463
|
+
paymentId = id8(payment.payment.payment_intent);
|
|
5464
|
+
}
|
|
5465
|
+
if (!chargeId) {
|
|
5466
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
5467
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
5468
|
+
if (payment.id !== paymentId || id8(payment.customer) !== seat.providerCustomerId || payment.status !== "succeeded" || payment.amount_received !== invoiceRef.totalCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod")) fail4();
|
|
5469
|
+
chargeId = id8(payment.latest_charge);
|
|
5470
|
+
}
|
|
5471
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
5472
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
5473
|
+
if (charge.id !== chargeId || id8(charge.customer) !== seat.providerCustomerId || charge.status !== "succeeded" || charge.livemode !== (seat.environment === "prod") || charge.amount !== invoiceRef.totalCents || charge.currency !== seat.currency || charge.invoice != null && id8(charge.invoice) !== invoice.id || paymentId && id8(charge.payment_intent) !== paymentId || !Number.isSafeInteger(charge.amount_refunded) || charge.amount_refunded < 0 || charge.amount_refunded > charge.amount) fail4();
|
|
5474
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
5475
|
+
if (items.length > 1) fail4();
|
|
5476
|
+
if (items.length && sub.status !== "canceled") {
|
|
5477
|
+
const item = items[0];
|
|
5478
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
5479
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
5480
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
5481
|
+
}
|
|
5482
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
5483
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
5484
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
5485
|
+
let refundedCents = 0;
|
|
5486
|
+
const refundIds = [];
|
|
5487
|
+
for (const refund of list.data) {
|
|
5488
|
+
if (id8(refund.charge) !== chargeId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !/^re_[A-Za-z0-9_]+$/.test(refund.id) || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
5489
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
5490
|
+
if (refund.status === "succeeded") {
|
|
5491
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
5492
|
+
refundedCents += refund.amount;
|
|
5493
|
+
refundIds.push(refund.id);
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
5497
|
+
if (refundedCents < seat.amountCents) {
|
|
5498
|
+
const remaining = seat.amountCents - refundedCents;
|
|
5499
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
5500
|
+
charge: chargeId,
|
|
5501
|
+
amount: remaining,
|
|
5502
|
+
reason: "requested_by_customer",
|
|
5503
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
5504
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
5505
|
+
if (id8(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
5506
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
5507
|
+
refundedCents += remaining;
|
|
5508
|
+
refundIds.push(refund.id);
|
|
5509
|
+
}
|
|
5510
|
+
return { refundedCents, refundIds };
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
// src/membership-seat-refund-stripe.ts
|
|
5514
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
5515
|
+
throw Object.assign(new Error(code), { code });
|
|
5516
|
+
};
|
|
5267
5517
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
5268
|
-
if (
|
|
5518
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
5519
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
5269
5520
|
const call = async (method, path, params, idempotencyKey) => {
|
|
5270
|
-
const result = await (0,
|
|
5271
|
-
if (!result.ok || !result.body) return
|
|
5521
|
+
const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
5522
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
5272
5523
|
return result.body;
|
|
5273
5524
|
};
|
|
5274
5525
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5275
5526
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
5276
|
-
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value))
|
|
5527
|
+
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail5();
|
|
5277
5528
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
5278
|
-
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data))
|
|
5529
|
+
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail5();
|
|
5279
5530
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
5280
|
-
if (items.length > 1)
|
|
5531
|
+
if (items.length > 1) fail5();
|
|
5281
5532
|
if (items.length && sub.status !== "canceled") {
|
|
5282
5533
|
const item = items[0];
|
|
5283
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
5534
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
5284
5535
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
5285
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
5536
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
5286
5537
|
}
|
|
5287
5538
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
5288
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
5539
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
5289
5540
|
let refundedCents = 0, pendingCents = 0;
|
|
5290
5541
|
const refundIds = [];
|
|
5291
5542
|
for (const refund of list.data) {
|
|
5292
|
-
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status))
|
|
5543
|
+
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail5();
|
|
5293
5544
|
if (refund.status === "succeeded") {
|
|
5294
5545
|
refundedCents += refund.amount;
|
|
5295
5546
|
refundIds.push(refund.id);
|
|
5296
5547
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
5297
5548
|
}
|
|
5298
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
5299
|
-
if (pendingCents)
|
|
5549
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
5550
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
5300
5551
|
if (refundedCents < seat.amountCents) {
|
|
5301
5552
|
const remaining = seat.amountCents - refundedCents;
|
|
5302
5553
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -5305,8 +5556,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
5305
5556
|
reason: "requested_by_customer",
|
|
5306
5557
|
metadata
|
|
5307
5558
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
5308
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
5309
|
-
if (refund.status !== "succeeded")
|
|
5559
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
5560
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
5310
5561
|
refundedCents += remaining;
|
|
5311
5562
|
refundIds.push(refund.id);
|
|
5312
5563
|
}
|
|
@@ -5322,7 +5573,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
5322
5573
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5323
5574
|
requireMembershipScope(member, input);
|
|
5324
5575
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
5325
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
5576
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !seat.combinedInvoice && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
5326
5577
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
5327
5578
|
await db.transact([
|
|
5328
5579
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -5427,14 +5678,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
5427
5678
|
async function readNamedSeatOffer(db, input, now) {
|
|
5428
5679
|
membershipClock(now);
|
|
5429
5680
|
await loadMembershipAuthorityState(db, input);
|
|
5430
|
-
const
|
|
5681
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
5431
5682
|
requireMembershipScope(member, input);
|
|
5432
5683
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
5433
5684
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
5434
5685
|
if (seat) requireMembershipScope(seat, input);
|
|
5435
5686
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
5436
5687
|
const offer = {
|
|
5437
|
-
membershipId:
|
|
5688
|
+
membershipId: id11,
|
|
5438
5689
|
amountCents: offering.seatPriceCents,
|
|
5439
5690
|
currency: offering.currency,
|
|
5440
5691
|
interval: offering.interval,
|
|
@@ -5584,7 +5835,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5584
5835
|
};
|
|
5585
5836
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5586
5837
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5587
|
-
const
|
|
5838
|
+
const checkout2 = {
|
|
5588
5839
|
...target,
|
|
5589
5840
|
priceId: peer.seatPriceId,
|
|
5590
5841
|
recipientName: String(input.recipientName),
|
|
@@ -5592,7 +5843,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5592
5843
|
purchaserEmail: String(input.purchaserEmail),
|
|
5593
5844
|
purchaserUserId: String(input.purchaserUserId)
|
|
5594
5845
|
};
|
|
5595
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
5846
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
5596
5847
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5597
5848
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5598
5849
|
if (action === "seat-decline") {
|
|
@@ -5608,7 +5859,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5608
5859
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5609
5860
|
}
|
|
5610
5861
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5611
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5862
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
5612
5863
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5613
5864
|
if (action === "seat-payments") {
|
|
5614
5865
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5679,9 +5930,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5679
5930
|
const identities = /* @__PURE__ */ new Set();
|
|
5680
5931
|
for (const peer of peers) {
|
|
5681
5932
|
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
|
|
5682
|
-
const
|
|
5683
|
-
if (identities.has(
|
|
5684
|
-
identities.add(
|
|
5933
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
5934
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
5935
|
+
identities.add(identity2);
|
|
5685
5936
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
5686
5937
|
}
|
|
5687
5938
|
return async (req) => {
|
|
@@ -5712,9 +5963,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5712
5963
|
try {
|
|
5713
5964
|
input = JSON.parse(verified.body);
|
|
5714
5965
|
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5715
|
-
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
5966
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3) && !(action === "checkouts" && key3 === "namedSeat")) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
5716
5967
|
throw new TypeError("invalid request");
|
|
5717
5968
|
}
|
|
5969
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
5970
|
+
const seat = input.namedSeat;
|
|
5971
|
+
if (!seat || Object.keys(seat).some((key3) => !["recipientName", "recipientEmail", "purchaserEmail"].includes(key3)) || typeof seat.purchaserEmail !== "string" || seat.purchaserEmail.length > 254) throw new TypeError("invalid seat");
|
|
5972
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
5973
|
+
}
|
|
5718
5974
|
} catch {
|
|
5719
5975
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5720
5976
|
}
|
|
@@ -5787,8 +6043,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5787
6043
|
offerId: String(input.offerId),
|
|
5788
6044
|
offerDigest: String(input.offerDigest),
|
|
5789
6045
|
quoteDigest: String(input.quoteDigest),
|
|
5790
|
-
priceId: String(input.priceId)
|
|
5791
|
-
|
|
6046
|
+
priceId: String(input.priceId),
|
|
6047
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
6048
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
5792
6049
|
return respond({ ok: true, binding });
|
|
5793
6050
|
}
|
|
5794
6051
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -5891,7 +6148,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
5891
6148
|
offerId: options.offerId,
|
|
5892
6149
|
offerDigest: options.offerDigest,
|
|
5893
6150
|
quoteDigest: options.quoteDigest,
|
|
5894
|
-
priceId: options.priceId
|
|
6151
|
+
priceId: options.priceId,
|
|
6152
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
5895
6153
|
});
|
|
5896
6154
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
5897
6155
|
}
|
|
@@ -6101,7 +6359,7 @@ async function requestMembershipCancellation(options) {
|
|
|
6101
6359
|
}
|
|
6102
6360
|
|
|
6103
6361
|
// src/membership-seat-cancel.ts
|
|
6104
|
-
var
|
|
6362
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
6105
6363
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
6106
6364
|
membershipClock(now);
|
|
6107
6365
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -6129,7 +6387,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
6129
6387
|
}
|
|
6130
6388
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
6131
6389
|
const request = async (method, path, params) => {
|
|
6132
|
-
const result = await (0,
|
|
6390
|
+
const result = await (0, import_stripe10.stripeRequest)(
|
|
6133
6391
|
{ secretKey },
|
|
6134
6392
|
method,
|
|
6135
6393
|
path,
|
|
@@ -6142,7 +6400,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
6142
6400
|
let payment;
|
|
6143
6401
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
6144
6402
|
else {
|
|
6145
|
-
const snapshot = await listStripeObjects((0,
|
|
6403
|
+
const snapshot = await listStripeObjects((0, import_stripe10.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
6146
6404
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
6147
6405
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
6148
6406
|
payment = matches2[0];
|
|
@@ -6155,9 +6413,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
6155
6413
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
6156
6414
|
if (payment.status !== "canceled") {
|
|
6157
6415
|
if (!["requires_payment_method", "requires_confirmation", "requires_action", "processing"].includes(payment.status)) throw new Error("seat payment outcome unresolved");
|
|
6158
|
-
const
|
|
6159
|
-
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(
|
|
6160
|
-
if (payment.id !==
|
|
6416
|
+
const id11 = payment.id;
|
|
6417
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id11)}/cancel`, { cancellation_reason: "abandoned" });
|
|
6418
|
+
if (payment.id !== id11) throw new Error("seat cancellation receipt mismatch");
|
|
6161
6419
|
}
|
|
6162
6420
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
6163
6421
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -6257,11 +6515,11 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
6257
6515
|
|
|
6258
6516
|
// src/schema-membership-offers.ts
|
|
6259
6517
|
var attr8 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
6260
|
-
var
|
|
6518
|
+
var id9 = () => attr8("string", false, true, true);
|
|
6261
6519
|
var key = () => attr8("string", false, false, true);
|
|
6262
6520
|
var membershipOfferEntities = {
|
|
6263
6521
|
membershipAuthorityState: { attrs: {
|
|
6264
|
-
id:
|
|
6522
|
+
id: id9(),
|
|
6265
6523
|
chapterId: key(),
|
|
6266
6524
|
environment: key(),
|
|
6267
6525
|
mode: key(),
|
|
@@ -6271,7 +6529,7 @@ var membershipOfferEntities = {
|
|
|
6271
6529
|
updatedAt: attr8("number")
|
|
6272
6530
|
} },
|
|
6273
6531
|
membershipCheckoutOffer: { attrs: {
|
|
6274
|
-
id:
|
|
6532
|
+
id: id9(),
|
|
6275
6533
|
applicationId: key(),
|
|
6276
6534
|
appId: key(),
|
|
6277
6535
|
chapterId: key(),
|
|
@@ -6286,7 +6544,7 @@ var membershipOfferEntities = {
|
|
|
6286
6544
|
authorityRevision: attr8("number")
|
|
6287
6545
|
} },
|
|
6288
6546
|
membershipOfferCheckout: { attrs: {
|
|
6289
|
-
id:
|
|
6547
|
+
id: id9(),
|
|
6290
6548
|
appId: key(),
|
|
6291
6549
|
chapterId: key(),
|
|
6292
6550
|
environment: key(),
|
|
@@ -6297,20 +6555,21 @@ var membershipOfferEntities = {
|
|
|
6297
6555
|
offerDigest: attr8("string"),
|
|
6298
6556
|
quoteDigest: attr8("string"),
|
|
6299
6557
|
priceId: attr8("string"),
|
|
6300
|
-
boundAt: attr8("number")
|
|
6558
|
+
boundAt: attr8("number"),
|
|
6559
|
+
namedSeat: attr8("json", true)
|
|
6301
6560
|
} }
|
|
6302
6561
|
};
|
|
6303
6562
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
6304
6563
|
|
|
6305
6564
|
// src/schema-membership-authority.ts
|
|
6306
6565
|
var attr9 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
6307
|
-
var
|
|
6566
|
+
var id10 = () => attr9("string", false, true, true);
|
|
6308
6567
|
var key2 = (optional = false) => attr9("string", optional, false, true);
|
|
6309
6568
|
var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
|
|
6310
6569
|
var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
|
|
6311
6570
|
var membershipAuthorityEntities = {
|
|
6312
6571
|
networkMembership: { attrs: {
|
|
6313
|
-
id:
|
|
6572
|
+
id: id10(),
|
|
6314
6573
|
membershipId: key2(),
|
|
6315
6574
|
...scope(),
|
|
6316
6575
|
personId: key2(true),
|
|
@@ -6350,7 +6609,7 @@ var membershipAuthorityEntities = {
|
|
|
6350
6609
|
restartId: key2(true)
|
|
6351
6610
|
} },
|
|
6352
6611
|
membershipRestart: { attrs: {
|
|
6353
|
-
id:
|
|
6612
|
+
id: id10(),
|
|
6354
6613
|
...scope(),
|
|
6355
6614
|
membershipId: key2(),
|
|
6356
6615
|
restartId: key2(),
|
|
@@ -6360,7 +6619,7 @@ var membershipAuthorityEntities = {
|
|
|
6360
6619
|
createdAt: attr9("number")
|
|
6361
6620
|
} },
|
|
6362
6621
|
membershipPurchaseHistory: { attrs: {
|
|
6363
|
-
id:
|
|
6622
|
+
id: id10(),
|
|
6364
6623
|
...scope(),
|
|
6365
6624
|
membershipId: key2(),
|
|
6366
6625
|
providerSubscriptionId: key2(),
|
|
@@ -6369,7 +6628,7 @@ var membershipAuthorityEntities = {
|
|
|
6369
6628
|
archivedAt: attr9("number")
|
|
6370
6629
|
} },
|
|
6371
6630
|
networkEntitlement: { attrs: {
|
|
6372
|
-
id:
|
|
6631
|
+
id: id10(),
|
|
6373
6632
|
...scope(),
|
|
6374
6633
|
membershipId: key2(),
|
|
6375
6634
|
seatId: key2(true),
|
|
@@ -6386,10 +6645,12 @@ var membershipAuthorityEntities = {
|
|
|
6386
6645
|
journeyApprovedAt: attr9("number", true)
|
|
6387
6646
|
} },
|
|
6388
6647
|
networkSeat: { attrs: {
|
|
6389
|
-
id:
|
|
6648
|
+
id: id10(),
|
|
6390
6649
|
...scope(),
|
|
6391
6650
|
primaryMembershipId: key2(),
|
|
6392
|
-
purchaserUserId: key2(),
|
|
6651
|
+
purchaserUserId: key2(true),
|
|
6652
|
+
purchaserEmailNormalized: key2(true),
|
|
6653
|
+
combinedInvoice: attr9("json", true),
|
|
6393
6654
|
recipientName: attr9("string"),
|
|
6394
6655
|
recipientEmailNormalized: key2(),
|
|
6395
6656
|
recipientClaimId: key2(),
|
|
@@ -6421,9 +6682,9 @@ var membershipAuthorityEntities = {
|
|
|
6421
6682
|
refundedCents: attr9("number", true),
|
|
6422
6683
|
providerRefundIds: attr9("json", true)
|
|
6423
6684
|
} },
|
|
6424
|
-
networkSeatRecipient: { attrs: { id:
|
|
6685
|
+
networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
|
|
6425
6686
|
membershipEffect: { attrs: {
|
|
6426
|
-
id:
|
|
6687
|
+
id: id10(),
|
|
6427
6688
|
chapterId: key2(),
|
|
6428
6689
|
environment: key2(),
|
|
6429
6690
|
runtime: key2(),
|
|
@@ -6548,14 +6809,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6548
6809
|
continue;
|
|
6549
6810
|
}
|
|
6550
6811
|
unknownKeys(raw2, PAGE_KEYS, path, errors);
|
|
6551
|
-
const
|
|
6812
|
+
const id11 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
6552
6813
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
6553
6814
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
6554
|
-
if (
|
|
6815
|
+
if (id11 && !ID.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6555
6816
|
if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
6556
|
-
if (ids2.has(
|
|
6817
|
+
if (ids2.has(id11) || knownParentIds.has(id11)) errors.push(`${path}.id duplicates page "${id11}"`);
|
|
6557
6818
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
6558
|
-
ids2.add(
|
|
6819
|
+
ids2.add(id11);
|
|
6559
6820
|
slugs.add(slug);
|
|
6560
6821
|
const navRaw = record(raw2.navigation);
|
|
6561
6822
|
let navigation = { section: "General", order: index2 };
|
|
@@ -6611,7 +6872,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6611
6872
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
6612
6873
|
}
|
|
6613
6874
|
}
|
|
6614
|
-
pages.push({ id:
|
|
6875
|
+
pages.push({ id: id11, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
6615
6876
|
}
|
|
6616
6877
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
6617
6878
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -6817,8 +7078,8 @@ function resourceValue(value, path, errors) {
|
|
|
6817
7078
|
return null;
|
|
6818
7079
|
}
|
|
6819
7080
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
6820
|
-
const
|
|
6821
|
-
if (
|
|
7081
|
+
const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
7082
|
+
if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
6822
7083
|
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
6823
7084
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
6824
7085
|
const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -6875,7 +7136,7 @@ function resourceValue(value, path, errors) {
|
|
|
6875
7136
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
6876
7137
|
...promotedFrom ? { promotedFrom } : {}
|
|
6877
7138
|
};
|
|
6878
|
-
return { id:
|
|
7139
|
+
return { id: id11, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
6879
7140
|
}
|
|
6880
7141
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
6881
7142
|
const errors = [];
|
|
@@ -6906,7 +7167,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
6906
7167
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
6907
7168
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
6908
7169
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
6909
|
-
const ids2 = resources.map(({ id:
|
|
7170
|
+
const ids2 = resources.map(({ id: id11 }) => id11);
|
|
6910
7171
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
6911
7172
|
for (const resource of resources) {
|
|
6912
7173
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -6993,6 +7254,41 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
6993
7254
|
return { document, digest: await builderLibraryDigest(document) };
|
|
6994
7255
|
}
|
|
6995
7256
|
|
|
7257
|
+
// src/email-retirement.ts
|
|
7258
|
+
function isRetiredEmailCopy(subject, text6) {
|
|
7259
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
7260
|
+
const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
7261
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
7262
|
+
}
|
|
7263
|
+
|
|
7264
|
+
// src/email.ts
|
|
7265
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
7266
|
+
|
|
7267
|
+
// src/email-events.ts
|
|
7268
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
7269
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
7270
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
7271
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
7272
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
7273
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
7274
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
7275
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
7276
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
7277
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
7278
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
7279
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
7280
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
7281
|
+
};
|
|
7282
|
+
function chapterEmailEvents(chapter) {
|
|
7283
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
7284
|
+
label: key3,
|
|
7285
|
+
trigger: "Administrator sends a message",
|
|
7286
|
+
audience: "Selected contact",
|
|
7287
|
+
variables: spec.vars
|
|
7288
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
7289
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
7290
|
+
}
|
|
7291
|
+
|
|
6996
7292
|
// src/email.ts
|
|
6997
7293
|
function render(template, vars) {
|
|
6998
7294
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -7016,19 +7312,158 @@ function isAlreadySent(priorRows) {
|
|
|
7016
7312
|
function planDelivery(input) {
|
|
7017
7313
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
7018
7314
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
7019
|
-
if (tpl.enabled
|
|
7315
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
7316
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
7317
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
7020
7318
|
const vars = groupVars(input.group, input.vars);
|
|
7319
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
7320
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7321
|
+
const issues = (0, import_crm5.validateEmailCopy)(tpl, {
|
|
7322
|
+
allowedVariables: event2?.variables,
|
|
7323
|
+
optionalVariables: event2?.optionalVariables,
|
|
7324
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
7325
|
+
values: vars,
|
|
7326
|
+
tierId: vars.tierId
|
|
7327
|
+
});
|
|
7328
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
7021
7329
|
const isProd = input.envName === "prod";
|
|
7022
7330
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
7023
7331
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
7024
7332
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
7025
|
-
const
|
|
7333
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
7334
|
+
const renderedText = render(tpl.text, vars);
|
|
7335
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
7336
|
+
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
7337
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
7026
7338
|
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
7027
7339
|
|
|
7028
|
-
` +
|
|
7340
|
+
` + renderedText : renderedText;
|
|
7029
7341
|
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
7030
7342
|
}
|
|
7031
7343
|
|
|
7344
|
+
// src/email-readiness.ts
|
|
7345
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
7346
|
+
function validateEmailReadiness(input) {
|
|
7347
|
+
const issues = [];
|
|
7348
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
7349
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
7350
|
+
}
|
|
7351
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
7352
|
+
for (const tier of input.tiers ?? []) {
|
|
7353
|
+
if (!(0, import_crm6.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
7354
|
+
}
|
|
7355
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm6.isEmailTierName)(tier.name, tier.id));
|
|
7356
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
7357
|
+
const events2 = Object.entries(input.events).map(([key3, event2]) => {
|
|
7358
|
+
const template = input.templates[key3];
|
|
7359
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm6.validateEmailCopy)(template, { allowedVariables: event2.variables });
|
|
7360
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
7361
|
+
if (template) {
|
|
7362
|
+
const used = [...`${template.subject ?? ""}
|
|
7363
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
7364
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
7365
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7369
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7370
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
7371
|
+
});
|
|
7372
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
// src/notify.ts
|
|
7376
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
7377
|
+
|
|
7378
|
+
// src/email-event-values.ts
|
|
7379
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
7380
|
+
|
|
7381
|
+
// src/tiers-store.ts
|
|
7382
|
+
function rowToTier(row2, groupId) {
|
|
7383
|
+
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") {
|
|
7384
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
7385
|
+
}
|
|
7386
|
+
return {
|
|
7387
|
+
id: String(row2.id ?? ""),
|
|
7388
|
+
groupId: String(row2.groupId ?? groupId),
|
|
7389
|
+
name: String(row2.name ?? ""),
|
|
7390
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
7391
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
7392
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
7393
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
7394
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
7395
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
7396
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
7397
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
7398
|
+
active: row2.active !== false
|
|
7399
|
+
};
|
|
7400
|
+
}
|
|
7401
|
+
async function loadTiers(db, group) {
|
|
7402
|
+
const groupId = String(group.id ?? "");
|
|
7403
|
+
let rows3;
|
|
7404
|
+
try {
|
|
7405
|
+
rows3 = (await db.query({
|
|
7406
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
7407
|
+
})).tiers;
|
|
7408
|
+
} catch {
|
|
7409
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
7410
|
+
}
|
|
7411
|
+
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
7412
|
+
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
7413
|
+
return resolveTiers(group, list);
|
|
7414
|
+
}
|
|
7415
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
7416
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
7417
|
+
}
|
|
7418
|
+
|
|
7419
|
+
// src/notification-variables.ts
|
|
7420
|
+
async function frozenCheckoutVariables(db, app) {
|
|
7421
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
7422
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
7423
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
7424
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
7425
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
7426
|
+
let formatted = "";
|
|
7427
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
7428
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
7429
|
+
}
|
|
7430
|
+
return {
|
|
7431
|
+
paymentAmount: formatted,
|
|
7432
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
7433
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
async function catalogTierName(db, group, app) {
|
|
7437
|
+
try {
|
|
7438
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
7439
|
+
} catch {
|
|
7440
|
+
return "";
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
async function checkoutVariables(db, group, app) {
|
|
7444
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
7445
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
7446
|
+
}
|
|
7447
|
+
|
|
7448
|
+
// src/email-event-values.ts
|
|
7449
|
+
async function emailTierVariables(db, input) {
|
|
7450
|
+
try {
|
|
7451
|
+
const [apps, groups2] = await Promise.all([
|
|
7452
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
7453
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
7454
|
+
]);
|
|
7455
|
+
const app = apps.applications?.[0], group = groups2.groups?.[0];
|
|
7456
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
7457
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
7458
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
7459
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
7460
|
+
const tierName = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
|
|
7461
|
+
return { tierName, tier: tierName, tierId };
|
|
7462
|
+
} catch {
|
|
7463
|
+
return { tierName: "", tier: "" };
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
|
|
7032
7467
|
// src/notify.ts
|
|
7033
7468
|
function fromAddress(configured, name) {
|
|
7034
7469
|
const display = name?.trim();
|
|
@@ -7046,24 +7481,27 @@ async function sendTemplated(deps, input) {
|
|
|
7046
7481
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
7047
7482
|
const override = deps.debugEmail?.trim();
|
|
7048
7483
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
7484
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7485
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
7049
7486
|
const decision = planDelivery({
|
|
7050
7487
|
envName: deps.envName,
|
|
7051
7488
|
group,
|
|
7052
7489
|
template: input.template,
|
|
7053
7490
|
to: input.to,
|
|
7054
|
-
vars
|
|
7491
|
+
vars,
|
|
7055
7492
|
cloudflareReady,
|
|
7056
|
-
force: input.force
|
|
7493
|
+
force: input.force,
|
|
7494
|
+
event: event2
|
|
7057
7495
|
});
|
|
7058
7496
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
7059
7497
|
const previous = prior[0];
|
|
7060
7498
|
if (previous && (previous.groupId !== input.group.id || previous.template !== input.template || previous.to !== decision.to || previous.transport !== decision.transport || previous.applicationId !== input.applicationId)) return { sent: false, reason: "delivery-identity-conflict" };
|
|
7061
7499
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
7062
|
-
const
|
|
7500
|
+
const id11 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
7063
7501
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
7064
7502
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
7065
7503
|
const row2 = {
|
|
7066
|
-
id:
|
|
7504
|
+
id: id11,
|
|
7067
7505
|
groupId: input.group.id,
|
|
7068
7506
|
to: decision.to,
|
|
7069
7507
|
template: input.template,
|
|
@@ -7078,16 +7516,18 @@ async function sendTemplated(deps, input) {
|
|
|
7078
7516
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
7079
7517
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
7080
7518
|
};
|
|
7519
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
7520
|
+
if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
7081
7521
|
try {
|
|
7082
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
7083
|
-
mutationId: `email-claim:${
|
|
7522
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: row2 }], {
|
|
7523
|
+
mutationId: `email-claim:${id11}:${attempt}`,
|
|
7084
7524
|
guards: [previous ? {
|
|
7085
7525
|
ns: "emailLog",
|
|
7086
|
-
id:
|
|
7526
|
+
id: id11,
|
|
7087
7527
|
exists: true,
|
|
7088
7528
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
7089
7529
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
7090
|
-
} : { ns: "emailLog", id:
|
|
7530
|
+
} : { ns: "emailLog", id: id11, exists: false }]
|
|
7091
7531
|
});
|
|
7092
7532
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
7093
7533
|
} catch (error) {
|
|
@@ -7107,16 +7547,16 @@ async function sendTemplated(deps, input) {
|
|
|
7107
7547
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
7108
7548
|
messageId = response.messageId;
|
|
7109
7549
|
} catch {
|
|
7110
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
7111
|
-
guards: [{ ns: "emailLog", id:
|
|
7550
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
7551
|
+
guards: [{ ns: "emailLog", id: id11, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
7112
7552
|
});
|
|
7113
7553
|
return { sent: false, reason: "delivery-uncertain" };
|
|
7114
7554
|
}
|
|
7115
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
7116
|
-
mutationId: `email-confirmed:${
|
|
7555
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
7556
|
+
mutationId: `email-confirmed:${id11}:${attempt}`,
|
|
7117
7557
|
guards: [{
|
|
7118
7558
|
ns: "emailLog",
|
|
7119
|
-
id:
|
|
7559
|
+
id: id11,
|
|
7120
7560
|
exists: true,
|
|
7121
7561
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
7122
7562
|
}]
|
|
@@ -7172,7 +7612,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
|
|
|
7172
7612
|
}
|
|
7173
7613
|
|
|
7174
7614
|
// src/payments.ts
|
|
7175
|
-
var
|
|
7615
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
7176
7616
|
function paymentsReady(group, hasSecretKey) {
|
|
7177
7617
|
return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
|
|
7178
7618
|
}
|
|
@@ -7231,35 +7671,35 @@ function findGiftRef(obj) {
|
|
|
7231
7671
|
...chapterId ? { chapterId } : {}
|
|
7232
7672
|
};
|
|
7233
7673
|
}
|
|
7234
|
-
function normalizeWebhookEvent(
|
|
7235
|
-
const obj =
|
|
7236
|
-
if (
|
|
7674
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
7675
|
+
const obj = event2.data?.object ?? {};
|
|
7676
|
+
if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
|
|
7237
7677
|
const gift = findGiftRef(obj);
|
|
7238
7678
|
if (gift.giftIntentId) {
|
|
7239
|
-
const { giftIntentId, providerObjectId, ...
|
|
7240
|
-
if (
|
|
7241
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
7679
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
7680
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
7681
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
7242
7682
|
}
|
|
7243
|
-
if (
|
|
7244
|
-
if (
|
|
7245
|
-
return { kind: "ignored", type:
|
|
7683
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
7684
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
7685
|
+
return { kind: "ignored", type: event2.type };
|
|
7246
7686
|
}
|
|
7247
7687
|
const ref = findApplicationRef(obj);
|
|
7248
7688
|
const seatId = obj.metadata?.seatId;
|
|
7249
7689
|
if (typeof seatId === "string" && seatId) {
|
|
7250
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
7251
|
-
if (
|
|
7252
|
-
if (
|
|
7253
|
-
return { kind: "ignored", type:
|
|
7690
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7691
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
7692
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
7693
|
+
return { kind: "ignored", type: event2.type };
|
|
7254
7694
|
}
|
|
7255
|
-
if (
|
|
7256
|
-
switch (
|
|
7695
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7696
|
+
switch (event2.type) {
|
|
7257
7697
|
case "invoice.payment_failed":
|
|
7258
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
7698
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
7259
7699
|
case "invoice.paid":
|
|
7260
7700
|
case "invoice.payment_succeeded": {
|
|
7261
7701
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
7262
|
-
return { kind: "ignored", type:
|
|
7702
|
+
return { kind: "ignored", type: event2.type };
|
|
7263
7703
|
}
|
|
7264
7704
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
7265
7705
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -7273,11 +7713,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
7273
7713
|
case "charge.refunded":
|
|
7274
7714
|
return { kind: "refunded", ...ref };
|
|
7275
7715
|
case "customer.subscription.updated":
|
|
7276
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
7716
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
7277
7717
|
case "customer.subscription.deleted":
|
|
7278
7718
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
7279
7719
|
default:
|
|
7280
|
-
return { kind: "ignored", type:
|
|
7720
|
+
return { kind: "ignored", type: event2.type };
|
|
7281
7721
|
}
|
|
7282
7722
|
}
|
|
7283
7723
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -7374,14 +7814,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
7374
7814
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
7375
7815
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
7376
7816
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
7377
|
-
const
|
|
7817
|
+
const id11 = typeof grant.id === "string" ? grant.id : "";
|
|
7378
7818
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
7379
7819
|
const normalized = normalizeAdmissionEmail(email);
|
|
7380
7820
|
const decision = grant.decision;
|
|
7381
7821
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
7382
7822
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
7383
|
-
if (!ID3.test(
|
|
7384
|
-
if (seenIds.has(
|
|
7823
|
+
if (!ID3.test(id11)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
7824
|
+
if (seenIds.has(id11)) errors.push(`document.grants repeats id "${id11}"`);
|
|
7385
7825
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
7386
7826
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
7387
7827
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -7391,10 +7831,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
7391
7831
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
7392
7832
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
7393
7833
|
}
|
|
7394
|
-
seenIds.add(
|
|
7834
|
+
seenIds.add(id11);
|
|
7395
7835
|
seenEmails.add(normalized);
|
|
7396
7836
|
grants.push({
|
|
7397
|
-
id:
|
|
7837
|
+
id: id11,
|
|
7398
7838
|
email,
|
|
7399
7839
|
decision,
|
|
7400
7840
|
...note ? { note } : {},
|
|
@@ -7466,8 +7906,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7466
7906
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
7467
7907
|
const ops = [];
|
|
7468
7908
|
for (const row2 of current) {
|
|
7469
|
-
const
|
|
7470
|
-
if (
|
|
7909
|
+
const id11 = String(row2.id ?? "");
|
|
7910
|
+
if (id11 && !incomingIds.has(id11)) ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
|
|
7471
7911
|
active: false,
|
|
7472
7912
|
revokedAt: receivedAt,
|
|
7473
7913
|
revokedBy: document.source.actor,
|
|
@@ -7476,10 +7916,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7476
7916
|
} });
|
|
7477
7917
|
}
|
|
7478
7918
|
for (const grant of document.grants) {
|
|
7479
|
-
const
|
|
7480
|
-
const existing = current.find((row2) => row2.id ===
|
|
7481
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
7482
|
-
id:
|
|
7919
|
+
const id11 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
7920
|
+
const existing = current.find((row2) => row2.id === id11);
|
|
7921
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
|
|
7922
|
+
id: id11,
|
|
7483
7923
|
sourceGrantId: grant.id,
|
|
7484
7924
|
sourceScope: "parent",
|
|
7485
7925
|
sourceId: document.source.appId,
|
|
@@ -7500,7 +7940,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7500
7940
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
7501
7941
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
7502
7942
|
];
|
|
7503
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
7943
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id11, attrs: retract });
|
|
7504
7944
|
}
|
|
7505
7945
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
7506
7946
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -7571,10 +8011,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
7571
8011
|
}
|
|
7572
8012
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
7573
8013
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7574
|
-
const
|
|
7575
|
-
if (
|
|
7576
|
-
if (ids2.has(
|
|
7577
|
-
ids2.add(
|
|
8014
|
+
const id11 = text4(item.id, `${path}.id`, errors, 80);
|
|
8015
|
+
if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8016
|
+
if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
8017
|
+
ids2.add(id11);
|
|
7578
8018
|
const targetRaw = record3(item.target);
|
|
7579
8019
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
7580
8020
|
if (targetRaw?.type === "tier") {
|
|
@@ -7600,7 +8040,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
7600
8040
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
7601
8041
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
7602
8042
|
offers.push({
|
|
7603
|
-
id:
|
|
8043
|
+
id: id11,
|
|
7604
8044
|
target,
|
|
7605
8045
|
billingMode: item.billingMode,
|
|
7606
8046
|
stripePriceId,
|
|
@@ -7625,10 +8065,10 @@ function findGiftOffer(catalog, offerId, now) {
|
|
|
7625
8065
|
}
|
|
7626
8066
|
|
|
7627
8067
|
// src/network.ts
|
|
7628
|
-
var
|
|
8068
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
7629
8069
|
|
|
7630
8070
|
// src/crm-person-projection.ts
|
|
7631
|
-
var
|
|
8071
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
7632
8072
|
async function ensureProjectedPerson(deps, input, options = {}) {
|
|
7633
8073
|
const email = String(input.email ?? "").trim().toLowerCase();
|
|
7634
8074
|
if (!email) throw new Error("crm_projection_email_required");
|
|
@@ -7644,10 +8084,10 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
7644
8084
|
const existing = await find();
|
|
7645
8085
|
if (existing) return { record: existing, created: false };
|
|
7646
8086
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
7647
|
-
const
|
|
8087
|
+
const id11 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7648
8088
|
try {
|
|
7649
|
-
const created = await (0,
|
|
7650
|
-
id:
|
|
8089
|
+
const created = await (0, import_crm9.createRecord)({ ...deps, db: deps.db }, {
|
|
8090
|
+
id: id11,
|
|
7651
8091
|
type: "person",
|
|
7652
8092
|
input: { ...input, email },
|
|
7653
8093
|
...options
|
|
@@ -7662,7 +8102,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
7662
8102
|
}
|
|
7663
8103
|
|
|
7664
8104
|
// src/applicant-projection.ts
|
|
7665
|
-
var
|
|
8105
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
7666
8106
|
|
|
7667
8107
|
// src/crm-application-fields.ts
|
|
7668
8108
|
function applicationCrmFields(crm, app) {
|
|
@@ -7681,10 +8121,10 @@ async function upsertPerson(deps, opts) {
|
|
|
7681
8121
|
const email = opts.email.toLowerCase();
|
|
7682
8122
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
7683
8123
|
const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
|
|
7684
|
-
if (!created) await (0,
|
|
8124
|
+
if (!created) await (0, import_crm10.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
|
|
7685
8125
|
const initial = deps.crm.initialStage("person");
|
|
7686
8126
|
if (!created && record4.stage == null && initial) {
|
|
7687
|
-
await (0,
|
|
8127
|
+
await (0, import_crm10.setStage)(crmDeps, {
|
|
7688
8128
|
id: String(record4.id),
|
|
7689
8129
|
to: initial,
|
|
7690
8130
|
authorId: "system",
|
|
@@ -7807,7 +8247,7 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
7807
8247
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
7808
8248
|
}
|
|
7809
8249
|
async function findSharedRecord(deps, record4, tag) {
|
|
7810
|
-
const structured = await (0,
|
|
8250
|
+
const structured = await (0, import_crm11.getRecordByOrigin)(
|
|
7811
8251
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
7812
8252
|
record4.sourceId,
|
|
7813
8253
|
record4.sourceRecordId
|
|
@@ -7859,15 +8299,15 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7859
8299
|
const existing = await findSharedRecord(deps, record4, tag);
|
|
7860
8300
|
let recordId;
|
|
7861
8301
|
if (existing && typeof existing.id === "string") {
|
|
7862
|
-
await (0,
|
|
8302
|
+
await (0, import_crm11.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
|
|
7863
8303
|
recordId = existing.id;
|
|
7864
8304
|
} else if (record4.type === "person" && typeof record4.input.email === "string") {
|
|
7865
8305
|
const projected = await ensureProjectedPerson(deps, record4.input);
|
|
7866
8306
|
recordId = String(projected.record.id);
|
|
7867
|
-
if (!projected.created) await (0,
|
|
8307
|
+
if (!projected.created) await (0, import_crm11.updateRecord)(crmDeps, { id: recordId, input: record4.input });
|
|
7868
8308
|
} else {
|
|
7869
8309
|
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
7870
|
-
await (0,
|
|
8310
|
+
await (0, import_crm11.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
7871
8311
|
type: record4.type,
|
|
7872
8312
|
input: record4.input,
|
|
7873
8313
|
mutationId: `share-create:${tag}`
|
|
@@ -7877,7 +8317,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7877
8317
|
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
7878
8318
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
7879
8319
|
);
|
|
7880
|
-
await (0,
|
|
8320
|
+
await (0, import_crm11.upsertRecordOrigin)(crmDeps, {
|
|
7881
8321
|
recordId,
|
|
7882
8322
|
sourceId: record4.sourceId,
|
|
7883
8323
|
sourceRecordId: record4.sourceRecordId,
|
|
@@ -7888,7 +8328,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
7888
8328
|
}
|
|
7889
8329
|
|
|
7890
8330
|
// src/crm-sync.ts
|
|
7891
|
-
var
|
|
8331
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
7892
8332
|
|
|
7893
8333
|
// src/crm-account-projection.ts
|
|
7894
8334
|
async function syncAccountToCrm(deps, userId) {
|
|
@@ -7973,9 +8413,9 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
7973
8413
|
const stage = opts.stage || void 0;
|
|
7974
8414
|
const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
|
|
7975
8415
|
const recordId = String(existing.id);
|
|
7976
|
-
if (!created) await (0,
|
|
8416
|
+
if (!created) await (0, import_crm12.updateRecord)(recordDeps, { id: recordId, input });
|
|
7977
8417
|
if (!created && stage && existing.stage !== stage) {
|
|
7978
|
-
await (0,
|
|
8418
|
+
await (0, import_crm12.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
|
|
7979
8419
|
}
|
|
7980
8420
|
const billing = billingColumns(opts.app);
|
|
7981
8421
|
await deps.db.transact([
|
|
@@ -7983,7 +8423,7 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
7983
8423
|
// Optional typed dates are removed with a retract, never an invalid null.
|
|
7984
8424
|
...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
|
|
7985
8425
|
]);
|
|
7986
|
-
await (0,
|
|
8426
|
+
await (0, import_crm12.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
7987
8427
|
return recordId;
|
|
7988
8428
|
}
|
|
7989
8429
|
async function backfillCrm(deps) {
|
|
@@ -8070,9 +8510,9 @@ function subAnnualCents(sub) {
|
|
|
8070
8510
|
}
|
|
8071
8511
|
|
|
8072
8512
|
// src/payments-stripe.ts
|
|
8073
|
-
var
|
|
8513
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
8074
8514
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8075
|
-
return (0,
|
|
8515
|
+
return (0, import_stripe12.stripeRequest)(
|
|
8076
8516
|
{ secretKey: sk },
|
|
8077
8517
|
method,
|
|
8078
8518
|
path,
|
|
@@ -8150,8 +8590,8 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
|
|
|
8150
8590
|
return res.ok;
|
|
8151
8591
|
}
|
|
8152
8592
|
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
8153
|
-
const
|
|
8154
|
-
return
|
|
8593
|
+
const id11 = await existingClerkUserId(secretKey, email, fetchImpl);
|
|
8594
|
+
return id11 ? updateClerkUserMetadata(secretKey, id11, input, fetchImpl) : false;
|
|
8155
8595
|
}
|
|
8156
8596
|
async function existingClerkUserId(secretKey, email, fetchImpl) {
|
|
8157
8597
|
const auth = { authorization: `Bearer ${secretKey}` };
|
|
@@ -8224,10 +8664,10 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch, source =
|
|
|
8224
8664
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
8225
8665
|
return user ? toRecord(user, source) : null;
|
|
8226
8666
|
}
|
|
8227
|
-
async function clerkGetUser(secretKey,
|
|
8228
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
8667
|
+
async function clerkGetUser(secretKey, id11, fetchImpl = fetch, source = "public") {
|
|
8668
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id11)}`, secretKey, fetchImpl).catch(() => null);
|
|
8229
8669
|
const record4 = data ? toRecord(data, source) : null;
|
|
8230
|
-
return record4?.id ===
|
|
8670
|
+
return record4?.id === id11 ? record4 : null;
|
|
8231
8671
|
}
|
|
8232
8672
|
async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
8233
8673
|
const out = [];
|
|
@@ -8243,8 +8683,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
|
8243
8683
|
}
|
|
8244
8684
|
return out;
|
|
8245
8685
|
}
|
|
8246
|
-
async function clerkSetRole(secretKey,
|
|
8247
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
8686
|
+
async function clerkSetRole(secretKey, id11, role, fetchImpl = fetch, source = "public") {
|
|
8687
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id11)}/metadata`, {
|
|
8248
8688
|
method: "PATCH",
|
|
8249
8689
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
8250
8690
|
redirect: "manual",
|
|
@@ -8261,7 +8701,7 @@ function planPrivateRoleMigration(input) {
|
|
|
8261
8701
|
if (input.environment !== "dev") blockers.push("dev_only");
|
|
8262
8702
|
if (!input.ladder.length || new Set(input.ladder).size !== input.ladder.length || input.ladder.some((role) => !role || role === "super_admin")) blockers.push("invalid_role_ladder");
|
|
8263
8703
|
const expected = new Set(input.expectedRuntimes);
|
|
8264
|
-
if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((
|
|
8704
|
+
if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((id11) => !id11)) blockers.push("runtime_inventory_required");
|
|
8265
8705
|
if (input.runtimes.length !== expected.size || new Set(input.runtimes.map((r) => r.runtime)).size !== expected.size || input.runtimes.some((r) => !expected.has(r.runtime))) blockers.push("runtime_inventory_mismatch");
|
|
8266
8706
|
for (const runtime of input.expectedRuntimes) {
|
|
8267
8707
|
const proof = input.runtimes.find((r) => r.runtime === runtime);
|
|
@@ -8291,7 +8731,7 @@ function planPrivateRoleMigration(input) {
|
|
|
8291
8731
|
if (privateRole === role && !Object.hasOwn(user.publicMetadata, "role")) alreadyCurrent++;
|
|
8292
8732
|
else changes.push({ userId: user.id, role, publicRole, privateRole });
|
|
8293
8733
|
}
|
|
8294
|
-
if (!input.protectedSuperAdminUserIds.some((
|
|
8734
|
+
if (!input.protectedSuperAdminUserIds.some((id11) => ids2.has(id11))) blockers.push("protected_operator_grant_required");
|
|
8295
8735
|
return { ready: blockers.length === 0, blockers, changes, alreadyCurrent, totalUsers: input.users.length };
|
|
8296
8736
|
}
|
|
8297
8737
|
|
|
@@ -8650,44 +9090,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
8650
9090
|
};
|
|
8651
9091
|
}
|
|
8652
9092
|
|
|
8653
|
-
// src/tiers-store.ts
|
|
8654
|
-
function rowToTier(row2, groupId) {
|
|
8655
|
-
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || typeof row2.id !== "string" || !row2.id || row2.groupId !== void 0 && row2.groupId !== groupId || !Number.isSafeInteger(row2.priceCents) || Number(row2.priceCents) < 0 || row2.currency !== void 0 && (typeof row2.currency !== "string" || !/^[a-z]{3}$/.test(row2.currency)) || row2.interval !== void 0 && row2.interval !== "month" && row2.interval !== "year") {
|
|
8656
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
8657
|
-
}
|
|
8658
|
-
return {
|
|
8659
|
-
id: String(row2.id ?? ""),
|
|
8660
|
-
groupId: String(row2.groupId ?? groupId),
|
|
8661
|
-
name: String(row2.name ?? ""),
|
|
8662
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
8663
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
8664
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
8665
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
8666
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
8667
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
8668
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
8669
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
8670
|
-
active: row2.active !== false
|
|
8671
|
-
};
|
|
8672
|
-
}
|
|
8673
|
-
async function loadTiers(db, group) {
|
|
8674
|
-
const groupId = String(group.id ?? "");
|
|
8675
|
-
let rows3;
|
|
8676
|
-
try {
|
|
8677
|
-
rows3 = (await db.query({
|
|
8678
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
8679
|
-
})).tiers;
|
|
8680
|
-
} catch {
|
|
8681
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
8682
|
-
}
|
|
8683
|
-
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
8684
|
-
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8685
|
-
return resolveTiers(group, list);
|
|
8686
|
-
}
|
|
8687
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
8688
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
8689
|
-
}
|
|
8690
|
-
|
|
8691
9093
|
// src/signup-control-contract.ts
|
|
8692
9094
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
8693
9095
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
@@ -8907,10 +9309,10 @@ function tiersOf(value, errors) {
|
|
|
8907
9309
|
return [];
|
|
8908
9310
|
}
|
|
8909
9311
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
8910
|
-
const
|
|
8911
|
-
if (
|
|
8912
|
-
if (seen.has(
|
|
8913
|
-
seen.add(
|
|
9312
|
+
const id11 = text5(item.id, `${path}.id`, errors, 80);
|
|
9313
|
+
if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9314
|
+
if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
9315
|
+
seen.add(id11);
|
|
8914
9316
|
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
8915
9317
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8916
9318
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -8931,7 +9333,7 @@ function tiersOf(value, errors) {
|
|
|
8931
9333
|
}
|
|
8932
9334
|
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8933
9335
|
return [{
|
|
8934
|
-
id:
|
|
9336
|
+
id: id11,
|
|
8935
9337
|
name,
|
|
8936
9338
|
available: item.available === true,
|
|
8937
9339
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -8993,17 +9395,17 @@ function policyOf(value, errors, tiers2) {
|
|
|
8993
9395
|
if (input.tierPolicies !== void 0) {
|
|
8994
9396
|
const overrides = recordOf(input.tierPolicies);
|
|
8995
9397
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
8996
|
-
else for (const [
|
|
8997
|
-
const path = `document.policy.tierPolicies.${
|
|
8998
|
-
const
|
|
8999
|
-
if (!tiers2.some((tier) => tier.id ===
|
|
9398
|
+
else for (const [id11, raw2] of Object.entries(overrides)) {
|
|
9399
|
+
const path = `document.policy.tierPolicies.${id11}`;
|
|
9400
|
+
const policy2 = recordOf(raw2);
|
|
9401
|
+
if (!tiers2.some((tier) => tier.id === id11) || !policy2) {
|
|
9000
9402
|
errors.push(`${path} must name a declared tier and policy`);
|
|
9001
9403
|
continue;
|
|
9002
9404
|
}
|
|
9003
|
-
unknownKeys3(
|
|
9004
|
-
tierPolicies[
|
|
9005
|
-
refundPolicyText: text5(
|
|
9006
|
-
...
|
|
9405
|
+
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
9406
|
+
tierPolicies[id11] = {
|
|
9407
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
9408
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
9007
9409
|
};
|
|
9008
9410
|
}
|
|
9009
9411
|
}
|
|
@@ -9058,7 +9460,7 @@ function validateSignupControlDocument(input) {
|
|
|
9058
9460
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
9059
9461
|
}
|
|
9060
9462
|
const application = applicationOf(root.application, errors);
|
|
9061
|
-
const
|
|
9463
|
+
const policy2 = policyOf(root.policy, errors, tiers2);
|
|
9062
9464
|
let scheduling;
|
|
9063
9465
|
if (root.scheduling !== void 0) {
|
|
9064
9466
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -9078,7 +9480,7 @@ function validateSignupControlDocument(input) {
|
|
|
9078
9480
|
tiers: tiers2,
|
|
9079
9481
|
membershipPage: page.value,
|
|
9080
9482
|
...application ? { application } : {},
|
|
9081
|
-
...
|
|
9483
|
+
...policy2 ? { policy: policy2 } : {},
|
|
9082
9484
|
...scheduling ? { scheduling } : {},
|
|
9083
9485
|
...theme ? { theme } : {},
|
|
9084
9486
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -9324,7 +9726,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
9324
9726
|
id: chapterId,
|
|
9325
9727
|
reason: "provider_identity",
|
|
9326
9728
|
paths: [
|
|
9327
|
-
...paidTierIds.map((
|
|
9729
|
+
...paidTierIds.map((id11) => `tiers[${id11}].stripePriceId`),
|
|
9328
9730
|
"groups.stripePublishableKey",
|
|
9329
9731
|
"applications.stripeCustomerId",
|
|
9330
9732
|
"applications.stripeSubscriptionId",
|
|
@@ -9366,9 +9768,9 @@ async function exportChapterPromotion(input) {
|
|
|
9366
9768
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
9367
9769
|
receiptDigests: [input.digest]
|
|
9368
9770
|
};
|
|
9369
|
-
const artifact = (kind,
|
|
9771
|
+
const artifact = (kind, id11, content) => ({
|
|
9370
9772
|
kind,
|
|
9371
|
-
id:
|
|
9773
|
+
id: id11,
|
|
9372
9774
|
sourceRevision: revision,
|
|
9373
9775
|
provenance: promotionProvenance,
|
|
9374
9776
|
content: (0, import_promotion.canonicalPromotionValue)(content)
|
|
@@ -9413,7 +9815,7 @@ var MEMBER_SYSTEM_RECORD_OWNERS = {
|
|
|
9413
9815
|
provider: "originating-provider",
|
|
9414
9816
|
protectedRoles: "odla-platform"
|
|
9415
9817
|
};
|
|
9416
|
-
var rows2 = (group) => group.fields.map((
|
|
9818
|
+
var rows2 = (group) => group.fields.map((id11) => ({ ...group, id: id11 }));
|
|
9417
9819
|
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
9418
9820
|
...rows2({
|
|
9419
9821
|
scope: "network",
|
|
@@ -9791,23 +10193,23 @@ function memberSystemAvailability(input) {
|
|
|
9791
10193
|
...!input.bnfFrontendReachable ? { reason: "bnf_frontend_unavailable" } : {}
|
|
9792
10194
|
};
|
|
9793
10195
|
}
|
|
9794
|
-
function classifyMemberStripeEvent(
|
|
10196
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
9795
10197
|
const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
9796
|
-
const supplied = keys2.filter((key3) =>
|
|
9797
|
-
if (supplied.some((key3) =>
|
|
10198
|
+
const supplied = keys2.filter((key3) => identity2[key3] !== void 0);
|
|
10199
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
9798
10200
|
if (supplied.length === keys2.length) return "apply";
|
|
9799
10201
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
9800
10202
|
}
|
|
9801
10203
|
function planMemberSystemEventReplay(events2) {
|
|
9802
10204
|
const seen = /* @__PURE__ */ new Map();
|
|
9803
10205
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
9804
|
-
for (const
|
|
9805
|
-
const digest = seen.get(
|
|
10206
|
+
for (const event2 of events2) {
|
|
10207
|
+
const digest = seen.get(event2.id);
|
|
9806
10208
|
if (digest === void 0) {
|
|
9807
|
-
seen.set(
|
|
9808
|
-
plan.accepted.push(
|
|
9809
|
-
} else if (digest ===
|
|
9810
|
-
else plan.collisions.push(
|
|
10209
|
+
seen.set(event2.id, event2.digest);
|
|
10210
|
+
plan.accepted.push(event2.id);
|
|
10211
|
+
} else if (digest === event2.digest) plan.replayed.push(event2.id);
|
|
10212
|
+
else plan.collisions.push(event2.id);
|
|
9811
10213
|
}
|
|
9812
10214
|
return plan;
|
|
9813
10215
|
}
|