@odla-ai/chapter 0.58.0 → 0.59.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 +63 -0
- package/dist/{chunk-FHKZVF5G.js → chunk-GADLTNJF.js} +4 -2
- package/dist/{chunk-FHKZVF5G.js.map → chunk-GADLTNJF.js.map} +1 -1
- package/dist/{chunk-L2IPS2FQ.js → chunk-WFTWT3V7.js} +141 -56
- package/dist/chunk-WFTWT3V7.js.map +1 -0
- package/dist/{chunk-CKC6X5H2.js → chunk-Y2H4VURI.js} +399 -308
- package/dist/chunk-Y2H4VURI.js.map +1 -0
- package/dist/{copy-context-DE4YMAbz.d.ts → copy-context-BuJBQafl.d.ts} +4 -0
- package/dist/index.cjs +218 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +214 -80
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +36 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +15 -3
- package/dist/ui/member/index.d.ts +33 -5
- package/dist/ui/member/index.js +10 -2
- package/dist/worker/index.cjs +611 -184
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +64 -2
- package/dist/worker/index.d.ts +64 -2
- package/dist/worker/index.js +604 -177
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CKC6X5H2.js.map +0 -1
- package/dist/chunk-L2IPS2FQ.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(worker_exports, {
|
|
|
24
24
|
DECLINABLE_FROM: () => DECLINABLE_FROM,
|
|
25
25
|
MEMBERSHIP_AUTHORITY_PATH: () => MEMBERSHIP_AUTHORITY_PATH,
|
|
26
26
|
MEMBERSHIP_CHAPTER_EFFECT_KINDS: () => MEMBERSHIP_CHAPTER_EFFECT_KINDS,
|
|
27
|
+
MEMBERSHIP_DISPUTED_STATUS: () => MEMBERSHIP_DISPUTED_STATUS,
|
|
27
28
|
NetworkTargetTransportError: () => NetworkTargetTransportError,
|
|
28
29
|
REGIFTABLE_SEAT_STATUSES: () => REGIFTABLE_SEAT_STATUSES,
|
|
29
30
|
chapterMembershipAccess: () => chapterMembershipAccess,
|
|
@@ -49,7 +50,10 @@ __export(worker_exports, {
|
|
|
49
50
|
namedSeatInvitationOpen: () => namedSeatInvitationOpen,
|
|
50
51
|
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
51
52
|
namedSeatRegiftable: () => namedSeatRegiftable,
|
|
53
|
+
readDisputedCharge: () => readDisputedCharge,
|
|
52
54
|
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
55
|
+
rechargeDisputedMembership: () => rechargeDisputedMembership,
|
|
56
|
+
reconcileMembershipDispute: () => reconcileMembershipDispute,
|
|
53
57
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
54
58
|
regiftNamedSeat: () => regiftNamedSeat,
|
|
55
59
|
releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
|
|
@@ -311,12 +315,37 @@ function networkCommercialAlerts(ctx, route) {
|
|
|
311
315
|
};
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
// src/worker-defer.ts
|
|
319
|
+
function deferWork(ctx, req, task) {
|
|
320
|
+
return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
|
|
321
|
+
}
|
|
322
|
+
function createDeferral() {
|
|
323
|
+
const executions = /* @__PURE__ */ new WeakMap();
|
|
324
|
+
const settle = (task) => task().then(() => void 0, (error) => {
|
|
325
|
+
console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
/** Remember the runtime's ExecutionContext for this request. */
|
|
329
|
+
bind(req, execution) {
|
|
330
|
+
if (execution) executions.set(req, execution);
|
|
331
|
+
},
|
|
332
|
+
/** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
|
|
333
|
+
defer(req, task) {
|
|
334
|
+
const execution = executions.get(req);
|
|
335
|
+
if (!execution) return settle(task);
|
|
336
|
+
execution.waitUntil(settle(task));
|
|
337
|
+
return Promise.resolve();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
314
342
|
// src/worker-context.ts
|
|
315
343
|
var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
|
|
316
344
|
function createWorkerContext(options) {
|
|
317
345
|
const { chapter } = options;
|
|
318
346
|
const auth = chapter.auth;
|
|
319
347
|
const crmBase = options.crmBasePath ?? "/api/crm";
|
|
348
|
+
const deferral = createDeferral();
|
|
320
349
|
let publicConfigCache = null;
|
|
321
350
|
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
322
351
|
async function getPublicConfig(env) {
|
|
@@ -421,6 +450,8 @@ function createWorkerContext(options) {
|
|
|
421
450
|
membershipAuthority: options.membershipAuthority,
|
|
422
451
|
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
423
452
|
recordError: options.recordError ?? consoleErrorRecorder,
|
|
453
|
+
bindExecution: deferral.bind,
|
|
454
|
+
defer: deferral.defer,
|
|
424
455
|
getPublicConfig,
|
|
425
456
|
verifyUser,
|
|
426
457
|
makeDb,
|
|
@@ -748,6 +779,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
748
779
|
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
749
780
|
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
750
781
|
else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
|
|
782
|
+
else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
|
|
751
783
|
const customerId = refId(object2.customer, "cus_");
|
|
752
784
|
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
|
|
753
785
|
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
|
|
@@ -757,6 +789,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
757
789
|
const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
|
|
758
790
|
const currency = text(object2.currency)?.toLowerCase();
|
|
759
791
|
const status2 = text(object2.status);
|
|
792
|
+
const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
|
|
760
793
|
return {
|
|
761
794
|
eventId,
|
|
762
795
|
digest,
|
|
@@ -775,6 +808,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
775
808
|
...invoiceId ? { invoiceId } : {},
|
|
776
809
|
...chargeId ? { chargeId } : {},
|
|
777
810
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
811
|
+
...disputeReason ? { disputeReason } : {},
|
|
778
812
|
...price,
|
|
779
813
|
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
780
814
|
...status2 ? { status: status2 } : {},
|
|
@@ -817,6 +851,7 @@ function ownRef(fact) {
|
|
|
817
851
|
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
818
852
|
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
819
853
|
if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
|
|
854
|
+
if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
|
|
820
855
|
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
821
856
|
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
822
857
|
}
|
|
@@ -872,6 +907,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
872
907
|
continue;
|
|
873
908
|
}
|
|
874
909
|
const source = assignments.get(fact.eventId);
|
|
910
|
+
if (!source && fact.objectKind === "dispute") continue;
|
|
875
911
|
if (!source) {
|
|
876
912
|
const direct = directSource(fact, sources);
|
|
877
913
|
const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
@@ -888,6 +924,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
888
924
|
const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
|
|
889
925
|
const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
|
|
890
926
|
const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
|
|
927
|
+
const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
|
|
891
928
|
const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
|
|
892
929
|
for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
|
|
893
930
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
@@ -908,6 +945,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
908
945
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
909
946
|
return {
|
|
910
947
|
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
948
|
+
...disputes.length ? { disputes } : {},
|
|
911
949
|
customers,
|
|
912
950
|
subscriptions,
|
|
913
951
|
invoices,
|
|
@@ -1244,6 +1282,17 @@ function findGiftRef(obj) {
|
|
|
1244
1282
|
}
|
|
1245
1283
|
function normalizeWebhookEvent(event2, options = {}) {
|
|
1246
1284
|
const obj = event2.data?.object ?? {};
|
|
1285
|
+
if (event2.type.startsWith("charge.dispute.")) {
|
|
1286
|
+
const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
|
|
1287
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
|
|
1288
|
+
return {
|
|
1289
|
+
kind: "disputed",
|
|
1290
|
+
chargeId,
|
|
1291
|
+
disputeId: obj.id,
|
|
1292
|
+
disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
|
|
1293
|
+
...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1247
1296
|
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 };
|
|
1248
1297
|
const gift = findGiftRef(obj);
|
|
1249
1298
|
if (gift.giftIntentId) {
|
|
@@ -1648,9 +1697,9 @@ async function backfillCrm(deps) {
|
|
|
1648
1697
|
}
|
|
1649
1698
|
|
|
1650
1699
|
// src/email-retirement.ts
|
|
1651
|
-
function isRetiredEmailCopy(subject,
|
|
1700
|
+
function isRetiredEmailCopy(subject, text9) {
|
|
1652
1701
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1653
|
-
const bodyCopy =
|
|
1702
|
+
const bodyCopy = text9.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1654
1703
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1655
1704
|
}
|
|
1656
1705
|
|
|
@@ -1728,10 +1777,10 @@ function planDelivery(input) {
|
|
|
1728
1777
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1729
1778
|
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1730
1779
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1731
|
-
const
|
|
1780
|
+
const text9 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1732
1781
|
|
|
1733
1782
|
` + renderedText : renderedText;
|
|
1734
|
-
return { deliver: true, transport, to, subject, text:
|
|
1783
|
+
return { deliver: true, transport, to, subject, text: text9, redirected: redirect };
|
|
1735
1784
|
}
|
|
1736
1785
|
|
|
1737
1786
|
// src/notify.ts
|
|
@@ -1879,8 +1928,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1879
1928
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1880
1929
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1881
1930
|
function applicantVariables(app) {
|
|
1882
|
-
const
|
|
1883
|
-
return { firstName:
|
|
1931
|
+
const text9 = (v) => typeof v === "string" ? v : "";
|
|
1932
|
+
return { firstName: text9(app.firstName), lastName: text9(app.lastName), email: text9(app.email), phone: text9(app.phone), state: text9(app.state) };
|
|
1884
1933
|
}
|
|
1885
1934
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1886
1935
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -2987,7 +3036,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
|
2987
3036
|
if (!intent) throw new Error("membership checkout reconciliation pending");
|
|
2988
3037
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2989
3038
|
const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
|
|
2990
|
-
if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
|
|
3039
|
+
if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
|
|
2991
3040
|
throw new Error("invalid network renewal receipt");
|
|
2992
3041
|
}
|
|
2993
3042
|
return row2;
|
|
@@ -3612,6 +3661,102 @@ function paymentLineItems(group, tier) {
|
|
|
3612
3661
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
3613
3662
|
}
|
|
3614
3663
|
|
|
3664
|
+
// src/membership-dispute-authority.ts
|
|
3665
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
3666
|
+
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3667
|
+
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3668
|
+
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3669
|
+
var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3670
|
+
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3671
|
+
var refOf = (value) => typeof value === "string" ? text2(value) : text2(value?.id);
|
|
3672
|
+
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3673
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3674
|
+
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
3675
|
+
const charge = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
|
|
3676
|
+
if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
|
|
3677
|
+
const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
|
|
3678
|
+
if (!customerId) membershipFail("membership_dispute_provider_unavailable");
|
|
3679
|
+
let subscriptionId;
|
|
3680
|
+
if (invoiceId) {
|
|
3681
|
+
const invoice = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
|
|
3682
|
+
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3683
|
+
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3684
|
+
}
|
|
3685
|
+
const seatId = text2(charge.body.metadata?.seatId);
|
|
3686
|
+
return {
|
|
3687
|
+
chargeId,
|
|
3688
|
+
customerId,
|
|
3689
|
+
...invoiceId ? { invoiceId } : {},
|
|
3690
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
3691
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
3692
|
+
...seatId ? { seatId } : {}
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
function disputedChargeCoversMembership(charge, member) {
|
|
3696
|
+
return charge.customerId === member.providerCustomerId && !charge.seatId && (charge.invoiceId !== void 0 && charge.invoiceId === member.providerInitialInvoiceId || charge.subscriptionId !== void 0 && charge.subscriptionId === member.providerSubscriptionId || charge.paymentIntentId !== void 0 && charge.paymentIntentId === member.rechargePaymentIntentId);
|
|
3697
|
+
}
|
|
3698
|
+
async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
3699
|
+
membershipClock(now);
|
|
3700
|
+
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3701
|
+
const fact = row2?.fact;
|
|
3702
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3703
|
+
if (!fact || fact.objectKind !== "dispute" || fact.eventId !== input.eventId || fact.digest !== row2.digest || !fact.chargeId || !fact.objectId || fact.livemode !== (input.environment === "prod")) membershipFail("membership_billing_evidence_pending");
|
|
3704
|
+
const id7 = await networkMembershipId(input);
|
|
3705
|
+
const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
|
|
3706
|
+
membershipId: id7,
|
|
3707
|
+
status: String(value.status),
|
|
3708
|
+
termEndsAt: Number(value.termEndsAt),
|
|
3709
|
+
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3710
|
+
autoRenew: value.autoRenew === true,
|
|
3711
|
+
...text2(value.disputeStatus) ? { disputeStatus: text2(value.disputeStatus) } : {},
|
|
3712
|
+
applied
|
|
3713
|
+
});
|
|
3714
|
+
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3715
|
+
requireMembershipScope(known, input);
|
|
3716
|
+
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3717
|
+
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3718
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3719
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3720
|
+
requireMembershipScope(member, input);
|
|
3721
|
+
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3722
|
+
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3723
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3724
|
+
const sameDispute = member.disputeId === fact.objectId;
|
|
3725
|
+
if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
|
|
3726
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3727
|
+
const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
|
|
3728
|
+
const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3729
|
+
const attrs = {
|
|
3730
|
+
status: next,
|
|
3731
|
+
disputeId: fact.objectId,
|
|
3732
|
+
disputeStatus: status2,
|
|
3733
|
+
disputeEventId: input.eventId,
|
|
3734
|
+
disputeClosedAt: closed ? now : 0,
|
|
3735
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3736
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3737
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3738
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
|
|
3739
|
+
revision: Number(member.revision) + 1,
|
|
3740
|
+
updatedAt: now
|
|
3741
|
+
};
|
|
3742
|
+
try {
|
|
3743
|
+
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3744
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
3745
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3746
|
+
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3747
|
+
evidenceGuard
|
|
3748
|
+
] });
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3751
|
+
continue;
|
|
3752
|
+
}
|
|
3753
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
3754
|
+
requireMembershipScope(saved, input);
|
|
3755
|
+
return receipt(saved, true);
|
|
3756
|
+
}
|
|
3757
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3615
3760
|
// src/member-system-contract.ts
|
|
3616
3761
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3617
3762
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3736,7 +3881,8 @@ async function firstRow2(db, ns, q) {
|
|
|
3736
3881
|
}
|
|
3737
3882
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3738
3883
|
const db = ctx.makeDb(env);
|
|
3739
|
-
const { rawBody, eventId
|
|
3884
|
+
const { rawBody, eventId } = input;
|
|
3885
|
+
let { event: event2 } = input;
|
|
3740
3886
|
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3741
3887
|
const digest = await stripeEventDigest(rawBody);
|
|
3742
3888
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
@@ -3792,6 +3938,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3792
3938
|
}
|
|
3793
3939
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3794
3940
|
}
|
|
3941
|
+
if (event2.kind === "disputed" && !event2.customerId) {
|
|
3942
|
+
const secret = await getVaultSecret(db, "stripe_secret_key");
|
|
3943
|
+
const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
|
|
3944
|
+
if (charge) event2 = { ...event2, customerId: charge.customerId };
|
|
3945
|
+
}
|
|
3795
3946
|
const app = await findPaymentApplication(db, event2);
|
|
3796
3947
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3797
3948
|
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
@@ -3829,11 +3980,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3829
3980
|
}
|
|
3830
3981
|
let authoritativeEvent = event2;
|
|
3831
3982
|
let membershipProjection = {};
|
|
3832
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
3983
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
|
|
3833
3984
|
try {
|
|
3834
3985
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3835
3986
|
membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
|
|
3836
|
-
authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
|
|
3987
|
+
authoritativeEvent = event2.kind === "disputed" ? event2 : receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
|
|
3837
3988
|
} catch {
|
|
3838
3989
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3839
3990
|
}
|
|
@@ -4482,10 +4633,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
4482
4633
|
}
|
|
4483
4634
|
|
|
4484
4635
|
// src/membership-seat-refund-stripe.ts
|
|
4485
|
-
var
|
|
4636
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4486
4637
|
|
|
4487
4638
|
// src/membership-checkout-seat-refund.ts
|
|
4488
|
-
var
|
|
4639
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4489
4640
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4490
4641
|
throw Object.assign(new Error(code), { code });
|
|
4491
4642
|
};
|
|
@@ -4494,7 +4645,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
4494
4645
|
const invoiceRef = seat.combinedInvoice;
|
|
4495
4646
|
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();
|
|
4496
4647
|
const call = async (method, path, params, key3) => {
|
|
4497
|
-
const result = await (0,
|
|
4648
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4498
4649
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4499
4650
|
return result.body;
|
|
4500
4651
|
};
|
|
@@ -4567,7 +4718,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
4567
4718
|
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4568
4719
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4569
4720
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4570
|
-
const result = await (0,
|
|
4721
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4571
4722
|
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4572
4723
|
return result.body;
|
|
4573
4724
|
};
|
|
@@ -5035,15 +5186,224 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5035
5186
|
}
|
|
5036
5187
|
};
|
|
5037
5188
|
|
|
5189
|
+
// src/membership-dispute-recharge.ts
|
|
5190
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5191
|
+
var fail6 = (code, extra = {}) => {
|
|
5192
|
+
throw Object.assign(new Error(code), { code, ...extra });
|
|
5193
|
+
};
|
|
5194
|
+
var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
|
|
5195
|
+
async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
5196
|
+
const subscription = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
5197
|
+
if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
|
|
5198
|
+
const onSubscription = refOf2(subscription.body.default_payment_method);
|
|
5199
|
+
if (onSubscription) return onSubscription;
|
|
5200
|
+
const customer = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
|
|
5201
|
+
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5202
|
+
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5203
|
+
}
|
|
5204
|
+
async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
5205
|
+
membershipClock(now);
|
|
5206
|
+
let member = await membershipLoad(db, "networkMembership", membershipId);
|
|
5207
|
+
if (!member) fail6("membership_not_found");
|
|
5208
|
+
const scope2 = { chapterId: String(member.chapterId), environment: String(member.environment) };
|
|
5209
|
+
const amountCents = Number(member.disputeAmountCents ?? member.amountCents), currency = String(member.currency);
|
|
5210
|
+
const done = (value, replayed) => ({
|
|
5211
|
+
membershipId,
|
|
5212
|
+
status: String(value.status),
|
|
5213
|
+
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5214
|
+
amountCents,
|
|
5215
|
+
currency,
|
|
5216
|
+
...replayed ? { replayed } : {}
|
|
5217
|
+
});
|
|
5218
|
+
if (member.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5219
|
+
if (member.disputeStatus === "recharged" && member.rechargePaymentIntentId) return done(member, true);
|
|
5220
|
+
fail6("membership_not_disputed");
|
|
5221
|
+
}
|
|
5222
|
+
if (member.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5223
|
+
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5224
|
+
let attempt = member.rechargeAttempt;
|
|
5225
|
+
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5226
|
+
attempt = { key: `membership-recharge:${membershipId}:${String(member.disputeId)}:${now}`, startedAt: now };
|
|
5227
|
+
await db.transact(
|
|
5228
|
+
[membershipUpdate("networkMembership", membershipId, { rechargeAttempt: attempt, revision: Number(member.revision) + 1, updatedAt: now })],
|
|
5229
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{
|
|
5230
|
+
ns: "networkMembership",
|
|
5231
|
+
id: membershipId,
|
|
5232
|
+
exists: true,
|
|
5233
|
+
equals: { revision: member.revision, status: MEMBERSHIP_DISPUTED_STATUS }
|
|
5234
|
+
}] }
|
|
5235
|
+
);
|
|
5236
|
+
member = await membershipLoad(db, "networkMembership", membershipId);
|
|
5237
|
+
if (!member || member.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5238
|
+
}
|
|
5239
|
+
const config = { secretKey };
|
|
5240
|
+
const paymentMethod = await defaultPaymentMethod(config, String(member.providerSubscriptionId), String(member.providerCustomerId));
|
|
5241
|
+
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5242
|
+
const intent = await (0, import_stripe9.stripeRequest)(config, "POST", "/v1/payment_intents", {
|
|
5243
|
+
amount: amountCents,
|
|
5244
|
+
currency,
|
|
5245
|
+
customer: String(member.providerCustomerId),
|
|
5246
|
+
payment_method: paymentMethod,
|
|
5247
|
+
off_session: "true",
|
|
5248
|
+
confirm: "true",
|
|
5249
|
+
description: `Membership payment reissued after dispute ${String(member.disputeId)}`,
|
|
5250
|
+
metadata: {
|
|
5251
|
+
...stripePartitionMetadata({ appId: String(member.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(member.runtime) }),
|
|
5252
|
+
applicationId: String(member.applicationId),
|
|
5253
|
+
membershipId,
|
|
5254
|
+
disputeId: String(member.disputeId),
|
|
5255
|
+
membershipRecharge: "true"
|
|
5256
|
+
}
|
|
5257
|
+
}, { idempotencyKey: attempt.key });
|
|
5258
|
+
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5259
|
+
const finish = async (outcome) => {
|
|
5260
|
+
const current = await membershipLoad(db, "networkMembership", membershipId);
|
|
5261
|
+
if (!current) return;
|
|
5262
|
+
await db.transact([membershipUpdate("networkMembership", membershipId, {
|
|
5263
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5264
|
+
revision: Number(current.revision) + 1,
|
|
5265
|
+
updatedAt: now
|
|
5266
|
+
})], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
|
|
5267
|
+
};
|
|
5268
|
+
if (!intent.ok) {
|
|
5269
|
+
if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
|
|
5270
|
+
await finish("declined");
|
|
5271
|
+
fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
|
|
5272
|
+
}
|
|
5273
|
+
fail6("membership_dispute_provider_unavailable");
|
|
5274
|
+
}
|
|
5275
|
+
if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
|
|
5276
|
+
await finish("declined");
|
|
5277
|
+
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5278
|
+
}
|
|
5279
|
+
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5280
|
+
if (refOf2(body.customer) !== member.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5281
|
+
for (let retry = 0; retry < 8; retry++) {
|
|
5282
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, "networkMembership", membershipId);
|
|
5283
|
+
if (!current) fail6("membership_not_found");
|
|
5284
|
+
if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
|
|
5285
|
+
const status2 = String(current.disputePriorStatus ?? (current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5286
|
+
try {
|
|
5287
|
+
await db.transact(
|
|
5288
|
+
[membershipUpdate("networkMembership", membershipId, {
|
|
5289
|
+
status: status2,
|
|
5290
|
+
disputeStatus: "recharged",
|
|
5291
|
+
rechargePaymentIntentId: String(body.id),
|
|
5292
|
+
rechargedAt: now,
|
|
5293
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
|
|
5294
|
+
revision: Number(current.revision) + 1,
|
|
5295
|
+
updatedAt: now
|
|
5296
|
+
})],
|
|
5297
|
+
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5298
|
+
{ ns: "networkMembership", id: membershipId, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5299
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5300
|
+
] }
|
|
5301
|
+
);
|
|
5302
|
+
} catch (cause) {
|
|
5303
|
+
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
5306
|
+
return done(await membershipLoad(db, "networkMembership", membershipId));
|
|
5307
|
+
}
|
|
5308
|
+
return fail6("membership_dispute_retry_required");
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
// src/worker-routes-admin-membership-disputes.ts
|
|
5312
|
+
var LIST2 = "/api/admin/membership-disputes";
|
|
5313
|
+
var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
|
|
5314
|
+
var PUBLIC2 = [
|
|
5315
|
+
"id",
|
|
5316
|
+
"appId",
|
|
5317
|
+
"chapterId",
|
|
5318
|
+
"environment",
|
|
5319
|
+
"runtime",
|
|
5320
|
+
"applicationId",
|
|
5321
|
+
"skuId",
|
|
5322
|
+
"status",
|
|
5323
|
+
"memberNumber",
|
|
5324
|
+
"amountCents",
|
|
5325
|
+
"currency",
|
|
5326
|
+
"disputeId",
|
|
5327
|
+
"disputeStatus",
|
|
5328
|
+
"disputeReason",
|
|
5329
|
+
"disputeAmountCents",
|
|
5330
|
+
"disputedAt",
|
|
5331
|
+
"disputeClosedAt",
|
|
5332
|
+
"disputePriorStatus",
|
|
5333
|
+
"rechargePaymentIntentId",
|
|
5334
|
+
"rechargedAt",
|
|
5335
|
+
"termEndsAt",
|
|
5336
|
+
"updatedAt"
|
|
5337
|
+
];
|
|
5338
|
+
var project2 = (row2) => ({
|
|
5339
|
+
...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
|
|
5340
|
+
...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
|
|
5341
|
+
startedAt: row2.rechargeAttempt.startedAt ?? null,
|
|
5342
|
+
finishedAt: row2.rechargeAttempt.finishedAt ?? null,
|
|
5343
|
+
outcome: row2.rechargeAttempt.outcome ?? null
|
|
5344
|
+
} } : {}
|
|
5345
|
+
});
|
|
5346
|
+
var FINAL = /* @__PURE__ */ new Set([
|
|
5347
|
+
"membership_not_found",
|
|
5348
|
+
"membership_not_disputed",
|
|
5349
|
+
"membership_dispute_open",
|
|
5350
|
+
"membership_dispute_amount_invalid",
|
|
5351
|
+
"recharge_payment_method_unavailable",
|
|
5352
|
+
"recharge_declined",
|
|
5353
|
+
"recharge_pending",
|
|
5354
|
+
"recharge_mismatch",
|
|
5355
|
+
"membership_dispute_changed"
|
|
5356
|
+
]);
|
|
5357
|
+
var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
5358
|
+
const one = url.pathname.match(ONE2);
|
|
5359
|
+
if (url.pathname !== LIST2 && !one) return null;
|
|
5360
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5361
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5362
|
+
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
5363
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5364
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5365
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5366
|
+
let db;
|
|
5367
|
+
try {
|
|
5368
|
+
db = operator.makeDb(env);
|
|
5369
|
+
} catch {
|
|
5370
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5371
|
+
}
|
|
5372
|
+
if (!one) {
|
|
5373
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } } })).networkMembership ?? [];
|
|
5374
|
+
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5375
|
+
return json({ disputes: disputes.map(project2), truncated: rows5.length >= 500 });
|
|
5376
|
+
}
|
|
5377
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5378
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5379
|
+
try {
|
|
5380
|
+
const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
|
|
5381
|
+
return json({ ok: true, ...receipt });
|
|
5382
|
+
} catch (error) {
|
|
5383
|
+
const failure = error;
|
|
5384
|
+
const code = typeof failure.code === "string" ? failure.code : void 0;
|
|
5385
|
+
if (code === "membership_not_found") return json({ error: "not found", code }, 404);
|
|
5386
|
+
if (code && FINAL.has(code)) {
|
|
5387
|
+
return json({
|
|
5388
|
+
error: "the charge could not be re-issued",
|
|
5389
|
+
code,
|
|
5390
|
+
...failure.declineCode ? { declineCode: failure.declineCode } : {},
|
|
5391
|
+
...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
|
|
5392
|
+
}, 409);
|
|
5393
|
+
}
|
|
5394
|
+
return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
|
|
5395
|
+
}
|
|
5396
|
+
};
|
|
5397
|
+
|
|
5038
5398
|
// src/membership-policy-versions.ts
|
|
5039
5399
|
var LIMIT = 200;
|
|
5040
|
-
var
|
|
5400
|
+
var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
5041
5401
|
var instant = (value) => {
|
|
5042
5402
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
5043
5403
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
5044
5404
|
};
|
|
5045
5405
|
function provenance(row2) {
|
|
5046
|
-
const source =
|
|
5406
|
+
const source = text3(row2.source), actor = text3(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
5047
5407
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
5048
5408
|
}
|
|
5049
5409
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -5137,16 +5497,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
5137
5497
|
};
|
|
5138
5498
|
|
|
5139
5499
|
// src/membership-discount-reterm.ts
|
|
5140
|
-
var
|
|
5500
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
5141
5501
|
|
|
5142
5502
|
// src/subscription-provider-contract.ts
|
|
5143
|
-
var
|
|
5503
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
5144
5504
|
function subscriptionContractMismatch() {
|
|
5145
5505
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
5146
5506
|
}
|
|
5147
5507
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
5148
5508
|
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
5149
|
-
const result = await (0,
|
|
5509
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
5150
5510
|
const price = result.body;
|
|
5151
5511
|
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
5152
5512
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -5184,7 +5544,7 @@ async function retermPolicy(db, scope2, id7) {
|
|
|
5184
5544
|
}
|
|
5185
5545
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
5186
5546
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
5187
|
-
const read2 = await (0,
|
|
5547
|
+
const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
5188
5548
|
const sub = read2.body, items = sub?.items?.data;
|
|
5189
5549
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
5190
5550
|
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
@@ -5195,7 +5555,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
5195
5555
|
const present = discounts.map(couponOf);
|
|
5196
5556
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
5197
5557
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
5198
|
-
const written = await (0,
|
|
5558
|
+
const written = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
5199
5559
|
"discounts[0][coupon]": couponId,
|
|
5200
5560
|
"expand[0]": "discounts",
|
|
5201
5561
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -6044,8 +6404,8 @@ var APPLICATION_LABELS = {
|
|
|
6044
6404
|
state: "State"
|
|
6045
6405
|
};
|
|
6046
6406
|
function humanize(id7) {
|
|
6047
|
-
const
|
|
6048
|
-
return
|
|
6407
|
+
const text9 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6408
|
+
return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id7;
|
|
6049
6409
|
}
|
|
6050
6410
|
function applicationDefs(chapter) {
|
|
6051
6411
|
try {
|
|
@@ -6112,7 +6472,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6112
6472
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6113
6473
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6114
6474
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6115
|
-
var
|
|
6475
|
+
var text4 = (value, path, errors, max) => {
|
|
6116
6476
|
if (typeof value !== "string" || !value.trim()) {
|
|
6117
6477
|
errors.push(`${path} is required`);
|
|
6118
6478
|
return "";
|
|
@@ -6140,23 +6500,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6140
6500
|
}
|
|
6141
6501
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6142
6502
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6143
|
-
const id7 =
|
|
6503
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
6144
6504
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6145
6505
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6146
6506
|
ids2.add(id7);
|
|
6147
6507
|
const targetRaw = record(item.target);
|
|
6148
6508
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6149
6509
|
if (targetRaw?.type === "tier") {
|
|
6150
|
-
const tierId =
|
|
6510
|
+
const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6151
6511
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6152
6512
|
target = { type: "tier", tierId };
|
|
6153
6513
|
} else if (targetRaw?.type === "entitlement") {
|
|
6154
|
-
const entitlementKey =
|
|
6514
|
+
const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6155
6515
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6156
6516
|
target = { type: "entitlement", entitlementKey };
|
|
6157
6517
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6158
6518
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6159
|
-
const stripePriceId =
|
|
6519
|
+
const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6160
6520
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6161
6521
|
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
6162
6522
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6178,9 +6538,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6178
6538
|
redemptionLifetimeHours: lifetime,
|
|
6179
6539
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6180
6540
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6181
|
-
title:
|
|
6182
|
-
description:
|
|
6183
|
-
ctaLabel:
|
|
6541
|
+
title: text4(item.title, `${path}.title`, errors, 160),
|
|
6542
|
+
description: text4(item.description, `${path}.description`, errors, 2e3),
|
|
6543
|
+
ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6184
6544
|
});
|
|
6185
6545
|
}
|
|
6186
6546
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6407,23 +6767,23 @@ async function createPaymentQuote(input) {
|
|
|
6407
6767
|
// src/membership-quote-projection.ts
|
|
6408
6768
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
6409
6769
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
6410
|
-
var
|
|
6770
|
+
var fail7 = (code) => {
|
|
6411
6771
|
throw Object.assign(new Error(code), { code });
|
|
6412
6772
|
};
|
|
6413
6773
|
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
6414
6774
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
6415
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
6416
|
-
if (row2.status !== "ready") return
|
|
6775
|
+
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail7("membership_quote_unavailable");
|
|
6776
|
+
if (row2.status !== "ready") return fail7("membership_quote_pending");
|
|
6417
6777
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
6418
6778
|
...target,
|
|
6419
6779
|
requestId: row2.requestId
|
|
6420
6780
|
}), now, lifetimeMs: 6e4 });
|
|
6421
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6781
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
|
|
6422
6782
|
const tier = row2.tier, quote = row2.quote;
|
|
6423
6783
|
const offering = offer.contract.offering;
|
|
6424
|
-
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return
|
|
6784
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
|
|
6425
6785
|
if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
|
|
6426
|
-
return
|
|
6786
|
+
return fail7("membership_catalog_mismatch");
|
|
6427
6787
|
}
|
|
6428
6788
|
const expected = await createPaymentQuote({
|
|
6429
6789
|
...target,
|
|
@@ -6436,7 +6796,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
6436
6796
|
seatOffer: quote.seatOffer,
|
|
6437
6797
|
namedSeat: quote.namedSeat
|
|
6438
6798
|
});
|
|
6439
|
-
if (quote.digest !== expected.digest) return
|
|
6799
|
+
if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
|
|
6440
6800
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
6441
6801
|
}
|
|
6442
6802
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -6444,9 +6804,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
6444
6804
|
}
|
|
6445
6805
|
async function prepareMembershipQuote(db, input, issue) {
|
|
6446
6806
|
const { applicationId: id7, now, app } = input;
|
|
6447
|
-
if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return
|
|
6807
|
+
if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail7("membership_quote_unavailable");
|
|
6448
6808
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6449
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6809
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
|
|
6450
6810
|
if (row2?.status === "ready") {
|
|
6451
6811
|
try {
|
|
6452
6812
|
return await restore2(row2, input, now);
|
|
@@ -6491,7 +6851,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6491
6851
|
}
|
|
6492
6852
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6493
6853
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6494
|
-
return
|
|
6854
|
+
return fail7("membership_quote_pending");
|
|
6495
6855
|
}
|
|
6496
6856
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6497
6857
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6540,7 +6900,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6540
6900
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6541
6901
|
}
|
|
6542
6902
|
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6543
|
-
if (!input.enabled) return
|
|
6903
|
+
if (!input.enabled) return fail7("named_seat_purchase_unavailable");
|
|
6544
6904
|
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6545
6905
|
const current = await restore2(row2, target, input.now);
|
|
6546
6906
|
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
@@ -6596,17 +6956,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6596
6956
|
if (!reader) return "";
|
|
6597
6957
|
const decoder = new TextDecoder();
|
|
6598
6958
|
let bytes = 0;
|
|
6599
|
-
let
|
|
6959
|
+
let text9 = "";
|
|
6600
6960
|
for (; ; ) {
|
|
6601
6961
|
const { done, value } = await reader.read();
|
|
6602
6962
|
if (req.signal.aborted) return invalid2();
|
|
6603
6963
|
if (done) {
|
|
6604
6964
|
complete = true;
|
|
6605
|
-
return
|
|
6965
|
+
return text9 + decoder.decode();
|
|
6606
6966
|
}
|
|
6607
6967
|
bytes += value.byteLength;
|
|
6608
6968
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6609
|
-
|
|
6969
|
+
text9 += decoder.decode(value, { stream: true });
|
|
6610
6970
|
}
|
|
6611
6971
|
} catch {
|
|
6612
6972
|
return invalid2();
|
|
@@ -6710,9 +7070,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
6710
7070
|
let selection;
|
|
6711
7071
|
if (request?.body) {
|
|
6712
7072
|
try {
|
|
6713
|
-
const
|
|
6714
|
-
if (
|
|
6715
|
-
const body = JSON.parse(
|
|
7073
|
+
const text9 = await readBoundedRequestText(request, 2048);
|
|
7074
|
+
if (text9 instanceof Response) return text9;
|
|
7075
|
+
const body = JSON.parse(text9);
|
|
6716
7076
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6717
7077
|
selection = body.namedSeat;
|
|
6718
7078
|
} catch {
|
|
@@ -7367,10 +7727,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7367
7727
|
const db = ctx.makeDb(env);
|
|
7368
7728
|
const groupId = url.searchParams.get("group") ?? chapter.id;
|
|
7369
7729
|
if (groupId !== chapter.id) return json({ error: "not found" }, 404);
|
|
7370
|
-
const { groups } = await
|
|
7730
|
+
const [{ groups }, stripeKey, activeControl] = await Promise.all([
|
|
7731
|
+
db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
|
|
7732
|
+
getVaultSecret(db, "stripe_secret_key"),
|
|
7733
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
|
|
7734
|
+
]);
|
|
7371
7735
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7372
7736
|
if (!group) return json({ error: "not found" }, 404);
|
|
7373
|
-
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7374
7737
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
7375
7738
|
const offered = await loadOfferableTiers(
|
|
7376
7739
|
db,
|
|
@@ -7379,7 +7742,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7379
7742
|
);
|
|
7380
7743
|
const seats = /* @__PURE__ */ new Map();
|
|
7381
7744
|
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7382
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7383
7745
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7384
7746
|
return json({
|
|
7385
7747
|
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
@@ -7417,11 +7779,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7417
7779
|
parsed.tierId = named.invitation.skuId;
|
|
7418
7780
|
submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
|
|
7419
7781
|
}
|
|
7420
|
-
const eligibility
|
|
7782
|
+
const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
|
|
7783
|
+
named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
|
|
7784
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
|
|
7785
|
+
typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
|
|
7786
|
+
db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
|
|
7787
|
+
]);
|
|
7421
7788
|
if (eligibility?.conflict) return eligibility.conflict;
|
|
7422
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7423
7789
|
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
7424
|
-
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
7425
7790
|
const now = Date.now();
|
|
7426
7791
|
const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
|
|
7427
7792
|
const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
|
|
@@ -7460,18 +7825,16 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7460
7825
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
7461
7826
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
7462
7827
|
}
|
|
7463
|
-
if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
|
|
7464
|
-
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
7465
|
-
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7828
|
+
if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
|
|
7466
7829
|
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
7467
|
-
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
7830
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
|
|
7468
7831
|
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
7469
7832
|
if (!state) return json({
|
|
7470
7833
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
7471
7834
|
code: "application_continuation_unavailable",
|
|
7472
7835
|
id: result.id
|
|
7473
7836
|
}, 503);
|
|
7474
|
-
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
7837
|
+
if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
|
|
7475
7838
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
7476
7839
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
7477
7840
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -7591,10 +7954,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7591
7954
|
errors.push(`${path} must be a string`);
|
|
7592
7955
|
return void 0;
|
|
7593
7956
|
}
|
|
7594
|
-
const
|
|
7595
|
-
if (required && !
|
|
7596
|
-
if (
|
|
7597
|
-
return
|
|
7957
|
+
const text9 = value.trim();
|
|
7958
|
+
if (required && !text9) errors.push(`${path} is required`);
|
|
7959
|
+
if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
7960
|
+
return text9;
|
|
7598
7961
|
}
|
|
7599
7962
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7600
7963
|
const errors = [];
|
|
@@ -7643,8 +8006,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
7643
8006
|
else {
|
|
7644
8007
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
7645
8008
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
7646
|
-
const
|
|
7647
|
-
return
|
|
8009
|
+
const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8010
|
+
return text9 ? [text9] : [];
|
|
7648
8011
|
});
|
|
7649
8012
|
}
|
|
7650
8013
|
}
|
|
@@ -7738,7 +8101,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
7738
8101
|
function unknownKeys(value, allowed2, path, errors) {
|
|
7739
8102
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7740
8103
|
}
|
|
7741
|
-
function
|
|
8104
|
+
function text5(value, path, errors, max = 65536) {
|
|
7742
8105
|
if (typeof value !== "string" || !value.trim()) {
|
|
7743
8106
|
errors.push(`${path} is required`);
|
|
7744
8107
|
return "";
|
|
@@ -7761,11 +8124,11 @@ function tiersOf(value, errors) {
|
|
|
7761
8124
|
return [];
|
|
7762
8125
|
}
|
|
7763
8126
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7764
|
-
const id7 =
|
|
8127
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
7765
8128
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7766
8129
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7767
8130
|
seen.add(id7);
|
|
7768
|
-
const name =
|
|
8131
|
+
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
7769
8132
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7770
8133
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
7771
8134
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -7783,7 +8146,7 @@ function tiersOf(value, errors) {
|
|
|
7783
8146
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
7784
8147
|
errors.push(`${path}.interval must be year or month`);
|
|
7785
8148
|
}
|
|
7786
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8149
|
+
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7787
8150
|
return [{
|
|
7788
8151
|
id: id7,
|
|
7789
8152
|
name,
|
|
@@ -7841,7 +8204,7 @@ function policyOf(value, errors, tiers) {
|
|
|
7841
8204
|
const result = {};
|
|
7842
8205
|
for (const key3 of allowed2) {
|
|
7843
8206
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
7844
|
-
result[key3] =
|
|
8207
|
+
result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
|
|
7845
8208
|
}
|
|
7846
8209
|
const tierPolicies = {};
|
|
7847
8210
|
if (input.tierPolicies !== void 0) {
|
|
@@ -7856,8 +8219,8 @@ function policyOf(value, errors, tiers) {
|
|
|
7856
8219
|
}
|
|
7857
8220
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7858
8221
|
tierPolicies[id7] = {
|
|
7859
|
-
refundPolicyText:
|
|
7860
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8222
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8223
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7861
8224
|
};
|
|
7862
8225
|
}
|
|
7863
8226
|
}
|
|
@@ -7897,9 +8260,9 @@ function validateSignupControlDocument(input) {
|
|
|
7897
8260
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
7898
8261
|
const source = recordOf(root.source);
|
|
7899
8262
|
const target = recordOf(root.target);
|
|
7900
|
-
const appId =
|
|
7901
|
-
const chapterId =
|
|
7902
|
-
const environment =
|
|
8263
|
+
const appId = text5(source?.appId, "document.source.appId", errors, 63);
|
|
8264
|
+
const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8265
|
+
const environment = text5(target?.environment, "document.target.environment", errors, 63);
|
|
7903
8266
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7904
8267
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
7905
8268
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9411,8 +9774,8 @@ async function bookSlot(req, env, ctx) {
|
|
|
9411
9774
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
9412
9775
|
await db.transact([meetingOp, appOp]);
|
|
9413
9776
|
const booked = { ...app, ...bookingAttrs };
|
|
9414
|
-
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
9415
|
-
await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
|
|
9777
|
+
await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
|
|
9778
|
+
await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
|
|
9416
9779
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
9417
9780
|
}
|
|
9418
9781
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -9436,28 +9799,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
9436
9799
|
};
|
|
9437
9800
|
|
|
9438
9801
|
// src/payments-stripe.ts
|
|
9439
|
-
var
|
|
9802
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
9440
9803
|
|
|
9441
9804
|
// src/payments-refund.ts
|
|
9442
|
-
var
|
|
9805
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
9443
9806
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9444
9807
|
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
9445
9808
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
9446
|
-
function
|
|
9809
|
+
function fail8(code = "refund_target_mismatch") {
|
|
9447
9810
|
throw Object.assign(new Error(code), { code });
|
|
9448
9811
|
}
|
|
9449
9812
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
9450
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9813
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
|
|
9451
9814
|
const call = async (method, path, params, key3) => {
|
|
9452
|
-
const result = await (0,
|
|
9453
|
-
if (!result.ok)
|
|
9454
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
9815
|
+
const result = await (0, import_stripe12.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9816
|
+
if (!result.ok) fail8("stripe_error");
|
|
9817
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
|
|
9455
9818
|
return result.body;
|
|
9456
9819
|
};
|
|
9457
9820
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9458
|
-
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId)
|
|
9821
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
|
|
9459
9822
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
9460
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9823
|
+
if (row(sub.metadata)[key3] !== value) fail8();
|
|
9461
9824
|
}
|
|
9462
9825
|
let cursor;
|
|
9463
9826
|
let earliest;
|
|
@@ -9470,66 +9833,66 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
9470
9833
|
limit: 100,
|
|
9471
9834
|
...cursor ? { starting_after: cursor } : {}
|
|
9472
9835
|
});
|
|
9473
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9836
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
|
|
9474
9837
|
for (const value of list.data) {
|
|
9475
9838
|
const invoice2 = row(value);
|
|
9476
9839
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
9477
|
-
if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0)
|
|
9840
|
+
if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail8();
|
|
9478
9841
|
seen.add(id3(invoice2));
|
|
9479
9842
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
9480
9843
|
}
|
|
9481
9844
|
if (!list.has_more) break;
|
|
9482
|
-
if (!list.data.length || page === 9)
|
|
9845
|
+
if (!list.data.length || page === 9) fail8("refund_history_incomplete");
|
|
9483
9846
|
cursor = id3(list.data[list.data.length - 1]);
|
|
9484
9847
|
}
|
|
9485
|
-
if (!earliest)
|
|
9848
|
+
if (!earliest) fail8("no_charge");
|
|
9486
9849
|
const invoice = earliest;
|
|
9487
|
-
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
9850
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
|
|
9488
9851
|
let chargeId = id3(invoice.charge);
|
|
9489
9852
|
let paymentIntentId = id3(invoice.payment_intent);
|
|
9490
9853
|
if (!chargeId && !paymentIntentId) {
|
|
9491
9854
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9492
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
9855
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
|
|
9493
9856
|
const payment = row(payments.data[0]);
|
|
9494
|
-
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
9857
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
|
|
9495
9858
|
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
9496
9859
|
}
|
|
9497
9860
|
if (!chargeId) {
|
|
9498
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9861
|
+
if (!providerId2(paymentIntentId, "pi")) fail8();
|
|
9499
9862
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
9500
|
-
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
9863
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
|
|
9501
9864
|
chargeId = id3(payment.latest_charge);
|
|
9502
9865
|
}
|
|
9503
|
-
if (!providerId2(chargeId, "ch"))
|
|
9866
|
+
if (!providerId2(chargeId, "ch")) fail8();
|
|
9504
9867
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
9505
|
-
if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid))
|
|
9506
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
9868
|
+
if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail8();
|
|
9869
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
|
|
9507
9870
|
if (charge.amount_refunded !== charge.amount) {
|
|
9508
9871
|
const refund = await call("POST", "/v1/refunds", {
|
|
9509
9872
|
charge: chargeId,
|
|
9510
9873
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
9511
9874
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
9512
|
-
if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded))
|
|
9875
|
+
if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail8("refund_not_confirmed");
|
|
9513
9876
|
}
|
|
9514
9877
|
let subscriptionCanceled = sub.status === "canceled";
|
|
9515
9878
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
9516
9879
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9517
|
-
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
9880
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
|
|
9518
9881
|
subscriptionCanceled = true;
|
|
9519
9882
|
}
|
|
9520
9883
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
9521
|
-
if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true)
|
|
9884
|
+
if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail8("refund_not_confirmed");
|
|
9522
9885
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
9523
9886
|
}
|
|
9524
9887
|
|
|
9525
9888
|
// src/subscription-payment-identity.ts
|
|
9526
|
-
var
|
|
9889
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
9527
9890
|
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
9528
9891
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
9529
9892
|
let paymentId = id4(invoice.payment_intent);
|
|
9530
9893
|
if (!paymentId) {
|
|
9531
9894
|
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9532
|
-
const result2 = await (0,
|
|
9895
|
+
const result2 = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
9533
9896
|
const list = result2.body;
|
|
9534
9897
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
9535
9898
|
const link = list.data[0];
|
|
@@ -9538,11 +9901,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9538
9901
|
}
|
|
9539
9902
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
9540
9903
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
9541
|
-
const result = await (0,
|
|
9904
|
+
const result = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", path);
|
|
9542
9905
|
const payment = result.body;
|
|
9543
9906
|
if (!result.ok || payment?.id !== paymentId || id4(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
|
|
9544
9907
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
9545
|
-
const tagged = await (0,
|
|
9908
|
+
const tagged = await (0, import_stripe13.stripeRequest)(
|
|
9546
9909
|
{ secretKey },
|
|
9547
9910
|
"POST",
|
|
9548
9911
|
path,
|
|
@@ -9554,7 +9917,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9554
9917
|
|
|
9555
9918
|
// src/payments-stripe.ts
|
|
9556
9919
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9557
|
-
return (0,
|
|
9920
|
+
return (0, import_stripe14.stripeRequest)(
|
|
9558
9921
|
{ secretKey: sk },
|
|
9559
9922
|
method,
|
|
9560
9923
|
path,
|
|
@@ -9562,7 +9925,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9562
9925
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9563
9926
|
);
|
|
9564
9927
|
}
|
|
9565
|
-
function
|
|
9928
|
+
function fail9(op, r) {
|
|
9566
9929
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9567
9930
|
err.code = "stripe_error";
|
|
9568
9931
|
throw err;
|
|
@@ -9621,7 +9984,7 @@ function createStripeProvider(config) {
|
|
|
9621
9984
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9622
9985
|
`cus:${input.applicationId}`
|
|
9623
9986
|
);
|
|
9624
|
-
if (!cust.ok)
|
|
9987
|
+
if (!cust.ok) fail9("customer create", cust);
|
|
9625
9988
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9626
9989
|
customerId = cust.body.id;
|
|
9627
9990
|
}
|
|
@@ -9666,7 +10029,7 @@ function createStripeProvider(config) {
|
|
|
9666
10029
|
// retry between create and the db write can't orphan a second one.
|
|
9667
10030
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9668
10031
|
);
|
|
9669
|
-
if (!sub.ok)
|
|
10032
|
+
if (!sub.ok) fail9("subscription create", sub);
|
|
9670
10033
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9671
10034
|
const subscriptionId = sub.body.id;
|
|
9672
10035
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9676,14 +10039,14 @@ function createStripeProvider(config) {
|
|
|
9676
10039
|
"expand[2]": "discounts",
|
|
9677
10040
|
"expand[3]": "latest_invoice.discounts"
|
|
9678
10041
|
});
|
|
9679
|
-
if (!expanded.ok)
|
|
10042
|
+
if (!expanded.ok) fail9("subscription invoice evidence", expanded);
|
|
9680
10043
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9681
10044
|
sub = expanded;
|
|
9682
10045
|
}
|
|
9683
10046
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9684
10047
|
const invoice = sub.body.latest_invoice;
|
|
9685
10048
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9686
|
-
if (!clientSecret2)
|
|
10049
|
+
if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
|
|
9687
10050
|
await tagSubscriptionPayment(
|
|
9688
10051
|
sk,
|
|
9689
10052
|
invoice,
|
|
@@ -9695,7 +10058,7 @@ function createStripeProvider(config) {
|
|
|
9695
10058
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
9696
10059
|
},
|
|
9697
10060
|
async ingestWebhook(rawBody, sigHeader) {
|
|
9698
|
-
if (!webhookSecret || !await (0,
|
|
10061
|
+
if (!webhookSecret || !await (0, import_stripe14.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
9699
10062
|
return { ok: false, reason: "bad_signature" };
|
|
9700
10063
|
}
|
|
9701
10064
|
const event2 = JSON.parse(rawBody);
|
|
@@ -10170,7 +10533,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10170
10533
|
|
|
10171
10534
|
// src/worker-payment-receipt.ts
|
|
10172
10535
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10173
|
-
var escape = (
|
|
10536
|
+
var escape = (text9) => text9.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10174
10537
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10175
10538
|
const id7 = url.searchParams.get("application");
|
|
10176
10539
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10181,7 +10544,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10181
10544
|
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
10182
10545
|
const receipt = intent.paymentReceipt;
|
|
10183
10546
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10184
|
-
const
|
|
10547
|
+
const text9 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10185
10548
|
if (req.method === "POST") {
|
|
10186
10549
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10187
10550
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10199,7 +10562,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10199
10562
|
template: "paymentReceipt",
|
|
10200
10563
|
to: String(intent.purchaserEmailNormalized),
|
|
10201
10564
|
applicationId: id7,
|
|
10202
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10565
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text9 },
|
|
10203
10566
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10204
10567
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10205
10568
|
});
|
|
@@ -10209,7 +10572,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10209
10572
|
const nonce = crypto.randomUUID();
|
|
10210
10573
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10211
10574
|
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
10212
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
10575
|
+
<h1>Payment receipt</h1><pre>${escape(text9)}</pre><button id="print">Print / save PDF</button>
|
|
10213
10576
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10214
10577
|
headers: {
|
|
10215
10578
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10679,16 +11042,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
10679
11042
|
};
|
|
10680
11043
|
|
|
10681
11044
|
// src/named-seat-accepted-notice.ts
|
|
10682
|
-
var
|
|
11045
|
+
var text6 = (value) => typeof value === "string" ? value : "";
|
|
10683
11046
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
10684
11047
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
10685
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
11048
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text6(purchaser.email)) {
|
|
10686
11049
|
return { sent: false, reason: "purchaser-missing" };
|
|
10687
11050
|
}
|
|
10688
11051
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
10689
11052
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
10690
11053
|
const config = emailGroupFrom(group);
|
|
10691
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
11054
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text6).join(" ").replace(/\s+/g, " ").trim();
|
|
10692
11055
|
return sendTemplated({
|
|
10693
11056
|
db,
|
|
10694
11057
|
envName: env.ODLA_ENV,
|
|
@@ -10700,12 +11063,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
10700
11063
|
}, {
|
|
10701
11064
|
group: config,
|
|
10702
11065
|
template: "seatAccepted",
|
|
10703
|
-
to:
|
|
11066
|
+
to: text6(purchaser.email),
|
|
10704
11067
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
10705
11068
|
applicationId: input.primaryApplicationId,
|
|
10706
11069
|
tierId: input.skuId,
|
|
10707
11070
|
vars: {
|
|
10708
|
-
firstName:
|
|
11071
|
+
firstName: text6(purchaser.firstName),
|
|
10709
11072
|
purchaserName,
|
|
10710
11073
|
recipientName: input.recipientName,
|
|
10711
11074
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -11149,7 +11512,7 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
|
11149
11512
|
const checked = validateScheduling({ ...stored, ...body });
|
|
11150
11513
|
if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
|
|
11151
11514
|
try {
|
|
11152
|
-
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
|
|
11515
|
+
await db.transact([{ t: "update", ns: "groups", id: { ns: "groups", attr: "id", value: String(group.id) }, attrs: { schedulingJson: checked.value } }]);
|
|
11153
11516
|
} catch (error) {
|
|
11154
11517
|
const code = error.code;
|
|
11155
11518
|
console.error("admin_scheduling_write_failed", { code: typeof code === "string" ? code : "unknown" });
|
|
@@ -12172,12 +12535,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12172
12535
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12173
12536
|
const t = value;
|
|
12174
12537
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12175
|
-
const subject = str2(t.subject).trim(),
|
|
12176
|
-
const issues = (0, import_crm16.validateEmailCopy)({ subject, text:
|
|
12538
|
+
const subject = str2(t.subject).trim(), text9 = str2(t.text);
|
|
12539
|
+
const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text9 }, { allowedVariables: events[key3].variables });
|
|
12177
12540
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12178
12541
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12179
12542
|
}
|
|
12180
|
-
clean[key3] = { subject, text:
|
|
12543
|
+
clean[key3] = { subject, text: text9, enabled: t.enabled };
|
|
12181
12544
|
}
|
|
12182
12545
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12183
12546
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -14381,9 +14744,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14381
14744
|
if (req.method === "POST") {
|
|
14382
14745
|
let body;
|
|
14383
14746
|
try {
|
|
14384
|
-
const
|
|
14385
|
-
if (new TextEncoder().encode(
|
|
14386
|
-
const parsed = JSON.parse(
|
|
14747
|
+
const text9 = await req.text();
|
|
14748
|
+
if (new TextEncoder().encode(text9).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14749
|
+
const parsed = JSON.parse(text9);
|
|
14387
14750
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14388
14751
|
body = parsed;
|
|
14389
14752
|
} catch {
|
|
@@ -14544,7 +14907,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14544
14907
|
};
|
|
14545
14908
|
|
|
14546
14909
|
// src/worker-routes-network-commercial.ts
|
|
14547
|
-
var
|
|
14910
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14548
14911
|
|
|
14549
14912
|
// src/worker-routes-network-commercial-parity.ts
|
|
14550
14913
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -14552,7 +14915,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
14552
14915
|
// src/network-commercial-parity.ts
|
|
14553
14916
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14554
14917
|
var MAX_RECORDS = 500;
|
|
14555
|
-
var
|
|
14918
|
+
var text7 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14556
14919
|
function parityMessage(partition, kind, value) {
|
|
14557
14920
|
return [
|
|
14558
14921
|
"chapter-commercial-parity",
|
|
@@ -14584,18 +14947,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14584
14947
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14585
14948
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14586
14949
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14587
|
-
const eligible = input.rows.filter((row2) =>
|
|
14950
|
+
const eligible = input.rows.filter((row2) => text7(row2.id) && (text7(row2.stripeCustomerId) || text7(row2.stripeSubscriptionId)));
|
|
14588
14951
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14589
14952
|
const key3 = await parityKey(input.secret);
|
|
14590
14953
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14591
|
-
const applicationId =
|
|
14592
|
-
const customerId =
|
|
14593
|
-
const subscriptionId =
|
|
14954
|
+
const applicationId = text7(row2.id);
|
|
14955
|
+
const customerId = text7(row2.stripeCustomerId);
|
|
14956
|
+
const subscriptionId = text7(row2.stripeSubscriptionId);
|
|
14594
14957
|
return {
|
|
14595
14958
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14596
14959
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14597
14960
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14598
|
-
status:
|
|
14961
|
+
status: text7(row2.status, 64),
|
|
14599
14962
|
canceled: row2.canceled === true
|
|
14600
14963
|
};
|
|
14601
14964
|
}));
|
|
@@ -14621,7 +14984,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14621
14984
|
if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
|
|
14622
14985
|
const seen = /* @__PURE__ */ new Set();
|
|
14623
14986
|
for (const record4 of records) {
|
|
14624
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
14987
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text7(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
14625
14988
|
const key3 = recordKey(record4);
|
|
14626
14989
|
if (seen.has(key3)) return false;
|
|
14627
14990
|
seen.add(key3);
|
|
@@ -14632,16 +14995,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14632
14995
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14633
14996
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14634
14997
|
const key3 = await parityKey(input.secret);
|
|
14635
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
14636
|
-
const customerId =
|
|
14637
|
-
const subscriptionId =
|
|
14998
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text7(row2.applicationId) && text7(row2.objectId)).map(async (row2) => {
|
|
14999
|
+
const customerId = text7(row2.customerId);
|
|
15000
|
+
const subscriptionId = text7(row2.subscriptionId) ?? text7(row2.objectId);
|
|
14638
15001
|
return {
|
|
14639
15002
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
14640
15003
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
14641
15004
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
14642
15005
|
status: null,
|
|
14643
15006
|
canceled: false,
|
|
14644
|
-
stripeStatus:
|
|
15007
|
+
stripeStatus: text7(row2.status, 64),
|
|
14645
15008
|
stripeCanceled: row2.status === "canceled"
|
|
14646
15009
|
};
|
|
14647
15010
|
}));
|
|
@@ -14838,6 +15201,34 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
|
14838
15201
|
};
|
|
14839
15202
|
}
|
|
14840
15203
|
|
|
15204
|
+
// src/worker-network-commercial-dispute.ts
|
|
15205
|
+
async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
15206
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
15207
|
+
if (!operator || !fact.chargeId) return { disposition: "unhosted" };
|
|
15208
|
+
let db;
|
|
15209
|
+
try {
|
|
15210
|
+
db = operator.makeDb(env);
|
|
15211
|
+
} catch {
|
|
15212
|
+
return { disposition: "unhosted" };
|
|
15213
|
+
}
|
|
15214
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15215
|
+
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15216
|
+
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15217
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15218
|
+
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15219
|
+
if (!member) return { disposition: "not_applicable" };
|
|
15220
|
+
const target = {
|
|
15221
|
+
appId: String(member.appId),
|
|
15222
|
+
chapterId: String(member.chapterId),
|
|
15223
|
+
environment: String(member.environment),
|
|
15224
|
+
runtime: String(member.runtime),
|
|
15225
|
+
applicationId: String(member.applicationId),
|
|
15226
|
+
skuId: String(member.skuId)
|
|
15227
|
+
};
|
|
15228
|
+
const receipt = await reconcileMembershipDispute(db, { ...target, eventId: fact.eventId }, now, secret, charge);
|
|
15229
|
+
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15230
|
+
}
|
|
15231
|
+
|
|
14841
15232
|
// src/worker-routes-network-commercial.ts
|
|
14842
15233
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
14843
15234
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -14902,6 +15293,12 @@ function safeProjection(projection) {
|
|
|
14902
15293
|
amountReceivedCents: seat.amountReceivedCents,
|
|
14903
15294
|
refundedCents: seat.refundedCents ?? 0
|
|
14904
15295
|
})),
|
|
15296
|
+
disputes: (projection.disputes ?? []).map((dispute) => ({
|
|
15297
|
+
...row2(dispute),
|
|
15298
|
+
chargeId: dispute.chargeId ?? null,
|
|
15299
|
+
paymentIntentId: dispute.paymentIntentId ?? null,
|
|
15300
|
+
reason: dispute.disputeReason ?? null
|
|
15301
|
+
})),
|
|
14905
15302
|
exceptions: projection.exceptions
|
|
14906
15303
|
};
|
|
14907
15304
|
}
|
|
@@ -14920,7 +15317,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14920
15317
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
14921
15318
|
}
|
|
14922
15319
|
try {
|
|
14923
|
-
const event2 = await (0,
|
|
15320
|
+
const event2 = await (0, import_stripe15.constructStripeEvent)(body, signature, secret);
|
|
14924
15321
|
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
14925
15322
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
14926
15323
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -14934,14 +15331,24 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14934
15331
|
if (result.status === "collision") {
|
|
14935
15332
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
14936
15333
|
}
|
|
15334
|
+
let dispute;
|
|
15335
|
+
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15336
|
+
try {
|
|
15337
|
+
dispute = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15338
|
+
} catch (error) {
|
|
15339
|
+
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15340
|
+
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
15341
|
+
}
|
|
15342
|
+
}
|
|
14937
15343
|
return json({
|
|
14938
15344
|
ok: true,
|
|
14939
15345
|
status: result.status,
|
|
14940
15346
|
eventId: fact.eventId,
|
|
15347
|
+
...dispute ? { dispute: dispute.disposition } : {},
|
|
14941
15348
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14942
15349
|
});
|
|
14943
15350
|
} catch (error) {
|
|
14944
|
-
if (error instanceof
|
|
15351
|
+
if (error instanceof import_stripe15.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
14945
15352
|
throw error;
|
|
14946
15353
|
}
|
|
14947
15354
|
};
|
|
@@ -15004,7 +15411,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15004
15411
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
15005
15412
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
15006
15413
|
try {
|
|
15007
|
-
const stripe = (0,
|
|
15414
|
+
const stripe = (0, import_stripe15.initStripe)({ secretKey });
|
|
15008
15415
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
15009
15416
|
listStripeObjects(stripe, "/v1/customers"),
|
|
15010
15417
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -15018,7 +15425,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15018
15425
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
15019
15426
|
});
|
|
15020
15427
|
} catch (error) {
|
|
15021
|
-
if (error instanceof
|
|
15428
|
+
if (error instanceof import_stripe15.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
15022
15429
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
15023
15430
|
}
|
|
15024
15431
|
}
|
|
@@ -15135,7 +15542,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15135
15542
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15136
15543
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15137
15544
|
}
|
|
15138
|
-
function
|
|
15545
|
+
function text8(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15139
15546
|
if (value === void 0 && !required) return void 0;
|
|
15140
15547
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15141
15548
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15161,7 +15568,7 @@ function scopeValue(value, path, errors) {
|
|
|
15161
15568
|
return { kind: raw2.kind };
|
|
15162
15569
|
}
|
|
15163
15570
|
if (raw2.kind === "chapter") {
|
|
15164
|
-
const chapterId =
|
|
15571
|
+
const chapterId = text8(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15165
15572
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15166
15573
|
return { kind: "chapter", chapterId };
|
|
15167
15574
|
}
|
|
@@ -15175,22 +15582,22 @@ function resourceValue(value, path, errors) {
|
|
|
15175
15582
|
return null;
|
|
15176
15583
|
}
|
|
15177
15584
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15178
|
-
const id7 =
|
|
15585
|
+
const id7 = text8(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15179
15586
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15180
|
-
const kind =
|
|
15587
|
+
const kind = text8(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15181
15588
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15182
|
-
const title =
|
|
15183
|
-
const summary =
|
|
15184
|
-
const status2 =
|
|
15589
|
+
const title = text8(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15590
|
+
const summary = text8(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15591
|
+
const status2 = text8(raw2.status, `${path}.status`, errors) ?? "";
|
|
15185
15592
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15186
15593
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15187
15594
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15188
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15595
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text8(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15189
15596
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15190
15597
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15191
15598
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15192
|
-
const bodyMarkdown =
|
|
15193
|
-
const url =
|
|
15599
|
+
const bodyMarkdown = text8(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15600
|
+
const url = text8(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15194
15601
|
if (url) {
|
|
15195
15602
|
try {
|
|
15196
15603
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15202,9 +15609,9 @@ function resourceValue(value, path, errors) {
|
|
|
15202
15609
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15203
15610
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15204
15611
|
const brandAsset = asset ? {
|
|
15205
|
-
bookId:
|
|
15206
|
-
assetId:
|
|
15207
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15612
|
+
bookId: text8(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15613
|
+
assetId: text8(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15614
|
+
...asset.filename === void 0 ? {} : { filename: text8(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15208
15615
|
} : void 0;
|
|
15209
15616
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15210
15617
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15219,17 +15626,17 @@ function resourceValue(value, path, errors) {
|
|
|
15219
15626
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15220
15627
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15221
15628
|
const promotedFrom = promotedRaw ? {
|
|
15222
|
-
chapterId:
|
|
15223
|
-
resourceId:
|
|
15629
|
+
chapterId: text8(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15630
|
+
resourceId: text8(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15224
15631
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15225
15632
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15226
|
-
promotedBy:
|
|
15633
|
+
promotedBy: text8(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15227
15634
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15228
15635
|
} : void 0;
|
|
15229
15636
|
const provenance2 = {
|
|
15230
|
-
createdBy:
|
|
15637
|
+
createdBy: text8(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15231
15638
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15232
|
-
updatedBy:
|
|
15639
|
+
updatedBy: text8(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15233
15640
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15234
15641
|
...promotedFrom ? { promotedFrom } : {}
|
|
15235
15642
|
};
|
|
@@ -15248,7 +15655,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15248
15655
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15249
15656
|
const sourceType = sourceRaw?.type;
|
|
15250
15657
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15251
|
-
const sourceAppId =
|
|
15658
|
+
const sourceAppId = text8(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15252
15659
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15253
15660
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15254
15661
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15256,8 +15663,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15256
15663
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15257
15664
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15258
15665
|
const target = targetRaw ? {
|
|
15259
|
-
appId:
|
|
15260
|
-
environment:
|
|
15666
|
+
appId: text8(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15667
|
+
environment: text8(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15261
15668
|
} : void 0;
|
|
15262
15669
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15263
15670
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15598,7 +16005,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
15598
16005
|
};
|
|
15599
16006
|
|
|
15600
16007
|
// src/membership-cancel-authority.ts
|
|
15601
|
-
var
|
|
16008
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15602
16009
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
15603
16010
|
membershipClock(now);
|
|
15604
16011
|
const id7 = await networkMembershipId(target);
|
|
@@ -15609,14 +16016,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
15609
16016
|
membershipFail("membership_cancellation_unavailable");
|
|
15610
16017
|
}
|
|
15611
16018
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
15612
|
-
const read2 = await (0,
|
|
16019
|
+
const read2 = await (0, import_stripe16.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
15613
16020
|
const sub = read2.body;
|
|
15614
16021
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
15615
16022
|
...stripePartitionMetadata(target),
|
|
15616
16023
|
applicationId: target.applicationId
|
|
15617
16024
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
15618
16025
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
15619
|
-
const write = await (0,
|
|
16026
|
+
const write = await (0, import_stripe16.stripeRequest)(
|
|
15620
16027
|
{ secretKey: stripeSecret },
|
|
15621
16028
|
"POST",
|
|
15622
16029
|
path,
|
|
@@ -15838,7 +16245,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
15838
16245
|
}
|
|
15839
16246
|
|
|
15840
16247
|
// src/membership-provider-lifecycle.ts
|
|
15841
|
-
var
|
|
16248
|
+
var import_stripe17 = require("@odla-ai/stripe");
|
|
15842
16249
|
|
|
15843
16250
|
// src/membership-renewal-authority.ts
|
|
15844
16251
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -15960,6 +16367,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15960
16367
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
15961
16368
|
const fact = row2?.fact;
|
|
15962
16369
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
16370
|
+
if (fact?.objectKind === "dispute") {
|
|
16371
|
+
const secret = await getVaultSecret(db, stripeSecretName);
|
|
16372
|
+
if (!secret) membershipFail("membership_billing_provider_unavailable");
|
|
16373
|
+
return reconcileMembershipDispute(db, input, now, secret);
|
|
16374
|
+
}
|
|
15963
16375
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15964
16376
|
if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
|
|
15965
16377
|
appId: input.appId,
|
|
@@ -16002,7 +16414,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
16002
16414
|
}
|
|
16003
16415
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
16004
16416
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
16005
|
-
const result = await (0,
|
|
16417
|
+
const result = await (0, import_stripe17.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
16006
16418
|
const sub = result.body;
|
|
16007
16419
|
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
|
|
16008
16420
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -16040,7 +16452,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
16040
16452
|
}
|
|
16041
16453
|
|
|
16042
16454
|
// src/membership-seat-stripe.ts
|
|
16043
|
-
var
|
|
16455
|
+
var import_stripe18 = require("@odla-ai/stripe");
|
|
16044
16456
|
var reject7 = () => {
|
|
16045
16457
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
16046
16458
|
};
|
|
@@ -16052,7 +16464,7 @@ var metadataFor = (seat) => ({
|
|
|
16052
16464
|
});
|
|
16053
16465
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
16054
16466
|
async function read(sk, path, params) {
|
|
16055
|
-
const response = await (0,
|
|
16467
|
+
const response = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
16056
16468
|
if (!response.ok) return reject7();
|
|
16057
16469
|
return response.body;
|
|
16058
16470
|
}
|
|
@@ -16089,11 +16501,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
16089
16501
|
}
|
|
16090
16502
|
await subscriptionForSeat(sk, seat);
|
|
16091
16503
|
const metadata = metadataFor(seat);
|
|
16092
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
16504
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe18.stripeRequest)(
|
|
16093
16505
|
{ secretKey: sk },
|
|
16094
16506
|
"GET",
|
|
16095
16507
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
16096
|
-
) : await (0,
|
|
16508
|
+
) : await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
16097
16509
|
amount: seat.amountCents,
|
|
16098
16510
|
currency: seat.currency,
|
|
16099
16511
|
customer: seat.providerCustomerId,
|
|
@@ -16115,7 +16527,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
16115
16527
|
return String(existing.id);
|
|
16116
16528
|
}
|
|
16117
16529
|
if (seat.combinedInvoice) reject7();
|
|
16118
|
-
const result = await (0,
|
|
16530
|
+
const result = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
16119
16531
|
subscription: seat.providerSubscriptionId,
|
|
16120
16532
|
price: seat.priceId,
|
|
16121
16533
|
quantity: 1,
|
|
@@ -16816,7 +17228,20 @@ var membershipAuthorityEntities = {
|
|
|
16816
17228
|
graceEndsAt: attr("number", true),
|
|
16817
17229
|
...times(),
|
|
16818
17230
|
refundRequestedAt: attr("number", true),
|
|
16819
|
-
restartId: key(true)
|
|
17231
|
+
restartId: key(true),
|
|
17232
|
+
// A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
|
|
17233
|
+
// the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
|
|
17234
|
+
disputeId: key(true),
|
|
17235
|
+
disputeStatus: attr("string", true),
|
|
17236
|
+
disputeReason: attr("string", true),
|
|
17237
|
+
disputeAmountCents: attr("number", true),
|
|
17238
|
+
disputedAt: attr("number", true),
|
|
17239
|
+
disputeClosedAt: attr("number", true),
|
|
17240
|
+
disputePriorStatus: attr("string", true),
|
|
17241
|
+
disputeEventId: key(true),
|
|
17242
|
+
rechargePaymentIntentId: key(true),
|
|
17243
|
+
rechargedAt: attr("number", true),
|
|
17244
|
+
rechargeAttempt: attr("json", true)
|
|
16820
17245
|
} },
|
|
16821
17246
|
membershipRestart: { attrs: {
|
|
16822
17247
|
id: id5(),
|
|
@@ -17013,7 +17438,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
17013
17438
|
}
|
|
17014
17439
|
|
|
17015
17440
|
// src/membership-seat-cancel.ts
|
|
17016
|
-
var
|
|
17441
|
+
var import_stripe19 = require("@odla-ai/stripe");
|
|
17017
17442
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
17018
17443
|
membershipClock(now);
|
|
17019
17444
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -17041,7 +17466,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
17041
17466
|
}
|
|
17042
17467
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
17043
17468
|
const request = async (method, path, params) => {
|
|
17044
|
-
const result = await (0,
|
|
17469
|
+
const result = await (0, import_stripe19.stripeRequest)(
|
|
17045
17470
|
{ secretKey },
|
|
17046
17471
|
method,
|
|
17047
17472
|
path,
|
|
@@ -17054,7 +17479,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
17054
17479
|
let payment;
|
|
17055
17480
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
17056
17481
|
else {
|
|
17057
|
-
const snapshot = await listStripeObjects((0,
|
|
17482
|
+
const snapshot = await listStripeObjects((0, import_stripe19.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
17058
17483
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
17059
17484
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
17060
17485
|
payment = matches2[0];
|
|
@@ -17229,7 +17654,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17229
17654
|
};
|
|
17230
17655
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17231
17656
|
let size = 0;
|
|
17232
|
-
let
|
|
17657
|
+
let text9 = "";
|
|
17233
17658
|
const decoder = new TextDecoder();
|
|
17234
17659
|
try {
|
|
17235
17660
|
while (true) {
|
|
@@ -17239,10 +17664,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17239
17664
|
if (done) break;
|
|
17240
17665
|
size += value.byteLength;
|
|
17241
17666
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17242
|
-
|
|
17667
|
+
text9 += decoder.decode(value, { stream: true });
|
|
17243
17668
|
}
|
|
17244
17669
|
try {
|
|
17245
|
-
return JSON.parse(
|
|
17670
|
+
return JSON.parse(text9 + decoder.decode());
|
|
17246
17671
|
} catch {
|
|
17247
17672
|
throw new TypeError(`invalid ${label}`);
|
|
17248
17673
|
}
|
|
@@ -17577,7 +18002,7 @@ async function requestMembershipRefund(options) {
|
|
|
17577
18002
|
async function requestMembershipRenewal(options) {
|
|
17578
18003
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
17579
18004
|
const row2 = result.renewal;
|
|
17580
|
-
if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
|
|
18005
|
+
if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
|
|
17581
18006
|
throw new Error("invalid membership renewal receipt");
|
|
17582
18007
|
}
|
|
17583
18008
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -17929,6 +18354,7 @@ var BUILTIN_ROUTES = [
|
|
|
17929
18354
|
handlePaymentRecovery,
|
|
17930
18355
|
handleAdminNamedSeat,
|
|
17931
18356
|
handleAdminMembershipSeats,
|
|
18357
|
+
handleAdminMembershipDisputes,
|
|
17932
18358
|
handleAdminMembershipPolicyVersions,
|
|
17933
18359
|
handleAdminMembershipReterm,
|
|
17934
18360
|
handleAdminMeetings,
|
|
@@ -17977,8 +18403,9 @@ function chapterWorker(options) {
|
|
|
17977
18403
|
const ctx = createWorkerContext(options);
|
|
17978
18404
|
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
17979
18405
|
return {
|
|
17980
|
-
async fetch(req, env) {
|
|
18406
|
+
async fetch(req, env, execution) {
|
|
17981
18407
|
const url = new URL(req.url);
|
|
18408
|
+
ctx.bindExecution(req, execution);
|
|
17982
18409
|
for (const route of routes) {
|
|
17983
18410
|
let res;
|
|
17984
18411
|
try {
|