@odla-ai/chapter 0.58.1 → 0.59.1
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 +66 -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-P6X3W2XP.js} +147 -56
- package/dist/chunk-P6X3W2XP.js.map +1 -0
- package/dist/{chunk-CKC6X5H2.js → chunk-SDGVPIXF.js} +401 -309
- package/dist/chunk-SDGVPIXF.js.map +1 -0
- package/dist/{copy-context-DE4YMAbz.d.ts → copy-context-BuJBQafl.d.ts} +4 -0
- package/dist/index.cjs +285 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +281 -81
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +40 -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 +34 -6
- package/dist/ui/member/index.js +10 -2
- package/dist/worker/index.cjs +694 -184
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +173 -3
- package/dist/worker/index.d.ts +173 -3
- package/dist/worker/index.js +687 -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,
|
|
@@ -41,6 +42,7 @@ __export(worker_exports, {
|
|
|
41
42
|
deliverMemberContent: () => deliverMemberContent,
|
|
42
43
|
deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
|
|
43
44
|
deliverSignupControl: () => deliverSignupControl,
|
|
45
|
+
disputedChargeCoversSeat: () => disputedChargeCoversSeat,
|
|
44
46
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
45
47
|
grantIncludedNamedSeat: () => grantIncludedNamedSeat,
|
|
46
48
|
liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
|
|
@@ -49,7 +51,12 @@ __export(worker_exports, {
|
|
|
49
51
|
namedSeatInvitationOpen: () => namedSeatInvitationOpen,
|
|
50
52
|
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
51
53
|
namedSeatRegiftable: () => namedSeatRegiftable,
|
|
54
|
+
readDisputedCharge: () => readDisputedCharge,
|
|
52
55
|
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
56
|
+
rechargeDisputedMembership: () => rechargeDisputedMembership,
|
|
57
|
+
rechargeDisputedSeat: () => rechargeDisputedSeat,
|
|
58
|
+
reconcileMembershipDispute: () => reconcileMembershipDispute,
|
|
59
|
+
reconcileNamedSeatDispute: () => reconcileNamedSeatDispute,
|
|
53
60
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
54
61
|
regiftNamedSeat: () => regiftNamedSeat,
|
|
55
62
|
releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
|
|
@@ -311,12 +318,37 @@ function networkCommercialAlerts(ctx, route) {
|
|
|
311
318
|
};
|
|
312
319
|
}
|
|
313
320
|
|
|
321
|
+
// src/worker-defer.ts
|
|
322
|
+
function deferWork(ctx, req, task) {
|
|
323
|
+
return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
|
|
324
|
+
}
|
|
325
|
+
function createDeferral() {
|
|
326
|
+
const executions = /* @__PURE__ */ new WeakMap();
|
|
327
|
+
const settle = (task) => task().then(() => void 0, (error) => {
|
|
328
|
+
console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
|
|
329
|
+
});
|
|
330
|
+
return {
|
|
331
|
+
/** Remember the runtime's ExecutionContext for this request. */
|
|
332
|
+
bind(req, execution) {
|
|
333
|
+
if (execution) executions.set(req, execution);
|
|
334
|
+
},
|
|
335
|
+
/** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
|
|
336
|
+
defer(req, task) {
|
|
337
|
+
const execution = executions.get(req);
|
|
338
|
+
if (!execution) return settle(task);
|
|
339
|
+
execution.waitUntil(settle(task));
|
|
340
|
+
return Promise.resolve();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
314
345
|
// src/worker-context.ts
|
|
315
346
|
var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
|
|
316
347
|
function createWorkerContext(options) {
|
|
317
348
|
const { chapter } = options;
|
|
318
349
|
const auth = chapter.auth;
|
|
319
350
|
const crmBase = options.crmBasePath ?? "/api/crm";
|
|
351
|
+
const deferral = createDeferral();
|
|
320
352
|
let publicConfigCache = null;
|
|
321
353
|
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
322
354
|
async function getPublicConfig(env) {
|
|
@@ -421,6 +453,8 @@ function createWorkerContext(options) {
|
|
|
421
453
|
membershipAuthority: options.membershipAuthority,
|
|
422
454
|
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
423
455
|
recordError: options.recordError ?? consoleErrorRecorder,
|
|
456
|
+
bindExecution: deferral.bind,
|
|
457
|
+
defer: deferral.defer,
|
|
424
458
|
getPublicConfig,
|
|
425
459
|
verifyUser,
|
|
426
460
|
makeDb,
|
|
@@ -748,6 +782,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
748
782
|
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
749
783
|
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
750
784
|
else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
|
|
785
|
+
else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
|
|
751
786
|
const customerId = refId(object2.customer, "cus_");
|
|
752
787
|
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
|
|
753
788
|
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
|
|
@@ -757,6 +792,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
757
792
|
const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
|
|
758
793
|
const currency = text(object2.currency)?.toLowerCase();
|
|
759
794
|
const status2 = text(object2.status);
|
|
795
|
+
const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
|
|
760
796
|
return {
|
|
761
797
|
eventId,
|
|
762
798
|
digest,
|
|
@@ -775,6 +811,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
775
811
|
...invoiceId ? { invoiceId } : {},
|
|
776
812
|
...chargeId ? { chargeId } : {},
|
|
777
813
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
814
|
+
...disputeReason ? { disputeReason } : {},
|
|
778
815
|
...price,
|
|
779
816
|
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
780
817
|
...status2 ? { status: status2 } : {},
|
|
@@ -817,6 +854,7 @@ function ownRef(fact) {
|
|
|
817
854
|
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
818
855
|
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
819
856
|
if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
|
|
857
|
+
if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
|
|
820
858
|
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
821
859
|
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
822
860
|
}
|
|
@@ -872,6 +910,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
872
910
|
continue;
|
|
873
911
|
}
|
|
874
912
|
const source = assignments.get(fact.eventId);
|
|
913
|
+
if (!source && fact.objectKind === "dispute") continue;
|
|
875
914
|
if (!source) {
|
|
876
915
|
const direct = directSource(fact, sources);
|
|
877
916
|
const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
@@ -888,6 +927,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
888
927
|
const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
|
|
889
928
|
const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
|
|
890
929
|
const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
|
|
930
|
+
const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
|
|
891
931
|
const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
|
|
892
932
|
for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
|
|
893
933
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
@@ -908,6 +948,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
908
948
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
909
949
|
return {
|
|
910
950
|
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
951
|
+
...disputes.length ? { disputes } : {},
|
|
911
952
|
customers,
|
|
912
953
|
subscriptions,
|
|
913
954
|
invoices,
|
|
@@ -1244,6 +1285,17 @@ function findGiftRef(obj) {
|
|
|
1244
1285
|
}
|
|
1245
1286
|
function normalizeWebhookEvent(event2, options = {}) {
|
|
1246
1287
|
const obj = event2.data?.object ?? {};
|
|
1288
|
+
if (event2.type.startsWith("charge.dispute.")) {
|
|
1289
|
+
const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
|
|
1290
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
|
|
1291
|
+
return {
|
|
1292
|
+
kind: "disputed",
|
|
1293
|
+
chargeId,
|
|
1294
|
+
disputeId: obj.id,
|
|
1295
|
+
disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
|
|
1296
|
+
...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1247
1299
|
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
1300
|
const gift = findGiftRef(obj);
|
|
1249
1301
|
if (gift.giftIntentId) {
|
|
@@ -1648,9 +1700,9 @@ async function backfillCrm(deps) {
|
|
|
1648
1700
|
}
|
|
1649
1701
|
|
|
1650
1702
|
// src/email-retirement.ts
|
|
1651
|
-
function isRetiredEmailCopy(subject,
|
|
1703
|
+
function isRetiredEmailCopy(subject, text10) {
|
|
1652
1704
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1653
|
-
const bodyCopy =
|
|
1705
|
+
const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1654
1706
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1655
1707
|
}
|
|
1656
1708
|
|
|
@@ -1728,10 +1780,10 @@ function planDelivery(input) {
|
|
|
1728
1780
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1729
1781
|
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1730
1782
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1731
|
-
const
|
|
1783
|
+
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1732
1784
|
|
|
1733
1785
|
` + renderedText : renderedText;
|
|
1734
|
-
return { deliver: true, transport, to, subject, text:
|
|
1786
|
+
return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
|
|
1735
1787
|
}
|
|
1736
1788
|
|
|
1737
1789
|
// src/notify.ts
|
|
@@ -1879,8 +1931,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1879
1931
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1880
1932
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1881
1933
|
function applicantVariables(app) {
|
|
1882
|
-
const
|
|
1883
|
-
return { firstName:
|
|
1934
|
+
const text10 = (v) => typeof v === "string" ? v : "";
|
|
1935
|
+
return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
|
|
1884
1936
|
}
|
|
1885
1937
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1886
1938
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -2987,7 +3039,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
|
2987
3039
|
if (!intent) throw new Error("membership checkout reconciliation pending");
|
|
2988
3040
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2989
3041
|
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)) {
|
|
3042
|
+
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
3043
|
throw new Error("invalid network renewal receipt");
|
|
2992
3044
|
}
|
|
2993
3045
|
return row2;
|
|
@@ -3612,6 +3664,163 @@ function paymentLineItems(group, tier) {
|
|
|
3612
3664
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
3613
3665
|
}
|
|
3614
3666
|
|
|
3667
|
+
// src/membership-dispute-authority.ts
|
|
3668
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
3669
|
+
|
|
3670
|
+
// src/membership-dispute-transition.ts
|
|
3671
|
+
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3672
|
+
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3673
|
+
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3674
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
3675
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3676
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
3677
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
3678
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3679
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
3680
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
3681
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3682
|
+
return {
|
|
3683
|
+
status: next,
|
|
3684
|
+
disputeId: fact.objectId,
|
|
3685
|
+
disputeStatus: status2,
|
|
3686
|
+
disputeEventId: eventId,
|
|
3687
|
+
disputeClosedAt: closed ? now : 0,
|
|
3688
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3689
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3690
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3691
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
3692
|
+
revision: Number(row2.revision) + 1,
|
|
3693
|
+
updatedAt: now
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// src/membership-seat-dispute.ts
|
|
3698
|
+
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3699
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
3700
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
|
|
3701
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
3702
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
3703
|
+
}
|
|
3704
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
3705
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
3706
|
+
const membership = async (applied, seat) => {
|
|
3707
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3708
|
+
requireMembershipScope(member, input);
|
|
3709
|
+
return {
|
|
3710
|
+
membershipId: id7,
|
|
3711
|
+
status: String(member.status),
|
|
3712
|
+
termEndsAt: Number(member.termEndsAt),
|
|
3713
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
3714
|
+
autoRenew: member.autoRenew === true,
|
|
3715
|
+
applied,
|
|
3716
|
+
...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
|
|
3717
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
3718
|
+
};
|
|
3719
|
+
};
|
|
3720
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3721
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
3722
|
+
if (!seat) return membership(false);
|
|
3723
|
+
requireMembershipScope(seat, input);
|
|
3724
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3725
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
3726
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
3727
|
+
if (!attrs) return membership(false, seat);
|
|
3728
|
+
try {
|
|
3729
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
3730
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
3731
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3732
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
3733
|
+
evidence.guard
|
|
3734
|
+
] });
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3737
|
+
continue;
|
|
3738
|
+
}
|
|
3739
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
3740
|
+
requireMembershipScope(saved, input);
|
|
3741
|
+
return membership(true, saved);
|
|
3742
|
+
}
|
|
3743
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// src/membership-dispute-authority.ts
|
|
3747
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3748
|
+
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3749
|
+
var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
|
|
3750
|
+
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3751
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3752
|
+
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
3753
|
+
const charge = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
|
|
3754
|
+
if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
|
|
3755
|
+
const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
|
|
3756
|
+
if (!customerId) membershipFail("membership_dispute_provider_unavailable");
|
|
3757
|
+
let subscriptionId;
|
|
3758
|
+
if (invoiceId) {
|
|
3759
|
+
const invoice = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
|
|
3760
|
+
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3761
|
+
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3762
|
+
}
|
|
3763
|
+
const seatId = text3(charge.body.metadata?.seatId);
|
|
3764
|
+
return {
|
|
3765
|
+
chargeId,
|
|
3766
|
+
customerId,
|
|
3767
|
+
...invoiceId ? { invoiceId } : {},
|
|
3768
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
3769
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
3770
|
+
...seatId ? { seatId } : {}
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function disputedChargeCoversMembership(charge, member) {
|
|
3774
|
+
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);
|
|
3775
|
+
}
|
|
3776
|
+
async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
3777
|
+
membershipClock(now);
|
|
3778
|
+
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3779
|
+
const fact = row2?.fact;
|
|
3780
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3781
|
+
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");
|
|
3782
|
+
const id7 = await networkMembershipId(input);
|
|
3783
|
+
const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
|
|
3784
|
+
membershipId: id7,
|
|
3785
|
+
status: String(value.status),
|
|
3786
|
+
termEndsAt: Number(value.termEndsAt),
|
|
3787
|
+
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3788
|
+
autoRenew: value.autoRenew === true,
|
|
3789
|
+
...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
|
|
3790
|
+
applied
|
|
3791
|
+
});
|
|
3792
|
+
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3793
|
+
requireMembershipScope(known, input);
|
|
3794
|
+
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3795
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
3796
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
3797
|
+
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3798
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3799
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
3800
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3801
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3802
|
+
requireMembershipScope(member, input);
|
|
3803
|
+
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3804
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
3805
|
+
if (!attrs) return receipt(member, false);
|
|
3806
|
+
try {
|
|
3807
|
+
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3808
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
3809
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3810
|
+
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3811
|
+
evidenceGuard
|
|
3812
|
+
] });
|
|
3813
|
+
} catch (error) {
|
|
3814
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3815
|
+
continue;
|
|
3816
|
+
}
|
|
3817
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
3818
|
+
requireMembershipScope(saved, input);
|
|
3819
|
+
return receipt(saved, true);
|
|
3820
|
+
}
|
|
3821
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3615
3824
|
// src/member-system-contract.ts
|
|
3616
3825
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3617
3826
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3736,7 +3945,8 @@ async function firstRow2(db, ns, q) {
|
|
|
3736
3945
|
}
|
|
3737
3946
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3738
3947
|
const db = ctx.makeDb(env);
|
|
3739
|
-
const { rawBody, eventId
|
|
3948
|
+
const { rawBody, eventId } = input;
|
|
3949
|
+
let { event: event2 } = input;
|
|
3740
3950
|
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3741
3951
|
const digest = await stripeEventDigest(rawBody);
|
|
3742
3952
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
@@ -3792,6 +4002,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3792
4002
|
}
|
|
3793
4003
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3794
4004
|
}
|
|
4005
|
+
if (event2.kind === "disputed" && !event2.customerId) {
|
|
4006
|
+
const secret = await getVaultSecret(db, "stripe_secret_key");
|
|
4007
|
+
const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
|
|
4008
|
+
if (charge) event2 = { ...event2, customerId: charge.customerId };
|
|
4009
|
+
}
|
|
3795
4010
|
const app = await findPaymentApplication(db, event2);
|
|
3796
4011
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3797
4012
|
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
@@ -3829,11 +4044,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3829
4044
|
}
|
|
3830
4045
|
let authoritativeEvent = event2;
|
|
3831
4046
|
let membershipProjection = {};
|
|
3832
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
4047
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
|
|
3833
4048
|
try {
|
|
3834
4049
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3835
4050
|
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 };
|
|
4051
|
+
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
4052
|
} catch {
|
|
3838
4053
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3839
4054
|
}
|
|
@@ -4482,10 +4697,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
4482
4697
|
}
|
|
4483
4698
|
|
|
4484
4699
|
// src/membership-seat-refund-stripe.ts
|
|
4485
|
-
var
|
|
4700
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4486
4701
|
|
|
4487
4702
|
// src/membership-checkout-seat-refund.ts
|
|
4488
|
-
var
|
|
4703
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4489
4704
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4490
4705
|
throw Object.assign(new Error(code), { code });
|
|
4491
4706
|
};
|
|
@@ -4494,7 +4709,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
4494
4709
|
const invoiceRef = seat.combinedInvoice;
|
|
4495
4710
|
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
4711
|
const call = async (method, path, params, key3) => {
|
|
4497
|
-
const result = await (0,
|
|
4712
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4498
4713
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4499
4714
|
return result.body;
|
|
4500
4715
|
};
|
|
@@ -4567,7 +4782,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
4567
4782
|
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4568
4783
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4569
4784
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4570
|
-
const result = await (0,
|
|
4785
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4571
4786
|
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4572
4787
|
return result.body;
|
|
4573
4788
|
};
|
|
@@ -4709,6 +4924,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4709
4924
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4710
4925
|
case "canceled":
|
|
4711
4926
|
return "canceled";
|
|
4927
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4928
|
+
case "payment_disputed":
|
|
4929
|
+
return "disputed";
|
|
4712
4930
|
default:
|
|
4713
4931
|
return "payment_pending";
|
|
4714
4932
|
}
|
|
@@ -5035,15 +5253,232 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5035
5253
|
}
|
|
5036
5254
|
};
|
|
5037
5255
|
|
|
5256
|
+
// src/membership-dispute-recharge.ts
|
|
5257
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5258
|
+
var NS = { membership: "networkMembership", seat: "networkSeat" };
|
|
5259
|
+
var fail6 = (code, extra = {}) => {
|
|
5260
|
+
throw Object.assign(new Error(code), { code, ...extra });
|
|
5261
|
+
};
|
|
5262
|
+
var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
|
|
5263
|
+
async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
5264
|
+
const subscription = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
5265
|
+
if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
|
|
5266
|
+
const onSubscription = refOf2(subscription.body.default_payment_method);
|
|
5267
|
+
if (onSubscription) return onSubscription;
|
|
5268
|
+
const customer = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
|
|
5269
|
+
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5270
|
+
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5271
|
+
}
|
|
5272
|
+
async function rechargeDisputed(db, kind, id7, secretKey, now) {
|
|
5273
|
+
membershipClock(now);
|
|
5274
|
+
const ns = NS[kind];
|
|
5275
|
+
let row2 = await membershipLoad(db, ns, id7);
|
|
5276
|
+
if (!row2) fail6("membership_not_found");
|
|
5277
|
+
const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
|
|
5278
|
+
const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
|
|
5279
|
+
const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
|
|
5280
|
+
const done = (value, replayed) => ({
|
|
5281
|
+
membershipId,
|
|
5282
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5283
|
+
status: String(value.status),
|
|
5284
|
+
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5285
|
+
amountCents,
|
|
5286
|
+
currency,
|
|
5287
|
+
...replayed ? { replayed } : {}
|
|
5288
|
+
});
|
|
5289
|
+
if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5290
|
+
if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
|
|
5291
|
+
fail6("membership_not_disputed");
|
|
5292
|
+
}
|
|
5293
|
+
if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5294
|
+
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5295
|
+
let attempt = row2.rechargeAttempt;
|
|
5296
|
+
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5297
|
+
attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
|
|
5298
|
+
await db.transact(
|
|
5299
|
+
[membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
|
|
5300
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
|
|
5301
|
+
);
|
|
5302
|
+
row2 = await membershipLoad(db, ns, id7);
|
|
5303
|
+
if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5304
|
+
}
|
|
5305
|
+
const config = { secretKey };
|
|
5306
|
+
const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
|
|
5307
|
+
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5308
|
+
const intent = await (0, import_stripe9.stripeRequest)(config, "POST", "/v1/payment_intents", {
|
|
5309
|
+
amount: amountCents,
|
|
5310
|
+
currency,
|
|
5311
|
+
customer: String(row2.providerCustomerId),
|
|
5312
|
+
payment_method: paymentMethod,
|
|
5313
|
+
off_session: "true",
|
|
5314
|
+
confirm: "true",
|
|
5315
|
+
description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
|
|
5316
|
+
metadata: {
|
|
5317
|
+
...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
|
|
5318
|
+
applicationId: String(row2.applicationId),
|
|
5319
|
+
membershipId,
|
|
5320
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5321
|
+
disputeId: String(row2.disputeId),
|
|
5322
|
+
membershipRecharge: "true"
|
|
5323
|
+
}
|
|
5324
|
+
}, { idempotencyKey: attempt.key });
|
|
5325
|
+
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5326
|
+
const finish = async (outcome) => {
|
|
5327
|
+
const current = await membershipLoad(db, ns, id7);
|
|
5328
|
+
if (!current) return;
|
|
5329
|
+
await db.transact([membershipUpdate(ns, id7, {
|
|
5330
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5331
|
+
revision: Number(current.revision) + 1,
|
|
5332
|
+
updatedAt: now
|
|
5333
|
+
})], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
|
|
5334
|
+
};
|
|
5335
|
+
if (!intent.ok) {
|
|
5336
|
+
if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
|
|
5337
|
+
await finish("declined");
|
|
5338
|
+
fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
|
|
5339
|
+
}
|
|
5340
|
+
fail6("membership_dispute_provider_unavailable");
|
|
5341
|
+
}
|
|
5342
|
+
if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
|
|
5343
|
+
await finish("declined");
|
|
5344
|
+
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5345
|
+
}
|
|
5346
|
+
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5347
|
+
if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5348
|
+
for (let retry = 0; retry < 8; retry++) {
|
|
5349
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
|
|
5350
|
+
if (!current) fail6("membership_not_found");
|
|
5351
|
+
if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
|
|
5352
|
+
const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5353
|
+
try {
|
|
5354
|
+
await db.transact(
|
|
5355
|
+
[membershipUpdate(ns, id7, {
|
|
5356
|
+
status: status2,
|
|
5357
|
+
disputeStatus: "recharged",
|
|
5358
|
+
rechargePaymentIntentId: String(body.id),
|
|
5359
|
+
rechargedAt: now,
|
|
5360
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
|
|
5361
|
+
revision: Number(current.revision) + 1,
|
|
5362
|
+
updatedAt: now
|
|
5363
|
+
})],
|
|
5364
|
+
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5365
|
+
{ ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5366
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5367
|
+
] }
|
|
5368
|
+
);
|
|
5369
|
+
} catch (cause) {
|
|
5370
|
+
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5371
|
+
continue;
|
|
5372
|
+
}
|
|
5373
|
+
return done(await membershipLoad(db, ns, id7));
|
|
5374
|
+
}
|
|
5375
|
+
return fail6("membership_dispute_retry_required");
|
|
5376
|
+
}
|
|
5377
|
+
var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
|
|
5378
|
+
var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
|
|
5379
|
+
|
|
5380
|
+
// src/worker-routes-admin-membership-disputes.ts
|
|
5381
|
+
var LIST2 = "/api/admin/membership-disputes";
|
|
5382
|
+
var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
|
|
5383
|
+
var PUBLIC2 = [
|
|
5384
|
+
"id",
|
|
5385
|
+
"appId",
|
|
5386
|
+
"chapterId",
|
|
5387
|
+
"environment",
|
|
5388
|
+
"runtime",
|
|
5389
|
+
"applicationId",
|
|
5390
|
+
"skuId",
|
|
5391
|
+
"status",
|
|
5392
|
+
"memberNumber",
|
|
5393
|
+
"amountCents",
|
|
5394
|
+
"currency",
|
|
5395
|
+
"recipientName",
|
|
5396
|
+
"primaryMembershipId",
|
|
5397
|
+
"purchasedAt",
|
|
5398
|
+
"disputeId",
|
|
5399
|
+
"disputeStatus",
|
|
5400
|
+
"disputeReason",
|
|
5401
|
+
"disputeAmountCents",
|
|
5402
|
+
"disputedAt",
|
|
5403
|
+
"disputeClosedAt",
|
|
5404
|
+
"disputePriorStatus",
|
|
5405
|
+
"rechargePaymentIntentId",
|
|
5406
|
+
"rechargedAt",
|
|
5407
|
+
"termEndsAt",
|
|
5408
|
+
"updatedAt"
|
|
5409
|
+
];
|
|
5410
|
+
var project2 = (row2) => ({
|
|
5411
|
+
...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
|
|
5412
|
+
...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
|
|
5413
|
+
startedAt: row2.rechargeAttempt.startedAt ?? null,
|
|
5414
|
+
finishedAt: row2.rechargeAttempt.finishedAt ?? null,
|
|
5415
|
+
outcome: row2.rechargeAttempt.outcome ?? null
|
|
5416
|
+
} } : {}
|
|
5417
|
+
});
|
|
5418
|
+
var FINAL = /* @__PURE__ */ new Set([
|
|
5419
|
+
"membership_not_found",
|
|
5420
|
+
"membership_not_disputed",
|
|
5421
|
+
"membership_dispute_open",
|
|
5422
|
+
"membership_dispute_amount_invalid",
|
|
5423
|
+
"recharge_payment_method_unavailable",
|
|
5424
|
+
"recharge_declined",
|
|
5425
|
+
"recharge_pending",
|
|
5426
|
+
"recharge_mismatch",
|
|
5427
|
+
"membership_dispute_changed"
|
|
5428
|
+
]);
|
|
5429
|
+
var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
5430
|
+
const one = url.pathname.match(ONE2);
|
|
5431
|
+
if (url.pathname !== LIST2 && !one) return null;
|
|
5432
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5433
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5434
|
+
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
5435
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5436
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5437
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5438
|
+
let db;
|
|
5439
|
+
try {
|
|
5440
|
+
db = operator.makeDb(env);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5443
|
+
}
|
|
5444
|
+
if (!one) {
|
|
5445
|
+
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5446
|
+
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5447
|
+
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5448
|
+
const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5449
|
+
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5450
|
+
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5451
|
+
}
|
|
5452
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5453
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5454
|
+
try {
|
|
5455
|
+
const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
|
|
5456
|
+
return json({ ok: true, ...receipt });
|
|
5457
|
+
} catch (error) {
|
|
5458
|
+
const failure = error;
|
|
5459
|
+
const code = typeof failure.code === "string" ? failure.code : void 0;
|
|
5460
|
+
if (code === "membership_not_found") return json({ error: "not found", code }, 404);
|
|
5461
|
+
if (code && FINAL.has(code)) {
|
|
5462
|
+
return json({
|
|
5463
|
+
error: "the charge could not be re-issued",
|
|
5464
|
+
code,
|
|
5465
|
+
...failure.declineCode ? { declineCode: failure.declineCode } : {},
|
|
5466
|
+
...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
|
|
5467
|
+
}, 409);
|
|
5468
|
+
}
|
|
5469
|
+
return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
|
|
5470
|
+
}
|
|
5471
|
+
};
|
|
5472
|
+
|
|
5038
5473
|
// src/membership-policy-versions.ts
|
|
5039
5474
|
var LIMIT = 200;
|
|
5040
|
-
var
|
|
5475
|
+
var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
5041
5476
|
var instant = (value) => {
|
|
5042
5477
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
5043
5478
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
5044
5479
|
};
|
|
5045
5480
|
function provenance(row2) {
|
|
5046
|
-
const source =
|
|
5481
|
+
const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
5047
5482
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
5048
5483
|
}
|
|
5049
5484
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -5137,16 +5572,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
5137
5572
|
};
|
|
5138
5573
|
|
|
5139
5574
|
// src/membership-discount-reterm.ts
|
|
5140
|
-
var
|
|
5575
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
5141
5576
|
|
|
5142
5577
|
// src/subscription-provider-contract.ts
|
|
5143
|
-
var
|
|
5578
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
5144
5579
|
function subscriptionContractMismatch() {
|
|
5145
5580
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
5146
5581
|
}
|
|
5147
5582
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
5148
5583
|
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,
|
|
5584
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
5150
5585
|
const price = result.body;
|
|
5151
5586
|
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
5587
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -5184,7 +5619,7 @@ async function retermPolicy(db, scope2, id7) {
|
|
|
5184
5619
|
}
|
|
5185
5620
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
5186
5621
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
5187
|
-
const read2 = await (0,
|
|
5622
|
+
const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
5188
5623
|
const sub = read2.body, items = sub?.items?.data;
|
|
5189
5624
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
5190
5625
|
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 +5630,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
5195
5630
|
const present = discounts.map(couponOf);
|
|
5196
5631
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
5197
5632
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
5198
|
-
const written = await (0,
|
|
5633
|
+
const written = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
5199
5634
|
"discounts[0][coupon]": couponId,
|
|
5200
5635
|
"expand[0]": "discounts",
|
|
5201
5636
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -6044,8 +6479,8 @@ var APPLICATION_LABELS = {
|
|
|
6044
6479
|
state: "State"
|
|
6045
6480
|
};
|
|
6046
6481
|
function humanize(id7) {
|
|
6047
|
-
const
|
|
6048
|
-
return
|
|
6482
|
+
const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6483
|
+
return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
|
|
6049
6484
|
}
|
|
6050
6485
|
function applicationDefs(chapter) {
|
|
6051
6486
|
try {
|
|
@@ -6112,7 +6547,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6112
6547
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6113
6548
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6114
6549
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6115
|
-
var
|
|
6550
|
+
var text5 = (value, path, errors, max) => {
|
|
6116
6551
|
if (typeof value !== "string" || !value.trim()) {
|
|
6117
6552
|
errors.push(`${path} is required`);
|
|
6118
6553
|
return "";
|
|
@@ -6140,23 +6575,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6140
6575
|
}
|
|
6141
6576
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6142
6577
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6143
|
-
const id7 =
|
|
6578
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6144
6579
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6145
6580
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6146
6581
|
ids2.add(id7);
|
|
6147
6582
|
const targetRaw = record(item.target);
|
|
6148
6583
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6149
6584
|
if (targetRaw?.type === "tier") {
|
|
6150
|
-
const tierId =
|
|
6585
|
+
const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6151
6586
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6152
6587
|
target = { type: "tier", tierId };
|
|
6153
6588
|
} else if (targetRaw?.type === "entitlement") {
|
|
6154
|
-
const entitlementKey =
|
|
6589
|
+
const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6155
6590
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6156
6591
|
target = { type: "entitlement", entitlementKey };
|
|
6157
6592
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6158
6593
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6159
|
-
const stripePriceId =
|
|
6594
|
+
const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6160
6595
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6161
6596
|
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
6597
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6178,9 +6613,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6178
6613
|
redemptionLifetimeHours: lifetime,
|
|
6179
6614
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6180
6615
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6181
|
-
title:
|
|
6182
|
-
description:
|
|
6183
|
-
ctaLabel:
|
|
6616
|
+
title: text5(item.title, `${path}.title`, errors, 160),
|
|
6617
|
+
description: text5(item.description, `${path}.description`, errors, 2e3),
|
|
6618
|
+
ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6184
6619
|
});
|
|
6185
6620
|
}
|
|
6186
6621
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6407,23 +6842,23 @@ async function createPaymentQuote(input) {
|
|
|
6407
6842
|
// src/membership-quote-projection.ts
|
|
6408
6843
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
6409
6844
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
6410
|
-
var
|
|
6845
|
+
var fail7 = (code) => {
|
|
6411
6846
|
throw Object.assign(new Error(code), { code });
|
|
6412
6847
|
};
|
|
6413
6848
|
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
6414
6849
|
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
|
|
6850
|
+
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");
|
|
6851
|
+
if (row2.status !== "ready") return fail7("membership_quote_pending");
|
|
6417
6852
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
6418
6853
|
...target,
|
|
6419
6854
|
requestId: row2.requestId
|
|
6420
6855
|
}), now, lifetimeMs: 6e4 });
|
|
6421
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6856
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
|
|
6422
6857
|
const tier = row2.tier, quote = row2.quote;
|
|
6423
6858
|
const offering = offer.contract.offering;
|
|
6424
|
-
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return
|
|
6859
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
|
|
6425
6860
|
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
|
|
6861
|
+
return fail7("membership_catalog_mismatch");
|
|
6427
6862
|
}
|
|
6428
6863
|
const expected = await createPaymentQuote({
|
|
6429
6864
|
...target,
|
|
@@ -6436,7 +6871,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
6436
6871
|
seatOffer: quote.seatOffer,
|
|
6437
6872
|
namedSeat: quote.namedSeat
|
|
6438
6873
|
});
|
|
6439
|
-
if (quote.digest !== expected.digest) return
|
|
6874
|
+
if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
|
|
6440
6875
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
6441
6876
|
}
|
|
6442
6877
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -6444,9 +6879,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
6444
6879
|
}
|
|
6445
6880
|
async function prepareMembershipQuote(db, input, issue) {
|
|
6446
6881
|
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
|
|
6882
|
+
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
6883
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6449
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6884
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
|
|
6450
6885
|
if (row2?.status === "ready") {
|
|
6451
6886
|
try {
|
|
6452
6887
|
return await restore2(row2, input, now);
|
|
@@ -6491,7 +6926,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6491
6926
|
}
|
|
6492
6927
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6493
6928
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6494
|
-
return
|
|
6929
|
+
return fail7("membership_quote_pending");
|
|
6495
6930
|
}
|
|
6496
6931
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6497
6932
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6540,7 +6975,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6540
6975
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6541
6976
|
}
|
|
6542
6977
|
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6543
|
-
if (!input.enabled) return
|
|
6978
|
+
if (!input.enabled) return fail7("named_seat_purchase_unavailable");
|
|
6544
6979
|
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6545
6980
|
const current = await restore2(row2, target, input.now);
|
|
6546
6981
|
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
@@ -6596,17 +7031,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6596
7031
|
if (!reader) return "";
|
|
6597
7032
|
const decoder = new TextDecoder();
|
|
6598
7033
|
let bytes = 0;
|
|
6599
|
-
let
|
|
7034
|
+
let text10 = "";
|
|
6600
7035
|
for (; ; ) {
|
|
6601
7036
|
const { done, value } = await reader.read();
|
|
6602
7037
|
if (req.signal.aborted) return invalid2();
|
|
6603
7038
|
if (done) {
|
|
6604
7039
|
complete = true;
|
|
6605
|
-
return
|
|
7040
|
+
return text10 + decoder.decode();
|
|
6606
7041
|
}
|
|
6607
7042
|
bytes += value.byteLength;
|
|
6608
7043
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6609
|
-
|
|
7044
|
+
text10 += decoder.decode(value, { stream: true });
|
|
6610
7045
|
}
|
|
6611
7046
|
} catch {
|
|
6612
7047
|
return invalid2();
|
|
@@ -6710,9 +7145,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
6710
7145
|
let selection;
|
|
6711
7146
|
if (request?.body) {
|
|
6712
7147
|
try {
|
|
6713
|
-
const
|
|
6714
|
-
if (
|
|
6715
|
-
const body = JSON.parse(
|
|
7148
|
+
const text10 = await readBoundedRequestText(request, 2048);
|
|
7149
|
+
if (text10 instanceof Response) return text10;
|
|
7150
|
+
const body = JSON.parse(text10);
|
|
6716
7151
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6717
7152
|
selection = body.namedSeat;
|
|
6718
7153
|
} catch {
|
|
@@ -7367,10 +7802,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7367
7802
|
const db = ctx.makeDb(env);
|
|
7368
7803
|
const groupId = url.searchParams.get("group") ?? chapter.id;
|
|
7369
7804
|
if (groupId !== chapter.id) return json({ error: "not found" }, 404);
|
|
7370
|
-
const { groups } = await
|
|
7805
|
+
const [{ groups }, stripeKey, activeControl] = await Promise.all([
|
|
7806
|
+
db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
|
|
7807
|
+
getVaultSecret(db, "stripe_secret_key"),
|
|
7808
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
|
|
7809
|
+
]);
|
|
7371
7810
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7372
7811
|
if (!group) return json({ error: "not found" }, 404);
|
|
7373
|
-
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7374
7812
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
7375
7813
|
const offered = await loadOfferableTiers(
|
|
7376
7814
|
db,
|
|
@@ -7379,7 +7817,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7379
7817
|
);
|
|
7380
7818
|
const seats = /* @__PURE__ */ new Map();
|
|
7381
7819
|
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7382
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7383
7820
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7384
7821
|
return json({
|
|
7385
7822
|
...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 +7854,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7417
7854
|
parsed.tierId = named.invitation.skuId;
|
|
7418
7855
|
submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
|
|
7419
7856
|
}
|
|
7420
|
-
const eligibility
|
|
7857
|
+
const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
|
|
7858
|
+
named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
|
|
7859
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
|
|
7860
|
+
typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
|
|
7861
|
+
db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
|
|
7862
|
+
]);
|
|
7421
7863
|
if (eligibility?.conflict) return eligibility.conflict;
|
|
7422
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7423
7864
|
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
7865
|
const now = Date.now();
|
|
7426
7866
|
const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
|
|
7427
7867
|
const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
|
|
@@ -7460,18 +7900,16 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7460
7900
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
7461
7901
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
7462
7902
|
}
|
|
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;
|
|
7903
|
+
if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
|
|
7466
7904
|
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
7467
|
-
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
7905
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
|
|
7468
7906
|
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
7469
7907
|
if (!state) return json({
|
|
7470
7908
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
7471
7909
|
code: "application_continuation_unavailable",
|
|
7472
7910
|
id: result.id
|
|
7473
7911
|
}, 503);
|
|
7474
|
-
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
7912
|
+
if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
|
|
7475
7913
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
7476
7914
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
7477
7915
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -7591,10 +8029,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7591
8029
|
errors.push(`${path} must be a string`);
|
|
7592
8030
|
return void 0;
|
|
7593
8031
|
}
|
|
7594
|
-
const
|
|
7595
|
-
if (required && !
|
|
7596
|
-
if (
|
|
7597
|
-
return
|
|
8032
|
+
const text10 = value.trim();
|
|
8033
|
+
if (required && !text10) errors.push(`${path} is required`);
|
|
8034
|
+
if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
8035
|
+
return text10;
|
|
7598
8036
|
}
|
|
7599
8037
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7600
8038
|
const errors = [];
|
|
@@ -7643,8 +8081,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
7643
8081
|
else {
|
|
7644
8082
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
7645
8083
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
7646
|
-
const
|
|
7647
|
-
return
|
|
8084
|
+
const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8085
|
+
return text10 ? [text10] : [];
|
|
7648
8086
|
});
|
|
7649
8087
|
}
|
|
7650
8088
|
}
|
|
@@ -7738,7 +8176,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
7738
8176
|
function unknownKeys(value, allowed2, path, errors) {
|
|
7739
8177
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7740
8178
|
}
|
|
7741
|
-
function
|
|
8179
|
+
function text6(value, path, errors, max = 65536) {
|
|
7742
8180
|
if (typeof value !== "string" || !value.trim()) {
|
|
7743
8181
|
errors.push(`${path} is required`);
|
|
7744
8182
|
return "";
|
|
@@ -7761,11 +8199,11 @@ function tiersOf(value, errors) {
|
|
|
7761
8199
|
return [];
|
|
7762
8200
|
}
|
|
7763
8201
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7764
|
-
const id7 =
|
|
8202
|
+
const id7 = text6(item.id, `${path}.id`, errors, 80);
|
|
7765
8203
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7766
8204
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7767
8205
|
seen.add(id7);
|
|
7768
|
-
const name =
|
|
8206
|
+
const name = text6(item.name, `${path}.name`, errors, 160);
|
|
7769
8207
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7770
8208
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
7771
8209
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -7783,7 +8221,7 @@ function tiersOf(value, errors) {
|
|
|
7783
8221
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
7784
8222
|
errors.push(`${path}.interval must be year or month`);
|
|
7785
8223
|
}
|
|
7786
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8224
|
+
const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7787
8225
|
return [{
|
|
7788
8226
|
id: id7,
|
|
7789
8227
|
name,
|
|
@@ -7841,7 +8279,7 @@ function policyOf(value, errors, tiers) {
|
|
|
7841
8279
|
const result = {};
|
|
7842
8280
|
for (const key3 of allowed2) {
|
|
7843
8281
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
7844
|
-
result[key3] =
|
|
8282
|
+
result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
|
|
7845
8283
|
}
|
|
7846
8284
|
const tierPolicies = {};
|
|
7847
8285
|
if (input.tierPolicies !== void 0) {
|
|
@@ -7856,8 +8294,8 @@ function policyOf(value, errors, tiers) {
|
|
|
7856
8294
|
}
|
|
7857
8295
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7858
8296
|
tierPolicies[id7] = {
|
|
7859
|
-
refundPolicyText:
|
|
7860
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8297
|
+
refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8298
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7861
8299
|
};
|
|
7862
8300
|
}
|
|
7863
8301
|
}
|
|
@@ -7897,9 +8335,9 @@ function validateSignupControlDocument(input) {
|
|
|
7897
8335
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
7898
8336
|
const source = recordOf(root.source);
|
|
7899
8337
|
const target = recordOf(root.target);
|
|
7900
|
-
const appId =
|
|
7901
|
-
const chapterId =
|
|
7902
|
-
const environment =
|
|
8338
|
+
const appId = text6(source?.appId, "document.source.appId", errors, 63);
|
|
8339
|
+
const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8340
|
+
const environment = text6(target?.environment, "document.target.environment", errors, 63);
|
|
7903
8341
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7904
8342
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
7905
8343
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9411,8 +9849,8 @@ async function bookSlot(req, env, ctx) {
|
|
|
9411
9849
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
9412
9850
|
await db.transact([meetingOp, appOp]);
|
|
9413
9851
|
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 } });
|
|
9852
|
+
await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
|
|
9853
|
+
await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
|
|
9416
9854
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
9417
9855
|
}
|
|
9418
9856
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -9436,28 +9874,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
9436
9874
|
};
|
|
9437
9875
|
|
|
9438
9876
|
// src/payments-stripe.ts
|
|
9439
|
-
var
|
|
9877
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
9440
9878
|
|
|
9441
9879
|
// src/payments-refund.ts
|
|
9442
|
-
var
|
|
9880
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
9443
9881
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9444
9882
|
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
9445
9883
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
9446
|
-
function
|
|
9884
|
+
function fail8(code = "refund_target_mismatch") {
|
|
9447
9885
|
throw Object.assign(new Error(code), { code });
|
|
9448
9886
|
}
|
|
9449
9887
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
9450
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9888
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
|
|
9451
9889
|
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))
|
|
9890
|
+
const result = await (0, import_stripe12.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9891
|
+
if (!result.ok) fail8("stripe_error");
|
|
9892
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
|
|
9455
9893
|
return result.body;
|
|
9456
9894
|
};
|
|
9457
9895
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9458
|
-
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId)
|
|
9896
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
|
|
9459
9897
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
9460
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9898
|
+
if (row(sub.metadata)[key3] !== value) fail8();
|
|
9461
9899
|
}
|
|
9462
9900
|
let cursor;
|
|
9463
9901
|
let earliest;
|
|
@@ -9470,66 +9908,66 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
9470
9908
|
limit: 100,
|
|
9471
9909
|
...cursor ? { starting_after: cursor } : {}
|
|
9472
9910
|
});
|
|
9473
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9911
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
|
|
9474
9912
|
for (const value of list.data) {
|
|
9475
9913
|
const invoice2 = row(value);
|
|
9476
9914
|
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)
|
|
9915
|
+
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
9916
|
seen.add(id3(invoice2));
|
|
9479
9917
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
9480
9918
|
}
|
|
9481
9919
|
if (!list.has_more) break;
|
|
9482
|
-
if (!list.data.length || page === 9)
|
|
9920
|
+
if (!list.data.length || page === 9) fail8("refund_history_incomplete");
|
|
9483
9921
|
cursor = id3(list.data[list.data.length - 1]);
|
|
9484
9922
|
}
|
|
9485
|
-
if (!earliest)
|
|
9923
|
+
if (!earliest) fail8("no_charge");
|
|
9486
9924
|
const invoice = earliest;
|
|
9487
|
-
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
9925
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
|
|
9488
9926
|
let chargeId = id3(invoice.charge);
|
|
9489
9927
|
let paymentIntentId = id3(invoice.payment_intent);
|
|
9490
9928
|
if (!chargeId && !paymentIntentId) {
|
|
9491
9929
|
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)
|
|
9930
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
|
|
9493
9931
|
const payment = row(payments.data[0]);
|
|
9494
|
-
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
9932
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
|
|
9495
9933
|
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
9496
9934
|
}
|
|
9497
9935
|
if (!chargeId) {
|
|
9498
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9936
|
+
if (!providerId2(paymentIntentId, "pi")) fail8();
|
|
9499
9937
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
9500
|
-
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
9938
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
|
|
9501
9939
|
chargeId = id3(payment.latest_charge);
|
|
9502
9940
|
}
|
|
9503
|
-
if (!providerId2(chargeId, "ch"))
|
|
9941
|
+
if (!providerId2(chargeId, "ch")) fail8();
|
|
9504
9942
|
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))
|
|
9943
|
+
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();
|
|
9944
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
|
|
9507
9945
|
if (charge.amount_refunded !== charge.amount) {
|
|
9508
9946
|
const refund = await call("POST", "/v1/refunds", {
|
|
9509
9947
|
charge: chargeId,
|
|
9510
9948
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
9511
9949
|
}, `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))
|
|
9950
|
+
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
9951
|
}
|
|
9514
9952
|
let subscriptionCanceled = sub.status === "canceled";
|
|
9515
9953
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
9516
9954
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9517
|
-
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
9955
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
|
|
9518
9956
|
subscriptionCanceled = true;
|
|
9519
9957
|
}
|
|
9520
9958
|
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)
|
|
9959
|
+
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
9960
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
9523
9961
|
}
|
|
9524
9962
|
|
|
9525
9963
|
// src/subscription-payment-identity.ts
|
|
9526
|
-
var
|
|
9964
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
9527
9965
|
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
9528
9966
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
9529
9967
|
let paymentId = id4(invoice.payment_intent);
|
|
9530
9968
|
if (!paymentId) {
|
|
9531
9969
|
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9532
|
-
const result2 = await (0,
|
|
9970
|
+
const result2 = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
9533
9971
|
const list = result2.body;
|
|
9534
9972
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
9535
9973
|
const link = list.data[0];
|
|
@@ -9538,11 +9976,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9538
9976
|
}
|
|
9539
9977
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
9540
9978
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
9541
|
-
const result = await (0,
|
|
9979
|
+
const result = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", path);
|
|
9542
9980
|
const payment = result.body;
|
|
9543
9981
|
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
9982
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
9545
|
-
const tagged = await (0,
|
|
9983
|
+
const tagged = await (0, import_stripe13.stripeRequest)(
|
|
9546
9984
|
{ secretKey },
|
|
9547
9985
|
"POST",
|
|
9548
9986
|
path,
|
|
@@ -9554,7 +9992,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9554
9992
|
|
|
9555
9993
|
// src/payments-stripe.ts
|
|
9556
9994
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9557
|
-
return (0,
|
|
9995
|
+
return (0, import_stripe14.stripeRequest)(
|
|
9558
9996
|
{ secretKey: sk },
|
|
9559
9997
|
method,
|
|
9560
9998
|
path,
|
|
@@ -9562,7 +10000,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9562
10000
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9563
10001
|
);
|
|
9564
10002
|
}
|
|
9565
|
-
function
|
|
10003
|
+
function fail9(op, r) {
|
|
9566
10004
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9567
10005
|
err.code = "stripe_error";
|
|
9568
10006
|
throw err;
|
|
@@ -9621,7 +10059,7 @@ function createStripeProvider(config) {
|
|
|
9621
10059
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9622
10060
|
`cus:${input.applicationId}`
|
|
9623
10061
|
);
|
|
9624
|
-
if (!cust.ok)
|
|
10062
|
+
if (!cust.ok) fail9("customer create", cust);
|
|
9625
10063
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9626
10064
|
customerId = cust.body.id;
|
|
9627
10065
|
}
|
|
@@ -9666,7 +10104,7 @@ function createStripeProvider(config) {
|
|
|
9666
10104
|
// retry between create and the db write can't orphan a second one.
|
|
9667
10105
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9668
10106
|
);
|
|
9669
|
-
if (!sub.ok)
|
|
10107
|
+
if (!sub.ok) fail9("subscription create", sub);
|
|
9670
10108
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9671
10109
|
const subscriptionId = sub.body.id;
|
|
9672
10110
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9676,14 +10114,14 @@ function createStripeProvider(config) {
|
|
|
9676
10114
|
"expand[2]": "discounts",
|
|
9677
10115
|
"expand[3]": "latest_invoice.discounts"
|
|
9678
10116
|
});
|
|
9679
|
-
if (!expanded.ok)
|
|
10117
|
+
if (!expanded.ok) fail9("subscription invoice evidence", expanded);
|
|
9680
10118
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9681
10119
|
sub = expanded;
|
|
9682
10120
|
}
|
|
9683
10121
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9684
10122
|
const invoice = sub.body.latest_invoice;
|
|
9685
10123
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9686
|
-
if (!clientSecret2)
|
|
10124
|
+
if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
|
|
9687
10125
|
await tagSubscriptionPayment(
|
|
9688
10126
|
sk,
|
|
9689
10127
|
invoice,
|
|
@@ -9695,7 +10133,7 @@ function createStripeProvider(config) {
|
|
|
9695
10133
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
9696
10134
|
},
|
|
9697
10135
|
async ingestWebhook(rawBody, sigHeader) {
|
|
9698
|
-
if (!webhookSecret || !await (0,
|
|
10136
|
+
if (!webhookSecret || !await (0, import_stripe14.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
9699
10137
|
return { ok: false, reason: "bad_signature" };
|
|
9700
10138
|
}
|
|
9701
10139
|
const event2 = JSON.parse(rawBody);
|
|
@@ -10170,7 +10608,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10170
10608
|
|
|
10171
10609
|
// src/worker-payment-receipt.ts
|
|
10172
10610
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10173
|
-
var escape = (
|
|
10611
|
+
var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10174
10612
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10175
10613
|
const id7 = url.searchParams.get("application");
|
|
10176
10614
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10181,7 +10619,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10181
10619
|
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
10620
|
const receipt = intent.paymentReceipt;
|
|
10183
10621
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10184
|
-
const
|
|
10622
|
+
const text10 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10185
10623
|
if (req.method === "POST") {
|
|
10186
10624
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10187
10625
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10199,7 +10637,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10199
10637
|
template: "paymentReceipt",
|
|
10200
10638
|
to: String(intent.purchaserEmailNormalized),
|
|
10201
10639
|
applicationId: id7,
|
|
10202
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10640
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text10 },
|
|
10203
10641
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10204
10642
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10205
10643
|
});
|
|
@@ -10209,7 +10647,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10209
10647
|
const nonce = crypto.randomUUID();
|
|
10210
10648
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10211
10649
|
<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(
|
|
10650
|
+
<h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
|
|
10213
10651
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10214
10652
|
headers: {
|
|
10215
10653
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10679,16 +11117,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
10679
11117
|
};
|
|
10680
11118
|
|
|
10681
11119
|
// src/named-seat-accepted-notice.ts
|
|
10682
|
-
var
|
|
11120
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
10683
11121
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
10684
11122
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
10685
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
11123
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
|
|
10686
11124
|
return { sent: false, reason: "purchaser-missing" };
|
|
10687
11125
|
}
|
|
10688
11126
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
10689
11127
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
10690
11128
|
const config = emailGroupFrom(group);
|
|
10691
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
11129
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
|
|
10692
11130
|
return sendTemplated({
|
|
10693
11131
|
db,
|
|
10694
11132
|
envName: env.ODLA_ENV,
|
|
@@ -10700,12 +11138,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
10700
11138
|
}, {
|
|
10701
11139
|
group: config,
|
|
10702
11140
|
template: "seatAccepted",
|
|
10703
|
-
to:
|
|
11141
|
+
to: text7(purchaser.email),
|
|
10704
11142
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
10705
11143
|
applicationId: input.primaryApplicationId,
|
|
10706
11144
|
tierId: input.skuId,
|
|
10707
11145
|
vars: {
|
|
10708
|
-
firstName:
|
|
11146
|
+
firstName: text7(purchaser.firstName),
|
|
10709
11147
|
purchaserName,
|
|
10710
11148
|
recipientName: input.recipientName,
|
|
10711
11149
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -12172,12 +12610,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12172
12610
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12173
12611
|
const t = value;
|
|
12174
12612
|
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:
|
|
12613
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12614
|
+
const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12177
12615
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12178
12616
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12179
12617
|
}
|
|
12180
|
-
clean[key3] = { subject, text:
|
|
12618
|
+
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12181
12619
|
}
|
|
12182
12620
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12183
12621
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -14381,9 +14819,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14381
14819
|
if (req.method === "POST") {
|
|
14382
14820
|
let body;
|
|
14383
14821
|
try {
|
|
14384
|
-
const
|
|
14385
|
-
if (new TextEncoder().encode(
|
|
14386
|
-
const parsed = JSON.parse(
|
|
14822
|
+
const text10 = await req.text();
|
|
14823
|
+
if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14824
|
+
const parsed = JSON.parse(text10);
|
|
14387
14825
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14388
14826
|
body = parsed;
|
|
14389
14827
|
} catch {
|
|
@@ -14544,7 +14982,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14544
14982
|
};
|
|
14545
14983
|
|
|
14546
14984
|
// src/worker-routes-network-commercial.ts
|
|
14547
|
-
var
|
|
14985
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14548
14986
|
|
|
14549
14987
|
// src/worker-routes-network-commercial-parity.ts
|
|
14550
14988
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -14552,7 +14990,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
14552
14990
|
// src/network-commercial-parity.ts
|
|
14553
14991
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14554
14992
|
var MAX_RECORDS = 500;
|
|
14555
|
-
var
|
|
14993
|
+
var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14556
14994
|
function parityMessage(partition, kind, value) {
|
|
14557
14995
|
return [
|
|
14558
14996
|
"chapter-commercial-parity",
|
|
@@ -14584,18 +15022,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14584
15022
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14585
15023
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14586
15024
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14587
|
-
const eligible = input.rows.filter((row2) =>
|
|
15025
|
+
const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
|
|
14588
15026
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14589
15027
|
const key3 = await parityKey(input.secret);
|
|
14590
15028
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14591
|
-
const applicationId =
|
|
14592
|
-
const customerId =
|
|
14593
|
-
const subscriptionId =
|
|
15029
|
+
const applicationId = text8(row2.id);
|
|
15030
|
+
const customerId = text8(row2.stripeCustomerId);
|
|
15031
|
+
const subscriptionId = text8(row2.stripeSubscriptionId);
|
|
14594
15032
|
return {
|
|
14595
15033
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14596
15034
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14597
15035
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14598
|
-
status:
|
|
15036
|
+
status: text8(row2.status, 64),
|
|
14599
15037
|
canceled: row2.canceled === true
|
|
14600
15038
|
};
|
|
14601
15039
|
}));
|
|
@@ -14621,7 +15059,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14621
15059
|
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
15060
|
const seen = /* @__PURE__ */ new Set();
|
|
14623
15061
|
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 &&
|
|
15062
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
14625
15063
|
const key3 = recordKey(record4);
|
|
14626
15064
|
if (seen.has(key3)) return false;
|
|
14627
15065
|
seen.add(key3);
|
|
@@ -14632,16 +15070,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14632
15070
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14633
15071
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14634
15072
|
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 =
|
|
15073
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
|
|
15074
|
+
const customerId = text8(row2.customerId);
|
|
15075
|
+
const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
|
|
14638
15076
|
return {
|
|
14639
15077
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
14640
15078
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
14641
15079
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
14642
15080
|
status: null,
|
|
14643
15081
|
canceled: false,
|
|
14644
|
-
stripeStatus:
|
|
15082
|
+
stripeStatus: text8(row2.status, 64),
|
|
14645
15083
|
stripeCanceled: row2.status === "canceled"
|
|
14646
15084
|
};
|
|
14647
15085
|
}));
|
|
@@ -14838,6 +15276,40 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
|
14838
15276
|
};
|
|
14839
15277
|
}
|
|
14840
15278
|
|
|
15279
|
+
// src/worker-network-commercial-dispute.ts
|
|
15280
|
+
async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
15281
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
15282
|
+
if (!operator || !fact.chargeId) return { disposition: "unhosted" };
|
|
15283
|
+
let db;
|
|
15284
|
+
try {
|
|
15285
|
+
db = operator.makeDb(env);
|
|
15286
|
+
} catch {
|
|
15287
|
+
return { disposition: "unhosted" };
|
|
15288
|
+
}
|
|
15289
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15290
|
+
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15291
|
+
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15292
|
+
const scopeOf = (row2) => ({
|
|
15293
|
+
appId: String(row2.appId),
|
|
15294
|
+
chapterId: String(row2.chapterId),
|
|
15295
|
+
environment: String(row2.environment),
|
|
15296
|
+
runtime: String(row2.runtime),
|
|
15297
|
+
applicationId: String(row2.applicationId),
|
|
15298
|
+
skuId: String(row2.skuId)
|
|
15299
|
+
});
|
|
15300
|
+
if (charge.seatId) {
|
|
15301
|
+
const seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
15302
|
+
if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
|
|
15303
|
+
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15304
|
+
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15305
|
+
}
|
|
15306
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15307
|
+
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15308
|
+
if (!member) return { disposition: "not_applicable" };
|
|
15309
|
+
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15310
|
+
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15311
|
+
}
|
|
15312
|
+
|
|
14841
15313
|
// src/worker-routes-network-commercial.ts
|
|
14842
15314
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
14843
15315
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -14902,6 +15374,12 @@ function safeProjection(projection) {
|
|
|
14902
15374
|
amountReceivedCents: seat.amountReceivedCents,
|
|
14903
15375
|
refundedCents: seat.refundedCents ?? 0
|
|
14904
15376
|
})),
|
|
15377
|
+
disputes: (projection.disputes ?? []).map((dispute2) => ({
|
|
15378
|
+
...row2(dispute2),
|
|
15379
|
+
chargeId: dispute2.chargeId ?? null,
|
|
15380
|
+
paymentIntentId: dispute2.paymentIntentId ?? null,
|
|
15381
|
+
reason: dispute2.disputeReason ?? null
|
|
15382
|
+
})),
|
|
14905
15383
|
exceptions: projection.exceptions
|
|
14906
15384
|
};
|
|
14907
15385
|
}
|
|
@@ -14920,7 +15398,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14920
15398
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
14921
15399
|
}
|
|
14922
15400
|
try {
|
|
14923
|
-
const event2 = await (0,
|
|
15401
|
+
const event2 = await (0, import_stripe15.constructStripeEvent)(body, signature, secret);
|
|
14924
15402
|
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
14925
15403
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
14926
15404
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -14934,14 +15412,24 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14934
15412
|
if (result.status === "collision") {
|
|
14935
15413
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
14936
15414
|
}
|
|
15415
|
+
let dispute2;
|
|
15416
|
+
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15417
|
+
try {
|
|
15418
|
+
dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15419
|
+
} catch (error) {
|
|
15420
|
+
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15421
|
+
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
15422
|
+
}
|
|
15423
|
+
}
|
|
14937
15424
|
return json({
|
|
14938
15425
|
ok: true,
|
|
14939
15426
|
status: result.status,
|
|
14940
15427
|
eventId: fact.eventId,
|
|
15428
|
+
...dispute2 ? { dispute: dispute2.disposition } : {},
|
|
14941
15429
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14942
15430
|
});
|
|
14943
15431
|
} catch (error) {
|
|
14944
|
-
if (error instanceof
|
|
15432
|
+
if (error instanceof import_stripe15.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
14945
15433
|
throw error;
|
|
14946
15434
|
}
|
|
14947
15435
|
};
|
|
@@ -15004,7 +15492,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15004
15492
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
15005
15493
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
15006
15494
|
try {
|
|
15007
|
-
const stripe = (0,
|
|
15495
|
+
const stripe = (0, import_stripe15.initStripe)({ secretKey });
|
|
15008
15496
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
15009
15497
|
listStripeObjects(stripe, "/v1/customers"),
|
|
15010
15498
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -15018,7 +15506,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15018
15506
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
15019
15507
|
});
|
|
15020
15508
|
} catch (error) {
|
|
15021
|
-
if (error instanceof
|
|
15509
|
+
if (error instanceof import_stripe15.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
15022
15510
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
15023
15511
|
}
|
|
15024
15512
|
}
|
|
@@ -15135,7 +15623,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15135
15623
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15136
15624
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15137
15625
|
}
|
|
15138
|
-
function
|
|
15626
|
+
function text9(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15139
15627
|
if (value === void 0 && !required) return void 0;
|
|
15140
15628
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15141
15629
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15161,7 +15649,7 @@ function scopeValue(value, path, errors) {
|
|
|
15161
15649
|
return { kind: raw2.kind };
|
|
15162
15650
|
}
|
|
15163
15651
|
if (raw2.kind === "chapter") {
|
|
15164
|
-
const chapterId =
|
|
15652
|
+
const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15165
15653
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15166
15654
|
return { kind: "chapter", chapterId };
|
|
15167
15655
|
}
|
|
@@ -15175,22 +15663,22 @@ function resourceValue(value, path, errors) {
|
|
|
15175
15663
|
return null;
|
|
15176
15664
|
}
|
|
15177
15665
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15178
|
-
const id7 =
|
|
15666
|
+
const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15179
15667
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15180
|
-
const kind =
|
|
15668
|
+
const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15181
15669
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15182
|
-
const title =
|
|
15183
|
-
const summary =
|
|
15184
|
-
const status2 =
|
|
15670
|
+
const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15671
|
+
const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15672
|
+
const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
|
|
15185
15673
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15186
15674
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15187
15675
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15188
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15676
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15189
15677
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15190
15678
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15191
15679
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15192
|
-
const bodyMarkdown =
|
|
15193
|
-
const url =
|
|
15680
|
+
const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15681
|
+
const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15194
15682
|
if (url) {
|
|
15195
15683
|
try {
|
|
15196
15684
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15202,9 +15690,9 @@ function resourceValue(value, path, errors) {
|
|
|
15202
15690
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15203
15691
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15204
15692
|
const brandAsset = asset ? {
|
|
15205
|
-
bookId:
|
|
15206
|
-
assetId:
|
|
15207
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15693
|
+
bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15694
|
+
assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15695
|
+
...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15208
15696
|
} : void 0;
|
|
15209
15697
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15210
15698
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15219,17 +15707,17 @@ function resourceValue(value, path, errors) {
|
|
|
15219
15707
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15220
15708
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15221
15709
|
const promotedFrom = promotedRaw ? {
|
|
15222
|
-
chapterId:
|
|
15223
|
-
resourceId:
|
|
15710
|
+
chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15711
|
+
resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15224
15712
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15225
15713
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15226
|
-
promotedBy:
|
|
15714
|
+
promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15227
15715
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15228
15716
|
} : void 0;
|
|
15229
15717
|
const provenance2 = {
|
|
15230
|
-
createdBy:
|
|
15718
|
+
createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15231
15719
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15232
|
-
updatedBy:
|
|
15720
|
+
updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15233
15721
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15234
15722
|
...promotedFrom ? { promotedFrom } : {}
|
|
15235
15723
|
};
|
|
@@ -15248,7 +15736,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15248
15736
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15249
15737
|
const sourceType = sourceRaw?.type;
|
|
15250
15738
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15251
|
-
const sourceAppId =
|
|
15739
|
+
const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15252
15740
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15253
15741
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15254
15742
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15256,8 +15744,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15256
15744
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15257
15745
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15258
15746
|
const target = targetRaw ? {
|
|
15259
|
-
appId:
|
|
15260
|
-
environment:
|
|
15747
|
+
appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15748
|
+
environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15261
15749
|
} : void 0;
|
|
15262
15750
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15263
15751
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15598,7 +16086,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
15598
16086
|
};
|
|
15599
16087
|
|
|
15600
16088
|
// src/membership-cancel-authority.ts
|
|
15601
|
-
var
|
|
16089
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15602
16090
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
15603
16091
|
membershipClock(now);
|
|
15604
16092
|
const id7 = await networkMembershipId(target);
|
|
@@ -15609,14 +16097,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
15609
16097
|
membershipFail("membership_cancellation_unavailable");
|
|
15610
16098
|
}
|
|
15611
16099
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
15612
|
-
const read2 = await (0,
|
|
16100
|
+
const read2 = await (0, import_stripe16.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
15613
16101
|
const sub = read2.body;
|
|
15614
16102
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
15615
16103
|
...stripePartitionMetadata(target),
|
|
15616
16104
|
applicationId: target.applicationId
|
|
15617
16105
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
15618
16106
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
15619
|
-
const write = await (0,
|
|
16107
|
+
const write = await (0, import_stripe16.stripeRequest)(
|
|
15620
16108
|
{ secretKey: stripeSecret },
|
|
15621
16109
|
"POST",
|
|
15622
16110
|
path,
|
|
@@ -15838,7 +16326,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
15838
16326
|
}
|
|
15839
16327
|
|
|
15840
16328
|
// src/membership-provider-lifecycle.ts
|
|
15841
|
-
var
|
|
16329
|
+
var import_stripe17 = require("@odla-ai/stripe");
|
|
15842
16330
|
|
|
15843
16331
|
// src/membership-renewal-authority.ts
|
|
15844
16332
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -15960,6 +16448,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15960
16448
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
15961
16449
|
const fact = row2?.fact;
|
|
15962
16450
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
16451
|
+
if (fact?.objectKind === "dispute") {
|
|
16452
|
+
const secret = await getVaultSecret(db, stripeSecretName);
|
|
16453
|
+
if (!secret) membershipFail("membership_billing_provider_unavailable");
|
|
16454
|
+
return reconcileMembershipDispute(db, input, now, secret);
|
|
16455
|
+
}
|
|
15963
16456
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15964
16457
|
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
16458
|
appId: input.appId,
|
|
@@ -16002,7 +16495,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
16002
16495
|
}
|
|
16003
16496
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
16004
16497
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
16005
|
-
const result = await (0,
|
|
16498
|
+
const result = await (0, import_stripe17.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
16006
16499
|
const sub = result.body;
|
|
16007
16500
|
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
16501
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -16040,7 +16533,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
16040
16533
|
}
|
|
16041
16534
|
|
|
16042
16535
|
// src/membership-seat-stripe.ts
|
|
16043
|
-
var
|
|
16536
|
+
var import_stripe18 = require("@odla-ai/stripe");
|
|
16044
16537
|
var reject7 = () => {
|
|
16045
16538
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
16046
16539
|
};
|
|
@@ -16052,7 +16545,7 @@ var metadataFor = (seat) => ({
|
|
|
16052
16545
|
});
|
|
16053
16546
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
16054
16547
|
async function read(sk, path, params) {
|
|
16055
|
-
const response = await (0,
|
|
16548
|
+
const response = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
16056
16549
|
if (!response.ok) return reject7();
|
|
16057
16550
|
return response.body;
|
|
16058
16551
|
}
|
|
@@ -16089,11 +16582,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
16089
16582
|
}
|
|
16090
16583
|
await subscriptionForSeat(sk, seat);
|
|
16091
16584
|
const metadata = metadataFor(seat);
|
|
16092
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
16585
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe18.stripeRequest)(
|
|
16093
16586
|
{ secretKey: sk },
|
|
16094
16587
|
"GET",
|
|
16095
16588
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
16096
|
-
) : await (0,
|
|
16589
|
+
) : await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
16097
16590
|
amount: seat.amountCents,
|
|
16098
16591
|
currency: seat.currency,
|
|
16099
16592
|
customer: seat.providerCustomerId,
|
|
@@ -16115,7 +16608,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
16115
16608
|
return String(existing.id);
|
|
16116
16609
|
}
|
|
16117
16610
|
if (seat.combinedInvoice) reject7();
|
|
16118
|
-
const result = await (0,
|
|
16611
|
+
const result = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
16119
16612
|
subscription: seat.providerSubscriptionId,
|
|
16120
16613
|
price: seat.priceId,
|
|
16121
16614
|
quantity: 1,
|
|
@@ -16777,6 +17270,19 @@ var id5 = () => attr("string", false, true, true);
|
|
|
16777
17270
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
16778
17271
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
16779
17272
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
17273
|
+
var dispute = () => ({
|
|
17274
|
+
disputeId: key(true),
|
|
17275
|
+
disputeStatus: attr("string", true),
|
|
17276
|
+
disputeReason: attr("string", true),
|
|
17277
|
+
disputeAmountCents: attr("number", true),
|
|
17278
|
+
disputedAt: attr("number", true),
|
|
17279
|
+
disputeClosedAt: attr("number", true),
|
|
17280
|
+
disputePriorStatus: attr("string", true),
|
|
17281
|
+
disputeEventId: key(true),
|
|
17282
|
+
rechargePaymentIntentId: key(true),
|
|
17283
|
+
rechargedAt: attr("number", true),
|
|
17284
|
+
rechargeAttempt: attr("json", true)
|
|
17285
|
+
});
|
|
16780
17286
|
var membershipAuthorityEntities = {
|
|
16781
17287
|
networkMembership: { attrs: {
|
|
16782
17288
|
id: id5(),
|
|
@@ -16816,7 +17322,8 @@ var membershipAuthorityEntities = {
|
|
|
16816
17322
|
graceEndsAt: attr("number", true),
|
|
16817
17323
|
...times(),
|
|
16818
17324
|
refundRequestedAt: attr("number", true),
|
|
16819
|
-
restartId: key(true)
|
|
17325
|
+
restartId: key(true),
|
|
17326
|
+
...dispute()
|
|
16820
17327
|
} },
|
|
16821
17328
|
membershipRestart: { attrs: {
|
|
16822
17329
|
id: id5(),
|
|
@@ -16892,7 +17399,8 @@ var membershipAuthorityEntities = {
|
|
|
16892
17399
|
refundedCents: attr("number", true),
|
|
16893
17400
|
providerRefundIds: attr("json", true),
|
|
16894
17401
|
regiftedAt: attr("number", true),
|
|
16895
|
-
formerRecipients: attr("json", true)
|
|
17402
|
+
formerRecipients: attr("json", true),
|
|
17403
|
+
...dispute()
|
|
16896
17404
|
} },
|
|
16897
17405
|
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
16898
17406
|
membershipEffect: { attrs: {
|
|
@@ -17013,7 +17521,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
17013
17521
|
}
|
|
17014
17522
|
|
|
17015
17523
|
// src/membership-seat-cancel.ts
|
|
17016
|
-
var
|
|
17524
|
+
var import_stripe19 = require("@odla-ai/stripe");
|
|
17017
17525
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
17018
17526
|
membershipClock(now);
|
|
17019
17527
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -17041,7 +17549,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
17041
17549
|
}
|
|
17042
17550
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
17043
17551
|
const request = async (method, path, params) => {
|
|
17044
|
-
const result = await (0,
|
|
17552
|
+
const result = await (0, import_stripe19.stripeRequest)(
|
|
17045
17553
|
{ secretKey },
|
|
17046
17554
|
method,
|
|
17047
17555
|
path,
|
|
@@ -17054,7 +17562,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
17054
17562
|
let payment;
|
|
17055
17563
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
17056
17564
|
else {
|
|
17057
|
-
const snapshot = await listStripeObjects((0,
|
|
17565
|
+
const snapshot = await listStripeObjects((0, import_stripe19.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
17058
17566
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
17059
17567
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
17060
17568
|
payment = matches2[0];
|
|
@@ -17229,7 +17737,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17229
17737
|
};
|
|
17230
17738
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17231
17739
|
let size = 0;
|
|
17232
|
-
let
|
|
17740
|
+
let text10 = "";
|
|
17233
17741
|
const decoder = new TextDecoder();
|
|
17234
17742
|
try {
|
|
17235
17743
|
while (true) {
|
|
@@ -17239,10 +17747,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17239
17747
|
if (done) break;
|
|
17240
17748
|
size += value.byteLength;
|
|
17241
17749
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17242
|
-
|
|
17750
|
+
text10 += decoder.decode(value, { stream: true });
|
|
17243
17751
|
}
|
|
17244
17752
|
try {
|
|
17245
|
-
return JSON.parse(
|
|
17753
|
+
return JSON.parse(text10 + decoder.decode());
|
|
17246
17754
|
} catch {
|
|
17247
17755
|
throw new TypeError(`invalid ${label}`);
|
|
17248
17756
|
}
|
|
@@ -17577,7 +18085,7 @@ async function requestMembershipRefund(options) {
|
|
|
17577
18085
|
async function requestMembershipRenewal(options) {
|
|
17578
18086
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
17579
18087
|
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)) {
|
|
18088
|
+
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
18089
|
throw new Error("invalid membership renewal receipt");
|
|
17582
18090
|
}
|
|
17583
18091
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -17929,6 +18437,7 @@ var BUILTIN_ROUTES = [
|
|
|
17929
18437
|
handlePaymentRecovery,
|
|
17930
18438
|
handleAdminNamedSeat,
|
|
17931
18439
|
handleAdminMembershipSeats,
|
|
18440
|
+
handleAdminMembershipDisputes,
|
|
17932
18441
|
handleAdminMembershipPolicyVersions,
|
|
17933
18442
|
handleAdminMembershipReterm,
|
|
17934
18443
|
handleAdminMeetings,
|
|
@@ -17977,8 +18486,9 @@ function chapterWorker(options) {
|
|
|
17977
18486
|
const ctx = createWorkerContext(options);
|
|
17978
18487
|
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
17979
18488
|
return {
|
|
17980
|
-
async fetch(req, env) {
|
|
18489
|
+
async fetch(req, env, execution) {
|
|
17981
18490
|
const url = new URL(req.url);
|
|
18491
|
+
ctx.bindExecution(req, execution);
|
|
17982
18492
|
for (const route of routes) {
|
|
17983
18493
|
let res;
|
|
17984
18494
|
try {
|