@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.js
CHANGED
|
@@ -246,12 +246,37 @@ function networkCommercialAlerts(ctx, route) {
|
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// src/worker-defer.ts
|
|
250
|
+
function deferWork(ctx, req, task) {
|
|
251
|
+
return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
|
|
252
|
+
}
|
|
253
|
+
function createDeferral() {
|
|
254
|
+
const executions = /* @__PURE__ */ new WeakMap();
|
|
255
|
+
const settle = (task) => task().then(() => void 0, (error) => {
|
|
256
|
+
console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
/** Remember the runtime's ExecutionContext for this request. */
|
|
260
|
+
bind(req, execution) {
|
|
261
|
+
if (execution) executions.set(req, execution);
|
|
262
|
+
},
|
|
263
|
+
/** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
|
|
264
|
+
defer(req, task) {
|
|
265
|
+
const execution = executions.get(req);
|
|
266
|
+
if (!execution) return settle(task);
|
|
267
|
+
execution.waitUntil(settle(task));
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
249
273
|
// src/worker-context.ts
|
|
250
274
|
var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
|
|
251
275
|
function createWorkerContext(options) {
|
|
252
276
|
const { chapter } = options;
|
|
253
277
|
const auth = chapter.auth;
|
|
254
278
|
const crmBase = options.crmBasePath ?? "/api/crm";
|
|
279
|
+
const deferral = createDeferral();
|
|
255
280
|
let publicConfigCache = null;
|
|
256
281
|
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
257
282
|
async function getPublicConfig(env) {
|
|
@@ -356,6 +381,8 @@ function createWorkerContext(options) {
|
|
|
356
381
|
membershipAuthority: options.membershipAuthority,
|
|
357
382
|
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
358
383
|
recordError: options.recordError ?? consoleErrorRecorder,
|
|
384
|
+
bindExecution: deferral.bind,
|
|
385
|
+
defer: deferral.defer,
|
|
359
386
|
getPublicConfig,
|
|
360
387
|
verifyUser,
|
|
361
388
|
makeDb,
|
|
@@ -683,6 +710,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
683
710
|
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
684
711
|
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
685
712
|
else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
|
|
713
|
+
else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
|
|
686
714
|
const customerId = refId(object2.customer, "cus_");
|
|
687
715
|
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
|
|
688
716
|
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
|
|
@@ -692,6 +720,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
692
720
|
const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
|
|
693
721
|
const currency = text(object2.currency)?.toLowerCase();
|
|
694
722
|
const status2 = text(object2.status);
|
|
723
|
+
const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
|
|
695
724
|
return {
|
|
696
725
|
eventId,
|
|
697
726
|
digest,
|
|
@@ -710,6 +739,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
710
739
|
...invoiceId ? { invoiceId } : {},
|
|
711
740
|
...chargeId ? { chargeId } : {},
|
|
712
741
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
742
|
+
...disputeReason ? { disputeReason } : {},
|
|
713
743
|
...price,
|
|
714
744
|
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
715
745
|
...status2 ? { status: status2 } : {},
|
|
@@ -752,6 +782,7 @@ function ownRef(fact) {
|
|
|
752
782
|
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
753
783
|
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
754
784
|
if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
|
|
785
|
+
if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
|
|
755
786
|
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
756
787
|
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
757
788
|
}
|
|
@@ -807,6 +838,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
807
838
|
continue;
|
|
808
839
|
}
|
|
809
840
|
const source = assignments.get(fact.eventId);
|
|
841
|
+
if (!source && fact.objectKind === "dispute") continue;
|
|
810
842
|
if (!source) {
|
|
811
843
|
const direct = directSource(fact, sources);
|
|
812
844
|
const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
@@ -823,6 +855,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
823
855
|
const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
|
|
824
856
|
const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
|
|
825
857
|
const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
|
|
858
|
+
const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
|
|
826
859
|
const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
|
|
827
860
|
for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
|
|
828
861
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
@@ -843,6 +876,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
843
876
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
844
877
|
return {
|
|
845
878
|
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
879
|
+
...disputes.length ? { disputes } : {},
|
|
846
880
|
customers,
|
|
847
881
|
subscriptions,
|
|
848
882
|
invoices,
|
|
@@ -1179,6 +1213,17 @@ function findGiftRef(obj) {
|
|
|
1179
1213
|
}
|
|
1180
1214
|
function normalizeWebhookEvent(event2, options = {}) {
|
|
1181
1215
|
const obj = event2.data?.object ?? {};
|
|
1216
|
+
if (event2.type.startsWith("charge.dispute.")) {
|
|
1217
|
+
const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
|
|
1218
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
|
|
1219
|
+
return {
|
|
1220
|
+
kind: "disputed",
|
|
1221
|
+
chargeId,
|
|
1222
|
+
disputeId: obj.id,
|
|
1223
|
+
disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
|
|
1224
|
+
...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1182
1227
|
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 };
|
|
1183
1228
|
const gift = findGiftRef(obj);
|
|
1184
1229
|
if (gift.giftIntentId) {
|
|
@@ -1588,9 +1633,9 @@ async function backfillCrm(deps) {
|
|
|
1588
1633
|
}
|
|
1589
1634
|
|
|
1590
1635
|
// src/email-retirement.ts
|
|
1591
|
-
function isRetiredEmailCopy(subject,
|
|
1636
|
+
function isRetiredEmailCopy(subject, text10) {
|
|
1592
1637
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1593
|
-
const bodyCopy =
|
|
1638
|
+
const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1594
1639
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1595
1640
|
}
|
|
1596
1641
|
|
|
@@ -1668,10 +1713,10 @@ function planDelivery(input) {
|
|
|
1668
1713
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1669
1714
|
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1670
1715
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1671
|
-
const
|
|
1716
|
+
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1672
1717
|
|
|
1673
1718
|
` + renderedText : renderedText;
|
|
1674
|
-
return { deliver: true, transport, to, subject, text:
|
|
1719
|
+
return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
|
|
1675
1720
|
}
|
|
1676
1721
|
|
|
1677
1722
|
// src/notify.ts
|
|
@@ -1819,8 +1864,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1819
1864
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1820
1865
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1821
1866
|
function applicantVariables(app) {
|
|
1822
|
-
const
|
|
1823
|
-
return { firstName:
|
|
1867
|
+
const text10 = (v) => typeof v === "string" ? v : "";
|
|
1868
|
+
return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
|
|
1824
1869
|
}
|
|
1825
1870
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1826
1871
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -2927,7 +2972,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
|
2927
2972
|
if (!intent) throw new Error("membership checkout reconciliation pending");
|
|
2928
2973
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2929
2974
|
const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
|
|
2930
|
-
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)) {
|
|
2975
|
+
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)) {
|
|
2931
2976
|
throw new Error("invalid network renewal receipt");
|
|
2932
2977
|
}
|
|
2933
2978
|
return row2;
|
|
@@ -3552,6 +3597,163 @@ function paymentLineItems(group, tier) {
|
|
|
3552
3597
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
3553
3598
|
}
|
|
3554
3599
|
|
|
3600
|
+
// src/membership-dispute-authority.ts
|
|
3601
|
+
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3602
|
+
|
|
3603
|
+
// src/membership-dispute-transition.ts
|
|
3604
|
+
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3605
|
+
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3606
|
+
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3607
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
3608
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3609
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
3610
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
3611
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3612
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
3613
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
3614
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3615
|
+
return {
|
|
3616
|
+
status: next,
|
|
3617
|
+
disputeId: fact.objectId,
|
|
3618
|
+
disputeStatus: status2,
|
|
3619
|
+
disputeEventId: eventId,
|
|
3620
|
+
disputeClosedAt: closed ? now : 0,
|
|
3621
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3622
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3623
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3624
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
3625
|
+
revision: Number(row2.revision) + 1,
|
|
3626
|
+
updatedAt: now
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// src/membership-seat-dispute.ts
|
|
3631
|
+
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3632
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
3633
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
|
|
3634
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
3635
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
3636
|
+
}
|
|
3637
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
3638
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
3639
|
+
const membership = async (applied, seat) => {
|
|
3640
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3641
|
+
requireMembershipScope(member, input);
|
|
3642
|
+
return {
|
|
3643
|
+
membershipId: id7,
|
|
3644
|
+
status: String(member.status),
|
|
3645
|
+
termEndsAt: Number(member.termEndsAt),
|
|
3646
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
3647
|
+
autoRenew: member.autoRenew === true,
|
|
3648
|
+
applied,
|
|
3649
|
+
...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
|
|
3650
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
3651
|
+
};
|
|
3652
|
+
};
|
|
3653
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3654
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
3655
|
+
if (!seat) return membership(false);
|
|
3656
|
+
requireMembershipScope(seat, input);
|
|
3657
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3658
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
3659
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
3660
|
+
if (!attrs) return membership(false, seat);
|
|
3661
|
+
try {
|
|
3662
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
3663
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
3664
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3665
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
3666
|
+
evidence.guard
|
|
3667
|
+
] });
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
3673
|
+
requireMembershipScope(saved, input);
|
|
3674
|
+
return membership(true, saved);
|
|
3675
|
+
}
|
|
3676
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/membership-dispute-authority.ts
|
|
3680
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3681
|
+
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3682
|
+
var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
|
|
3683
|
+
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3684
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3685
|
+
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
3686
|
+
const charge = await stripeRequest4(config, "GET", `/v1/charges/${chargeId}`);
|
|
3687
|
+
if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
|
|
3688
|
+
const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
|
|
3689
|
+
if (!customerId) membershipFail("membership_dispute_provider_unavailable");
|
|
3690
|
+
let subscriptionId;
|
|
3691
|
+
if (invoiceId) {
|
|
3692
|
+
const invoice = await stripeRequest4(config, "GET", `/v1/invoices/${invoiceId}`);
|
|
3693
|
+
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3694
|
+
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3695
|
+
}
|
|
3696
|
+
const seatId = text3(charge.body.metadata?.seatId);
|
|
3697
|
+
return {
|
|
3698
|
+
chargeId,
|
|
3699
|
+
customerId,
|
|
3700
|
+
...invoiceId ? { invoiceId } : {},
|
|
3701
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
3702
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
3703
|
+
...seatId ? { seatId } : {}
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
function disputedChargeCoversMembership(charge, member) {
|
|
3707
|
+
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);
|
|
3708
|
+
}
|
|
3709
|
+
async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
3710
|
+
membershipClock(now);
|
|
3711
|
+
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3712
|
+
const fact = row2?.fact;
|
|
3713
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3714
|
+
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");
|
|
3715
|
+
const id7 = await networkMembershipId(input);
|
|
3716
|
+
const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
|
|
3717
|
+
membershipId: id7,
|
|
3718
|
+
status: String(value.status),
|
|
3719
|
+
termEndsAt: Number(value.termEndsAt),
|
|
3720
|
+
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3721
|
+
autoRenew: value.autoRenew === true,
|
|
3722
|
+
...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
|
|
3723
|
+
applied
|
|
3724
|
+
});
|
|
3725
|
+
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3726
|
+
requireMembershipScope(known, input);
|
|
3727
|
+
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3728
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
3729
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
3730
|
+
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3731
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3732
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
3733
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3734
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3735
|
+
requireMembershipScope(member, input);
|
|
3736
|
+
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3737
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
3738
|
+
if (!attrs) return receipt(member, false);
|
|
3739
|
+
try {
|
|
3740
|
+
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3741
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
3742
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3743
|
+
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3744
|
+
evidenceGuard
|
|
3745
|
+
] });
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3748
|
+
continue;
|
|
3749
|
+
}
|
|
3750
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
3751
|
+
requireMembershipScope(saved, input);
|
|
3752
|
+
return receipt(saved, true);
|
|
3753
|
+
}
|
|
3754
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3555
3757
|
// src/member-system-contract.ts
|
|
3556
3758
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3557
3759
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3676,7 +3878,8 @@ async function firstRow2(db, ns, q) {
|
|
|
3676
3878
|
}
|
|
3677
3879
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3678
3880
|
const db = ctx.makeDb(env);
|
|
3679
|
-
const { rawBody, eventId
|
|
3881
|
+
const { rawBody, eventId } = input;
|
|
3882
|
+
let { event: event2 } = input;
|
|
3680
3883
|
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3681
3884
|
const digest = await stripeEventDigest(rawBody);
|
|
3682
3885
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
@@ -3732,6 +3935,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3732
3935
|
}
|
|
3733
3936
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3734
3937
|
}
|
|
3938
|
+
if (event2.kind === "disputed" && !event2.customerId) {
|
|
3939
|
+
const secret = await getVaultSecret(db, "stripe_secret_key");
|
|
3940
|
+
const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
|
|
3941
|
+
if (charge) event2 = { ...event2, customerId: charge.customerId };
|
|
3942
|
+
}
|
|
3735
3943
|
const app = await findPaymentApplication(db, event2);
|
|
3736
3944
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3737
3945
|
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
@@ -3769,11 +3977,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3769
3977
|
}
|
|
3770
3978
|
let authoritativeEvent = event2;
|
|
3771
3979
|
let membershipProjection = {};
|
|
3772
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
3980
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
|
|
3773
3981
|
try {
|
|
3774
3982
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3775
3983
|
membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
|
|
3776
|
-
authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
|
|
3984
|
+
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 };
|
|
3777
3985
|
} catch {
|
|
3778
3986
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3779
3987
|
}
|
|
@@ -4422,10 +4630,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
4422
4630
|
}
|
|
4423
4631
|
|
|
4424
4632
|
// src/membership-seat-refund-stripe.ts
|
|
4425
|
-
import { stripeRequest as
|
|
4633
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4426
4634
|
|
|
4427
4635
|
// src/membership-checkout-seat-refund.ts
|
|
4428
|
-
import { stripeRequest as
|
|
4636
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4429
4637
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4430
4638
|
throw Object.assign(new Error(code), { code });
|
|
4431
4639
|
};
|
|
@@ -4434,7 +4642,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
4434
4642
|
const invoiceRef = seat.combinedInvoice;
|
|
4435
4643
|
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();
|
|
4436
4644
|
const call = async (method, path, params, key3) => {
|
|
4437
|
-
const result = await
|
|
4645
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4438
4646
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4439
4647
|
return result.body;
|
|
4440
4648
|
};
|
|
@@ -4507,7 +4715,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
4507
4715
|
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4508
4716
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4509
4717
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4510
|
-
const result = await
|
|
4718
|
+
const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4511
4719
|
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4512
4720
|
return result.body;
|
|
4513
4721
|
};
|
|
@@ -4649,6 +4857,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4649
4857
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4650
4858
|
case "canceled":
|
|
4651
4859
|
return "canceled";
|
|
4860
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4861
|
+
case "payment_disputed":
|
|
4862
|
+
return "disputed";
|
|
4652
4863
|
default:
|
|
4653
4864
|
return "payment_pending";
|
|
4654
4865
|
}
|
|
@@ -4975,15 +5186,232 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4975
5186
|
}
|
|
4976
5187
|
};
|
|
4977
5188
|
|
|
5189
|
+
// src/membership-dispute-recharge.ts
|
|
5190
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
5191
|
+
var NS = { membership: "networkMembership", seat: "networkSeat" };
|
|
5192
|
+
var fail6 = (code, extra = {}) => {
|
|
5193
|
+
throw Object.assign(new Error(code), { code, ...extra });
|
|
5194
|
+
};
|
|
5195
|
+
var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
|
|
5196
|
+
async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
5197
|
+
const subscription = await stripeRequest7(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
5198
|
+
if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
|
|
5199
|
+
const onSubscription = refOf2(subscription.body.default_payment_method);
|
|
5200
|
+
if (onSubscription) return onSubscription;
|
|
5201
|
+
const customer = await stripeRequest7(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
|
|
5202
|
+
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5203
|
+
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5204
|
+
}
|
|
5205
|
+
async function rechargeDisputed(db, kind, id7, secretKey, now) {
|
|
5206
|
+
membershipClock(now);
|
|
5207
|
+
const ns = NS[kind];
|
|
5208
|
+
let row2 = await membershipLoad(db, ns, id7);
|
|
5209
|
+
if (!row2) fail6("membership_not_found");
|
|
5210
|
+
const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
|
|
5211
|
+
const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
|
|
5212
|
+
const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
|
|
5213
|
+
const done = (value, replayed) => ({
|
|
5214
|
+
membershipId,
|
|
5215
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5216
|
+
status: String(value.status),
|
|
5217
|
+
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5218
|
+
amountCents,
|
|
5219
|
+
currency,
|
|
5220
|
+
...replayed ? { replayed } : {}
|
|
5221
|
+
});
|
|
5222
|
+
if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5223
|
+
if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
|
|
5224
|
+
fail6("membership_not_disputed");
|
|
5225
|
+
}
|
|
5226
|
+
if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5227
|
+
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5228
|
+
let attempt = row2.rechargeAttempt;
|
|
5229
|
+
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5230
|
+
attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
|
|
5231
|
+
await db.transact(
|
|
5232
|
+
[membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
|
|
5233
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
|
|
5234
|
+
);
|
|
5235
|
+
row2 = await membershipLoad(db, ns, id7);
|
|
5236
|
+
if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5237
|
+
}
|
|
5238
|
+
const config = { secretKey };
|
|
5239
|
+
const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
|
|
5240
|
+
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5241
|
+
const intent = await stripeRequest7(config, "POST", "/v1/payment_intents", {
|
|
5242
|
+
amount: amountCents,
|
|
5243
|
+
currency,
|
|
5244
|
+
customer: String(row2.providerCustomerId),
|
|
5245
|
+
payment_method: paymentMethod,
|
|
5246
|
+
off_session: "true",
|
|
5247
|
+
confirm: "true",
|
|
5248
|
+
description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
|
|
5249
|
+
metadata: {
|
|
5250
|
+
...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
|
|
5251
|
+
applicationId: String(row2.applicationId),
|
|
5252
|
+
membershipId,
|
|
5253
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5254
|
+
disputeId: String(row2.disputeId),
|
|
5255
|
+
membershipRecharge: "true"
|
|
5256
|
+
}
|
|
5257
|
+
}, { idempotencyKey: attempt.key });
|
|
5258
|
+
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5259
|
+
const finish = async (outcome) => {
|
|
5260
|
+
const current = await membershipLoad(db, ns, id7);
|
|
5261
|
+
if (!current) return;
|
|
5262
|
+
await db.transact([membershipUpdate(ns, id7, {
|
|
5263
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5264
|
+
revision: Number(current.revision) + 1,
|
|
5265
|
+
updatedAt: now
|
|
5266
|
+
})], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
|
|
5267
|
+
};
|
|
5268
|
+
if (!intent.ok) {
|
|
5269
|
+
if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
|
|
5270
|
+
await finish("declined");
|
|
5271
|
+
fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
|
|
5272
|
+
}
|
|
5273
|
+
fail6("membership_dispute_provider_unavailable");
|
|
5274
|
+
}
|
|
5275
|
+
if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
|
|
5276
|
+
await finish("declined");
|
|
5277
|
+
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5278
|
+
}
|
|
5279
|
+
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5280
|
+
if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5281
|
+
for (let retry = 0; retry < 8; retry++) {
|
|
5282
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
|
|
5283
|
+
if (!current) fail6("membership_not_found");
|
|
5284
|
+
if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
|
|
5285
|
+
const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5286
|
+
try {
|
|
5287
|
+
await db.transact(
|
|
5288
|
+
[membershipUpdate(ns, id7, {
|
|
5289
|
+
status: status2,
|
|
5290
|
+
disputeStatus: "recharged",
|
|
5291
|
+
rechargePaymentIntentId: String(body.id),
|
|
5292
|
+
rechargedAt: now,
|
|
5293
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
|
|
5294
|
+
revision: Number(current.revision) + 1,
|
|
5295
|
+
updatedAt: now
|
|
5296
|
+
})],
|
|
5297
|
+
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5298
|
+
{ ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5299
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5300
|
+
] }
|
|
5301
|
+
);
|
|
5302
|
+
} catch (cause) {
|
|
5303
|
+
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
5306
|
+
return done(await membershipLoad(db, ns, id7));
|
|
5307
|
+
}
|
|
5308
|
+
return fail6("membership_dispute_retry_required");
|
|
5309
|
+
}
|
|
5310
|
+
var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
|
|
5311
|
+
var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
|
|
5312
|
+
|
|
5313
|
+
// src/worker-routes-admin-membership-disputes.ts
|
|
5314
|
+
var LIST2 = "/api/admin/membership-disputes";
|
|
5315
|
+
var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
|
|
5316
|
+
var PUBLIC2 = [
|
|
5317
|
+
"id",
|
|
5318
|
+
"appId",
|
|
5319
|
+
"chapterId",
|
|
5320
|
+
"environment",
|
|
5321
|
+
"runtime",
|
|
5322
|
+
"applicationId",
|
|
5323
|
+
"skuId",
|
|
5324
|
+
"status",
|
|
5325
|
+
"memberNumber",
|
|
5326
|
+
"amountCents",
|
|
5327
|
+
"currency",
|
|
5328
|
+
"recipientName",
|
|
5329
|
+
"primaryMembershipId",
|
|
5330
|
+
"purchasedAt",
|
|
5331
|
+
"disputeId",
|
|
5332
|
+
"disputeStatus",
|
|
5333
|
+
"disputeReason",
|
|
5334
|
+
"disputeAmountCents",
|
|
5335
|
+
"disputedAt",
|
|
5336
|
+
"disputeClosedAt",
|
|
5337
|
+
"disputePriorStatus",
|
|
5338
|
+
"rechargePaymentIntentId",
|
|
5339
|
+
"rechargedAt",
|
|
5340
|
+
"termEndsAt",
|
|
5341
|
+
"updatedAt"
|
|
5342
|
+
];
|
|
5343
|
+
var project2 = (row2) => ({
|
|
5344
|
+
...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
|
|
5345
|
+
...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
|
|
5346
|
+
startedAt: row2.rechargeAttempt.startedAt ?? null,
|
|
5347
|
+
finishedAt: row2.rechargeAttempt.finishedAt ?? null,
|
|
5348
|
+
outcome: row2.rechargeAttempt.outcome ?? null
|
|
5349
|
+
} } : {}
|
|
5350
|
+
});
|
|
5351
|
+
var FINAL = /* @__PURE__ */ new Set([
|
|
5352
|
+
"membership_not_found",
|
|
5353
|
+
"membership_not_disputed",
|
|
5354
|
+
"membership_dispute_open",
|
|
5355
|
+
"membership_dispute_amount_invalid",
|
|
5356
|
+
"recharge_payment_method_unavailable",
|
|
5357
|
+
"recharge_declined",
|
|
5358
|
+
"recharge_pending",
|
|
5359
|
+
"recharge_mismatch",
|
|
5360
|
+
"membership_dispute_changed"
|
|
5361
|
+
]);
|
|
5362
|
+
var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
5363
|
+
const one = url.pathname.match(ONE2);
|
|
5364
|
+
if (url.pathname !== LIST2 && !one) return null;
|
|
5365
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5366
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5367
|
+
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
5368
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5369
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5370
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5371
|
+
let db;
|
|
5372
|
+
try {
|
|
5373
|
+
db = operator.makeDb(env);
|
|
5374
|
+
} catch {
|
|
5375
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5376
|
+
}
|
|
5377
|
+
if (!one) {
|
|
5378
|
+
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5379
|
+
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5380
|
+
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5381
|
+
const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5382
|
+
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5383
|
+
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5384
|
+
}
|
|
5385
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5386
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5387
|
+
try {
|
|
5388
|
+
const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
|
|
5389
|
+
return json({ ok: true, ...receipt });
|
|
5390
|
+
} catch (error) {
|
|
5391
|
+
const failure = error;
|
|
5392
|
+
const code = typeof failure.code === "string" ? failure.code : void 0;
|
|
5393
|
+
if (code === "membership_not_found") return json({ error: "not found", code }, 404);
|
|
5394
|
+
if (code && FINAL.has(code)) {
|
|
5395
|
+
return json({
|
|
5396
|
+
error: "the charge could not be re-issued",
|
|
5397
|
+
code,
|
|
5398
|
+
...failure.declineCode ? { declineCode: failure.declineCode } : {},
|
|
5399
|
+
...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
|
|
5400
|
+
}, 409);
|
|
5401
|
+
}
|
|
5402
|
+
return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
|
|
4978
5406
|
// src/membership-policy-versions.ts
|
|
4979
5407
|
var LIMIT = 200;
|
|
4980
|
-
var
|
|
5408
|
+
var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4981
5409
|
var instant = (value) => {
|
|
4982
5410
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4983
5411
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
4984
5412
|
};
|
|
4985
5413
|
function provenance(row2) {
|
|
4986
|
-
const source =
|
|
5414
|
+
const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
4987
5415
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
4988
5416
|
}
|
|
4989
5417
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -5077,16 +5505,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
5077
5505
|
};
|
|
5078
5506
|
|
|
5079
5507
|
// src/membership-discount-reterm.ts
|
|
5080
|
-
import { stripeRequest as
|
|
5508
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
5081
5509
|
|
|
5082
5510
|
// src/subscription-provider-contract.ts
|
|
5083
|
-
import { stripeRequest as
|
|
5511
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
5084
5512
|
function subscriptionContractMismatch() {
|
|
5085
5513
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
5086
5514
|
}
|
|
5087
5515
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
5088
5516
|
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();
|
|
5089
|
-
const result = await
|
|
5517
|
+
const result = await stripeRequest8({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
5090
5518
|
const price = result.body;
|
|
5091
5519
|
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();
|
|
5092
5520
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -5124,7 +5552,7 @@ async function retermPolicy(db, scope2, id7) {
|
|
|
5124
5552
|
}
|
|
5125
5553
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
5126
5554
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
5127
|
-
const read2 = await
|
|
5555
|
+
const read2 = await stripeRequest9({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
5128
5556
|
const sub = read2.body, items = sub?.items?.data;
|
|
5129
5557
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
5130
5558
|
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) {
|
|
@@ -5135,7 +5563,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
5135
5563
|
const present = discounts.map(couponOf);
|
|
5136
5564
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
5137
5565
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
5138
|
-
const written = await
|
|
5566
|
+
const written = await stripeRequest9({ secretKey: sk }, "POST", path, {
|
|
5139
5567
|
"discounts[0][coupon]": couponId,
|
|
5140
5568
|
"expand[0]": "discounts",
|
|
5141
5569
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -5984,8 +6412,8 @@ var APPLICATION_LABELS = {
|
|
|
5984
6412
|
state: "State"
|
|
5985
6413
|
};
|
|
5986
6414
|
function humanize(id7) {
|
|
5987
|
-
const
|
|
5988
|
-
return
|
|
6415
|
+
const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6416
|
+
return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
|
|
5989
6417
|
}
|
|
5990
6418
|
function applicationDefs(chapter) {
|
|
5991
6419
|
try {
|
|
@@ -6052,7 +6480,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6052
6480
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6053
6481
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6054
6482
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6055
|
-
var
|
|
6483
|
+
var text5 = (value, path, errors, max) => {
|
|
6056
6484
|
if (typeof value !== "string" || !value.trim()) {
|
|
6057
6485
|
errors.push(`${path} is required`);
|
|
6058
6486
|
return "";
|
|
@@ -6080,23 +6508,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6080
6508
|
}
|
|
6081
6509
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6082
6510
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6083
|
-
const id7 =
|
|
6511
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6084
6512
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6085
6513
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6086
6514
|
ids2.add(id7);
|
|
6087
6515
|
const targetRaw = record(item.target);
|
|
6088
6516
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6089
6517
|
if (targetRaw?.type === "tier") {
|
|
6090
|
-
const tierId =
|
|
6518
|
+
const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6091
6519
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6092
6520
|
target = { type: "tier", tierId };
|
|
6093
6521
|
} else if (targetRaw?.type === "entitlement") {
|
|
6094
|
-
const entitlementKey =
|
|
6522
|
+
const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6095
6523
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6096
6524
|
target = { type: "entitlement", entitlementKey };
|
|
6097
6525
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6098
6526
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6099
|
-
const stripePriceId =
|
|
6527
|
+
const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6100
6528
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6101
6529
|
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`);
|
|
6102
6530
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6118,9 +6546,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6118
6546
|
redemptionLifetimeHours: lifetime,
|
|
6119
6547
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6120
6548
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6121
|
-
title:
|
|
6122
|
-
description:
|
|
6123
|
-
ctaLabel:
|
|
6549
|
+
title: text5(item.title, `${path}.title`, errors, 160),
|
|
6550
|
+
description: text5(item.description, `${path}.description`, errors, 2e3),
|
|
6551
|
+
ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6124
6552
|
});
|
|
6125
6553
|
}
|
|
6126
6554
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6347,23 +6775,23 @@ async function createPaymentQuote(input) {
|
|
|
6347
6775
|
// src/membership-quote-projection.ts
|
|
6348
6776
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
6349
6777
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
6350
|
-
var
|
|
6778
|
+
var fail7 = (code) => {
|
|
6351
6779
|
throw Object.assign(new Error(code), { code });
|
|
6352
6780
|
};
|
|
6353
6781
|
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
6354
6782
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
6355
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
6356
|
-
if (row2.status !== "ready") return
|
|
6783
|
+
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");
|
|
6784
|
+
if (row2.status !== "ready") return fail7("membership_quote_pending");
|
|
6357
6785
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
6358
6786
|
...target,
|
|
6359
6787
|
requestId: row2.requestId
|
|
6360
6788
|
}), now, lifetimeMs: 6e4 });
|
|
6361
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6789
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
|
|
6362
6790
|
const tier = row2.tier, quote = row2.quote;
|
|
6363
6791
|
const offering = offer.contract.offering;
|
|
6364
|
-
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return
|
|
6792
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
|
|
6365
6793
|
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) {
|
|
6366
|
-
return
|
|
6794
|
+
return fail7("membership_catalog_mismatch");
|
|
6367
6795
|
}
|
|
6368
6796
|
const expected = await createPaymentQuote({
|
|
6369
6797
|
...target,
|
|
@@ -6376,7 +6804,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
6376
6804
|
seatOffer: quote.seatOffer,
|
|
6377
6805
|
namedSeat: quote.namedSeat
|
|
6378
6806
|
});
|
|
6379
|
-
if (quote.digest !== expected.digest) return
|
|
6807
|
+
if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
|
|
6380
6808
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
6381
6809
|
}
|
|
6382
6810
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -6384,9 +6812,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
6384
6812
|
}
|
|
6385
6813
|
async function prepareMembershipQuote(db, input, issue) {
|
|
6386
6814
|
const { applicationId: id7, now, app } = input;
|
|
6387
|
-
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
|
|
6815
|
+
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");
|
|
6388
6816
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6389
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6817
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
|
|
6390
6818
|
if (row2?.status === "ready") {
|
|
6391
6819
|
try {
|
|
6392
6820
|
return await restore2(row2, input, now);
|
|
@@ -6431,7 +6859,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6431
6859
|
}
|
|
6432
6860
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6433
6861
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6434
|
-
return
|
|
6862
|
+
return fail7("membership_quote_pending");
|
|
6435
6863
|
}
|
|
6436
6864
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6437
6865
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6480,7 +6908,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6480
6908
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6481
6909
|
}
|
|
6482
6910
|
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6483
|
-
if (!input.enabled) return
|
|
6911
|
+
if (!input.enabled) return fail7("named_seat_purchase_unavailable");
|
|
6484
6912
|
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6485
6913
|
const current = await restore2(row2, target, input.now);
|
|
6486
6914
|
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
@@ -6536,17 +6964,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6536
6964
|
if (!reader) return "";
|
|
6537
6965
|
const decoder = new TextDecoder();
|
|
6538
6966
|
let bytes = 0;
|
|
6539
|
-
let
|
|
6967
|
+
let text10 = "";
|
|
6540
6968
|
for (; ; ) {
|
|
6541
6969
|
const { done, value } = await reader.read();
|
|
6542
6970
|
if (req.signal.aborted) return invalid2();
|
|
6543
6971
|
if (done) {
|
|
6544
6972
|
complete = true;
|
|
6545
|
-
return
|
|
6973
|
+
return text10 + decoder.decode();
|
|
6546
6974
|
}
|
|
6547
6975
|
bytes += value.byteLength;
|
|
6548
6976
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6549
|
-
|
|
6977
|
+
text10 += decoder.decode(value, { stream: true });
|
|
6550
6978
|
}
|
|
6551
6979
|
} catch {
|
|
6552
6980
|
return invalid2();
|
|
@@ -6650,9 +7078,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
6650
7078
|
let selection;
|
|
6651
7079
|
if (request?.body) {
|
|
6652
7080
|
try {
|
|
6653
|
-
const
|
|
6654
|
-
if (
|
|
6655
|
-
const body = JSON.parse(
|
|
7081
|
+
const text10 = await readBoundedRequestText(request, 2048);
|
|
7082
|
+
if (text10 instanceof Response) return text10;
|
|
7083
|
+
const body = JSON.parse(text10);
|
|
6656
7084
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6657
7085
|
selection = body.namedSeat;
|
|
6658
7086
|
} catch {
|
|
@@ -7307,10 +7735,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7307
7735
|
const db = ctx.makeDb(env);
|
|
7308
7736
|
const groupId = url.searchParams.get("group") ?? chapter.id;
|
|
7309
7737
|
if (groupId !== chapter.id) return json({ error: "not found" }, 404);
|
|
7310
|
-
const { groups } = await
|
|
7738
|
+
const [{ groups }, stripeKey, activeControl] = await Promise.all([
|
|
7739
|
+
db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
|
|
7740
|
+
getVaultSecret(db, "stripe_secret_key"),
|
|
7741
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
|
|
7742
|
+
]);
|
|
7311
7743
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7312
7744
|
if (!group) return json({ error: "not found" }, 404);
|
|
7313
|
-
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7314
7745
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
7315
7746
|
const offered = await loadOfferableTiers(
|
|
7316
7747
|
db,
|
|
@@ -7319,7 +7750,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7319
7750
|
);
|
|
7320
7751
|
const seats = /* @__PURE__ */ new Map();
|
|
7321
7752
|
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7322
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7323
7753
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7324
7754
|
return json({
|
|
7325
7755
|
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
@@ -7357,11 +7787,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7357
7787
|
parsed.tierId = named.invitation.skuId;
|
|
7358
7788
|
submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
|
|
7359
7789
|
}
|
|
7360
|
-
const eligibility
|
|
7790
|
+
const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
|
|
7791
|
+
named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
|
|
7792
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
|
|
7793
|
+
typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
|
|
7794
|
+
db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
|
|
7795
|
+
]);
|
|
7361
7796
|
if (eligibility?.conflict) return eligibility.conflict;
|
|
7362
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7363
7797
|
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
7364
|
-
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
7365
7798
|
const now = Date.now();
|
|
7366
7799
|
const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
|
|
7367
7800
|
const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
|
|
@@ -7400,18 +7833,16 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7400
7833
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
7401
7834
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
7402
7835
|
}
|
|
7403
|
-
if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
|
|
7404
|
-
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
7405
|
-
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7836
|
+
if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
|
|
7406
7837
|
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
7407
|
-
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
7838
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
|
|
7408
7839
|
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
7409
7840
|
if (!state) return json({
|
|
7410
7841
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
7411
7842
|
code: "application_continuation_unavailable",
|
|
7412
7843
|
id: result.id
|
|
7413
7844
|
}, 503);
|
|
7414
|
-
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
7845
|
+
if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
|
|
7415
7846
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
7416
7847
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
7417
7848
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -7531,10 +7962,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7531
7962
|
errors.push(`${path} must be a string`);
|
|
7532
7963
|
return void 0;
|
|
7533
7964
|
}
|
|
7534
|
-
const
|
|
7535
|
-
if (required && !
|
|
7536
|
-
if (
|
|
7537
|
-
return
|
|
7965
|
+
const text10 = value.trim();
|
|
7966
|
+
if (required && !text10) errors.push(`${path} is required`);
|
|
7967
|
+
if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
7968
|
+
return text10;
|
|
7538
7969
|
}
|
|
7539
7970
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7540
7971
|
const errors = [];
|
|
@@ -7583,8 +8014,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
7583
8014
|
else {
|
|
7584
8015
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
7585
8016
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
7586
|
-
const
|
|
7587
|
-
return
|
|
8017
|
+
const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8018
|
+
return text10 ? [text10] : [];
|
|
7588
8019
|
});
|
|
7589
8020
|
}
|
|
7590
8021
|
}
|
|
@@ -7678,7 +8109,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
7678
8109
|
function unknownKeys(value, allowed2, path, errors) {
|
|
7679
8110
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7680
8111
|
}
|
|
7681
|
-
function
|
|
8112
|
+
function text6(value, path, errors, max = 65536) {
|
|
7682
8113
|
if (typeof value !== "string" || !value.trim()) {
|
|
7683
8114
|
errors.push(`${path} is required`);
|
|
7684
8115
|
return "";
|
|
@@ -7701,11 +8132,11 @@ function tiersOf(value, errors) {
|
|
|
7701
8132
|
return [];
|
|
7702
8133
|
}
|
|
7703
8134
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7704
|
-
const id7 =
|
|
8135
|
+
const id7 = text6(item.id, `${path}.id`, errors, 80);
|
|
7705
8136
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7706
8137
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7707
8138
|
seen.add(id7);
|
|
7708
|
-
const name =
|
|
8139
|
+
const name = text6(item.name, `${path}.name`, errors, 160);
|
|
7709
8140
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7710
8141
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
7711
8142
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -7723,7 +8154,7 @@ function tiersOf(value, errors) {
|
|
|
7723
8154
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
7724
8155
|
errors.push(`${path}.interval must be year or month`);
|
|
7725
8156
|
}
|
|
7726
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8157
|
+
const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7727
8158
|
return [{
|
|
7728
8159
|
id: id7,
|
|
7729
8160
|
name,
|
|
@@ -7781,7 +8212,7 @@ function policyOf(value, errors, tiers) {
|
|
|
7781
8212
|
const result = {};
|
|
7782
8213
|
for (const key3 of allowed2) {
|
|
7783
8214
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
7784
|
-
result[key3] =
|
|
8215
|
+
result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
|
|
7785
8216
|
}
|
|
7786
8217
|
const tierPolicies = {};
|
|
7787
8218
|
if (input.tierPolicies !== void 0) {
|
|
@@ -7796,8 +8227,8 @@ function policyOf(value, errors, tiers) {
|
|
|
7796
8227
|
}
|
|
7797
8228
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7798
8229
|
tierPolicies[id7] = {
|
|
7799
|
-
refundPolicyText:
|
|
7800
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8230
|
+
refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8231
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7801
8232
|
};
|
|
7802
8233
|
}
|
|
7803
8234
|
}
|
|
@@ -7837,9 +8268,9 @@ function validateSignupControlDocument(input) {
|
|
|
7837
8268
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
7838
8269
|
const source = recordOf(root.source);
|
|
7839
8270
|
const target = recordOf(root.target);
|
|
7840
|
-
const appId =
|
|
7841
|
-
const chapterId =
|
|
7842
|
-
const environment =
|
|
8271
|
+
const appId = text6(source?.appId, "document.source.appId", errors, 63);
|
|
8272
|
+
const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8273
|
+
const environment = text6(target?.environment, "document.target.environment", errors, 63);
|
|
7843
8274
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7844
8275
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
7845
8276
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9351,8 +9782,8 @@ async function bookSlot(req, env, ctx) {
|
|
|
9351
9782
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
9352
9783
|
await db.transact([meetingOp, appOp]);
|
|
9353
9784
|
const booked = { ...app, ...bookingAttrs };
|
|
9354
|
-
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
9355
|
-
await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
|
|
9785
|
+
await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
|
|
9786
|
+
await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
|
|
9356
9787
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
9357
9788
|
}
|
|
9358
9789
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -9376,28 +9807,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
9376
9807
|
};
|
|
9377
9808
|
|
|
9378
9809
|
// src/payments-stripe.ts
|
|
9379
|
-
import { stripeRequest as
|
|
9810
|
+
import { stripeRequest as stripeRequest12, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
9380
9811
|
|
|
9381
9812
|
// src/payments-refund.ts
|
|
9382
|
-
import { stripeRequest as
|
|
9813
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
9383
9814
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9384
9815
|
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
9385
9816
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
9386
|
-
function
|
|
9817
|
+
function fail8(code = "refund_target_mismatch") {
|
|
9387
9818
|
throw Object.assign(new Error(code), { code });
|
|
9388
9819
|
}
|
|
9389
9820
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
9390
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9821
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
|
|
9391
9822
|
const call = async (method, path, params, key3) => {
|
|
9392
|
-
const result = await
|
|
9393
|
-
if (!result.ok)
|
|
9394
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
9823
|
+
const result = await stripeRequest10({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9824
|
+
if (!result.ok) fail8("stripe_error");
|
|
9825
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
|
|
9395
9826
|
return result.body;
|
|
9396
9827
|
};
|
|
9397
9828
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9398
|
-
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId)
|
|
9829
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
|
|
9399
9830
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
9400
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9831
|
+
if (row(sub.metadata)[key3] !== value) fail8();
|
|
9401
9832
|
}
|
|
9402
9833
|
let cursor;
|
|
9403
9834
|
let earliest;
|
|
@@ -9410,66 +9841,66 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
9410
9841
|
limit: 100,
|
|
9411
9842
|
...cursor ? { starting_after: cursor } : {}
|
|
9412
9843
|
});
|
|
9413
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9844
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
|
|
9414
9845
|
for (const value of list.data) {
|
|
9415
9846
|
const invoice2 = row(value);
|
|
9416
9847
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
9417
|
-
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)
|
|
9848
|
+
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();
|
|
9418
9849
|
seen.add(id3(invoice2));
|
|
9419
9850
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
9420
9851
|
}
|
|
9421
9852
|
if (!list.has_more) break;
|
|
9422
|
-
if (!list.data.length || page === 9)
|
|
9853
|
+
if (!list.data.length || page === 9) fail8("refund_history_incomplete");
|
|
9423
9854
|
cursor = id3(list.data[list.data.length - 1]);
|
|
9424
9855
|
}
|
|
9425
|
-
if (!earliest)
|
|
9856
|
+
if (!earliest) fail8("no_charge");
|
|
9426
9857
|
const invoice = earliest;
|
|
9427
|
-
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
9858
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
|
|
9428
9859
|
let chargeId = id3(invoice.charge);
|
|
9429
9860
|
let paymentIntentId = id3(invoice.payment_intent);
|
|
9430
9861
|
if (!chargeId && !paymentIntentId) {
|
|
9431
9862
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9432
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
9863
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
|
|
9433
9864
|
const payment = row(payments.data[0]);
|
|
9434
|
-
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
9865
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
|
|
9435
9866
|
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
9436
9867
|
}
|
|
9437
9868
|
if (!chargeId) {
|
|
9438
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9869
|
+
if (!providerId2(paymentIntentId, "pi")) fail8();
|
|
9439
9870
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
9440
|
-
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
9871
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
|
|
9441
9872
|
chargeId = id3(payment.latest_charge);
|
|
9442
9873
|
}
|
|
9443
|
-
if (!providerId2(chargeId, "ch"))
|
|
9874
|
+
if (!providerId2(chargeId, "ch")) fail8();
|
|
9444
9875
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
9445
|
-
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))
|
|
9446
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
9876
|
+
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();
|
|
9877
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
|
|
9447
9878
|
if (charge.amount_refunded !== charge.amount) {
|
|
9448
9879
|
const refund = await call("POST", "/v1/refunds", {
|
|
9449
9880
|
charge: chargeId,
|
|
9450
9881
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
9451
9882
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
9452
|
-
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))
|
|
9883
|
+
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");
|
|
9453
9884
|
}
|
|
9454
9885
|
let subscriptionCanceled = sub.status === "canceled";
|
|
9455
9886
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
9456
9887
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9457
|
-
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
9888
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
|
|
9458
9889
|
subscriptionCanceled = true;
|
|
9459
9890
|
}
|
|
9460
9891
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
9461
|
-
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)
|
|
9892
|
+
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");
|
|
9462
9893
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
9463
9894
|
}
|
|
9464
9895
|
|
|
9465
9896
|
// src/subscription-payment-identity.ts
|
|
9466
|
-
import { stripeRequest as
|
|
9897
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
9467
9898
|
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
9468
9899
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
9469
9900
|
let paymentId = id4(invoice.payment_intent);
|
|
9470
9901
|
if (!paymentId) {
|
|
9471
9902
|
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9472
|
-
const result2 = await
|
|
9903
|
+
const result2 = await stripeRequest11({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
9473
9904
|
const list = result2.body;
|
|
9474
9905
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
9475
9906
|
const link = list.data[0];
|
|
@@ -9478,11 +9909,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9478
9909
|
}
|
|
9479
9910
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
9480
9911
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
9481
|
-
const result = await
|
|
9912
|
+
const result = await stripeRequest11({ secretKey }, "GET", path);
|
|
9482
9913
|
const payment = result.body;
|
|
9483
9914
|
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();
|
|
9484
9915
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
9485
|
-
const tagged = await
|
|
9916
|
+
const tagged = await stripeRequest11(
|
|
9486
9917
|
{ secretKey },
|
|
9487
9918
|
"POST",
|
|
9488
9919
|
path,
|
|
@@ -9494,7 +9925,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9494
9925
|
|
|
9495
9926
|
// src/payments-stripe.ts
|
|
9496
9927
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9497
|
-
return
|
|
9928
|
+
return stripeRequest12(
|
|
9498
9929
|
{ secretKey: sk },
|
|
9499
9930
|
method,
|
|
9500
9931
|
path,
|
|
@@ -9502,7 +9933,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9502
9933
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9503
9934
|
);
|
|
9504
9935
|
}
|
|
9505
|
-
function
|
|
9936
|
+
function fail9(op, r) {
|
|
9506
9937
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9507
9938
|
err.code = "stripe_error";
|
|
9508
9939
|
throw err;
|
|
@@ -9561,7 +9992,7 @@ function createStripeProvider(config) {
|
|
|
9561
9992
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9562
9993
|
`cus:${input.applicationId}`
|
|
9563
9994
|
);
|
|
9564
|
-
if (!cust.ok)
|
|
9995
|
+
if (!cust.ok) fail9("customer create", cust);
|
|
9565
9996
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9566
9997
|
customerId = cust.body.id;
|
|
9567
9998
|
}
|
|
@@ -9606,7 +10037,7 @@ function createStripeProvider(config) {
|
|
|
9606
10037
|
// retry between create and the db write can't orphan a second one.
|
|
9607
10038
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9608
10039
|
);
|
|
9609
|
-
if (!sub.ok)
|
|
10040
|
+
if (!sub.ok) fail9("subscription create", sub);
|
|
9610
10041
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9611
10042
|
const subscriptionId = sub.body.id;
|
|
9612
10043
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9616,14 +10047,14 @@ function createStripeProvider(config) {
|
|
|
9616
10047
|
"expand[2]": "discounts",
|
|
9617
10048
|
"expand[3]": "latest_invoice.discounts"
|
|
9618
10049
|
});
|
|
9619
|
-
if (!expanded.ok)
|
|
10050
|
+
if (!expanded.ok) fail9("subscription invoice evidence", expanded);
|
|
9620
10051
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9621
10052
|
sub = expanded;
|
|
9622
10053
|
}
|
|
9623
10054
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9624
10055
|
const invoice = sub.body.latest_invoice;
|
|
9625
10056
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9626
|
-
if (!clientSecret2)
|
|
10057
|
+
if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
|
|
9627
10058
|
await tagSubscriptionPayment(
|
|
9628
10059
|
sk,
|
|
9629
10060
|
invoice,
|
|
@@ -10110,7 +10541,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10110
10541
|
|
|
10111
10542
|
// src/worker-payment-receipt.ts
|
|
10112
10543
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10113
|
-
var escape = (
|
|
10544
|
+
var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10114
10545
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10115
10546
|
const id7 = url.searchParams.get("application");
|
|
10116
10547
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10121,7 +10552,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10121
10552
|
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);
|
|
10122
10553
|
const receipt = intent.paymentReceipt;
|
|
10123
10554
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10124
|
-
const
|
|
10555
|
+
const text10 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10125
10556
|
if (req.method === "POST") {
|
|
10126
10557
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10127
10558
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10139,7 +10570,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10139
10570
|
template: "paymentReceipt",
|
|
10140
10571
|
to: String(intent.purchaserEmailNormalized),
|
|
10141
10572
|
applicationId: id7,
|
|
10142
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10573
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text10 },
|
|
10143
10574
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10144
10575
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10145
10576
|
});
|
|
@@ -10149,7 +10580,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10149
10580
|
const nonce = crypto.randomUUID();
|
|
10150
10581
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10151
10582
|
<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>
|
|
10152
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
10583
|
+
<h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
|
|
10153
10584
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10154
10585
|
headers: {
|
|
10155
10586
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10619,16 +11050,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
10619
11050
|
};
|
|
10620
11051
|
|
|
10621
11052
|
// src/named-seat-accepted-notice.ts
|
|
10622
|
-
var
|
|
11053
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
10623
11054
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
10624
11055
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
10625
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
11056
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
|
|
10626
11057
|
return { sent: false, reason: "purchaser-missing" };
|
|
10627
11058
|
}
|
|
10628
11059
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
10629
11060
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
10630
11061
|
const config = emailGroupFrom(group);
|
|
10631
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
11062
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
|
|
10632
11063
|
return sendTemplated({
|
|
10633
11064
|
db,
|
|
10634
11065
|
envName: env.ODLA_ENV,
|
|
@@ -10640,12 +11071,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
10640
11071
|
}, {
|
|
10641
11072
|
group: config,
|
|
10642
11073
|
template: "seatAccepted",
|
|
10643
|
-
to:
|
|
11074
|
+
to: text7(purchaser.email),
|
|
10644
11075
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
10645
11076
|
applicationId: input.primaryApplicationId,
|
|
10646
11077
|
tierId: input.skuId,
|
|
10647
11078
|
vars: {
|
|
10648
|
-
firstName:
|
|
11079
|
+
firstName: text7(purchaser.firstName),
|
|
10649
11080
|
purchaserName,
|
|
10650
11081
|
recipientName: input.recipientName,
|
|
10651
11082
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -12112,12 +12543,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12112
12543
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12113
12544
|
const t = value;
|
|
12114
12545
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12115
|
-
const subject = str2(t.subject).trim(),
|
|
12116
|
-
const issues = validateEmailCopy4({ subject, text:
|
|
12546
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12547
|
+
const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12117
12548
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12118
12549
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12119
12550
|
}
|
|
12120
|
-
clean[key3] = { subject, text:
|
|
12551
|
+
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12121
12552
|
}
|
|
12122
12553
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12123
12554
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -14330,9 +14761,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14330
14761
|
if (req.method === "POST") {
|
|
14331
14762
|
let body;
|
|
14332
14763
|
try {
|
|
14333
|
-
const
|
|
14334
|
-
if (new TextEncoder().encode(
|
|
14335
|
-
const parsed = JSON.parse(
|
|
14764
|
+
const text10 = await req.text();
|
|
14765
|
+
if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14766
|
+
const parsed = JSON.parse(text10);
|
|
14336
14767
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14337
14768
|
body = parsed;
|
|
14338
14769
|
} catch {
|
|
@@ -14501,7 +14932,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
|
|
|
14501
14932
|
// src/network-commercial-parity.ts
|
|
14502
14933
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14503
14934
|
var MAX_RECORDS = 500;
|
|
14504
|
-
var
|
|
14935
|
+
var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14505
14936
|
function parityMessage(partition, kind, value) {
|
|
14506
14937
|
return [
|
|
14507
14938
|
"chapter-commercial-parity",
|
|
@@ -14533,18 +14964,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14533
14964
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14534
14965
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14535
14966
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14536
|
-
const eligible = input.rows.filter((row2) =>
|
|
14967
|
+
const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
|
|
14537
14968
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14538
14969
|
const key3 = await parityKey(input.secret);
|
|
14539
14970
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14540
|
-
const applicationId =
|
|
14541
|
-
const customerId =
|
|
14542
|
-
const subscriptionId =
|
|
14971
|
+
const applicationId = text8(row2.id);
|
|
14972
|
+
const customerId = text8(row2.stripeCustomerId);
|
|
14973
|
+
const subscriptionId = text8(row2.stripeSubscriptionId);
|
|
14543
14974
|
return {
|
|
14544
14975
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14545
14976
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14546
14977
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14547
|
-
status:
|
|
14978
|
+
status: text8(row2.status, 64),
|
|
14548
14979
|
canceled: row2.canceled === true
|
|
14549
14980
|
};
|
|
14550
14981
|
}));
|
|
@@ -14570,7 +15001,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14570
15001
|
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;
|
|
14571
15002
|
const seen = /* @__PURE__ */ new Set();
|
|
14572
15003
|
for (const record4 of records) {
|
|
14573
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
15004
|
+
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;
|
|
14574
15005
|
const key3 = recordKey(record4);
|
|
14575
15006
|
if (seen.has(key3)) return false;
|
|
14576
15007
|
seen.add(key3);
|
|
@@ -14581,16 +15012,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14581
15012
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14582
15013
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14583
15014
|
const key3 = await parityKey(input.secret);
|
|
14584
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
14585
|
-
const customerId =
|
|
14586
|
-
const subscriptionId =
|
|
15015
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
|
|
15016
|
+
const customerId = text8(row2.customerId);
|
|
15017
|
+
const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
|
|
14587
15018
|
return {
|
|
14588
15019
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
14589
15020
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
14590
15021
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
14591
15022
|
status: null,
|
|
14592
15023
|
canceled: false,
|
|
14593
|
-
stripeStatus:
|
|
15024
|
+
stripeStatus: text8(row2.status, 64),
|
|
14594
15025
|
stripeCanceled: row2.status === "canceled"
|
|
14595
15026
|
};
|
|
14596
15027
|
}));
|
|
@@ -14787,6 +15218,40 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
|
14787
15218
|
};
|
|
14788
15219
|
}
|
|
14789
15220
|
|
|
15221
|
+
// src/worker-network-commercial-dispute.ts
|
|
15222
|
+
async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
15223
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
15224
|
+
if (!operator || !fact.chargeId) return { disposition: "unhosted" };
|
|
15225
|
+
let db;
|
|
15226
|
+
try {
|
|
15227
|
+
db = operator.makeDb(env);
|
|
15228
|
+
} catch {
|
|
15229
|
+
return { disposition: "unhosted" };
|
|
15230
|
+
}
|
|
15231
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15232
|
+
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15233
|
+
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15234
|
+
const scopeOf = (row2) => ({
|
|
15235
|
+
appId: String(row2.appId),
|
|
15236
|
+
chapterId: String(row2.chapterId),
|
|
15237
|
+
environment: String(row2.environment),
|
|
15238
|
+
runtime: String(row2.runtime),
|
|
15239
|
+
applicationId: String(row2.applicationId),
|
|
15240
|
+
skuId: String(row2.skuId)
|
|
15241
|
+
});
|
|
15242
|
+
if (charge.seatId) {
|
|
15243
|
+
const seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
15244
|
+
if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
|
|
15245
|
+
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15246
|
+
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15247
|
+
}
|
|
15248
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15249
|
+
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15250
|
+
if (!member) return { disposition: "not_applicable" };
|
|
15251
|
+
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15252
|
+
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15253
|
+
}
|
|
15254
|
+
|
|
14790
15255
|
// src/worker-routes-network-commercial.ts
|
|
14791
15256
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
14792
15257
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -14851,6 +15316,12 @@ function safeProjection(projection) {
|
|
|
14851
15316
|
amountReceivedCents: seat.amountReceivedCents,
|
|
14852
15317
|
refundedCents: seat.refundedCents ?? 0
|
|
14853
15318
|
})),
|
|
15319
|
+
disputes: (projection.disputes ?? []).map((dispute2) => ({
|
|
15320
|
+
...row2(dispute2),
|
|
15321
|
+
chargeId: dispute2.chargeId ?? null,
|
|
15322
|
+
paymentIntentId: dispute2.paymentIntentId ?? null,
|
|
15323
|
+
reason: dispute2.disputeReason ?? null
|
|
15324
|
+
})),
|
|
14854
15325
|
exceptions: projection.exceptions
|
|
14855
15326
|
};
|
|
14856
15327
|
}
|
|
@@ -14883,10 +15354,20 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14883
15354
|
if (result.status === "collision") {
|
|
14884
15355
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
14885
15356
|
}
|
|
15357
|
+
let dispute2;
|
|
15358
|
+
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15359
|
+
try {
|
|
15360
|
+
dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15361
|
+
} catch (error) {
|
|
15362
|
+
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15363
|
+
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
15364
|
+
}
|
|
15365
|
+
}
|
|
14886
15366
|
return json({
|
|
14887
15367
|
ok: true,
|
|
14888
15368
|
status: result.status,
|
|
14889
15369
|
eventId: fact.eventId,
|
|
15370
|
+
...dispute2 ? { dispute: dispute2.disposition } : {},
|
|
14890
15371
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14891
15372
|
});
|
|
14892
15373
|
} catch (error) {
|
|
@@ -15084,7 +15565,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15084
15565
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15085
15566
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15086
15567
|
}
|
|
15087
|
-
function
|
|
15568
|
+
function text9(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15088
15569
|
if (value === void 0 && !required) return void 0;
|
|
15089
15570
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15090
15571
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15110,7 +15591,7 @@ function scopeValue(value, path, errors) {
|
|
|
15110
15591
|
return { kind: raw2.kind };
|
|
15111
15592
|
}
|
|
15112
15593
|
if (raw2.kind === "chapter") {
|
|
15113
|
-
const chapterId =
|
|
15594
|
+
const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15114
15595
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15115
15596
|
return { kind: "chapter", chapterId };
|
|
15116
15597
|
}
|
|
@@ -15124,22 +15605,22 @@ function resourceValue(value, path, errors) {
|
|
|
15124
15605
|
return null;
|
|
15125
15606
|
}
|
|
15126
15607
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15127
|
-
const id7 =
|
|
15608
|
+
const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15128
15609
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15129
|
-
const kind =
|
|
15610
|
+
const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15130
15611
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15131
|
-
const title =
|
|
15132
|
-
const summary =
|
|
15133
|
-
const status2 =
|
|
15612
|
+
const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15613
|
+
const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15614
|
+
const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
|
|
15134
15615
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15135
15616
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15136
15617
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15137
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15618
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15138
15619
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15139
15620
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15140
15621
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15141
|
-
const bodyMarkdown =
|
|
15142
|
-
const url =
|
|
15622
|
+
const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15623
|
+
const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15143
15624
|
if (url) {
|
|
15144
15625
|
try {
|
|
15145
15626
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15151,9 +15632,9 @@ function resourceValue(value, path, errors) {
|
|
|
15151
15632
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15152
15633
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15153
15634
|
const brandAsset = asset ? {
|
|
15154
|
-
bookId:
|
|
15155
|
-
assetId:
|
|
15156
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15635
|
+
bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15636
|
+
assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15637
|
+
...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15157
15638
|
} : void 0;
|
|
15158
15639
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15159
15640
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15168,17 +15649,17 @@ function resourceValue(value, path, errors) {
|
|
|
15168
15649
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15169
15650
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15170
15651
|
const promotedFrom = promotedRaw ? {
|
|
15171
|
-
chapterId:
|
|
15172
|
-
resourceId:
|
|
15652
|
+
chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15653
|
+
resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15173
15654
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15174
15655
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15175
|
-
promotedBy:
|
|
15656
|
+
promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15176
15657
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15177
15658
|
} : void 0;
|
|
15178
15659
|
const provenance2 = {
|
|
15179
|
-
createdBy:
|
|
15660
|
+
createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15180
15661
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15181
|
-
updatedBy:
|
|
15662
|
+
updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15182
15663
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15183
15664
|
...promotedFrom ? { promotedFrom } : {}
|
|
15184
15665
|
};
|
|
@@ -15197,7 +15678,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15197
15678
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15198
15679
|
const sourceType = sourceRaw?.type;
|
|
15199
15680
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15200
|
-
const sourceAppId =
|
|
15681
|
+
const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15201
15682
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15202
15683
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15203
15684
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15205,8 +15686,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15205
15686
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15206
15687
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15207
15688
|
const target = targetRaw ? {
|
|
15208
|
-
appId:
|
|
15209
|
-
environment:
|
|
15689
|
+
appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15690
|
+
environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15210
15691
|
} : void 0;
|
|
15211
15692
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15212
15693
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15547,7 +16028,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
15547
16028
|
};
|
|
15548
16029
|
|
|
15549
16030
|
// src/membership-cancel-authority.ts
|
|
15550
|
-
import { stripeRequest as
|
|
16031
|
+
import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15551
16032
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
15552
16033
|
membershipClock(now);
|
|
15553
16034
|
const id7 = await networkMembershipId(target);
|
|
@@ -15558,14 +16039,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
15558
16039
|
membershipFail("membership_cancellation_unavailable");
|
|
15559
16040
|
}
|
|
15560
16041
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
15561
|
-
const read2 = await
|
|
16042
|
+
const read2 = await stripeRequest13({ secretKey: stripeSecret }, "GET", path);
|
|
15562
16043
|
const sub = read2.body;
|
|
15563
16044
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
15564
16045
|
...stripePartitionMetadata(target),
|
|
15565
16046
|
applicationId: target.applicationId
|
|
15566
16047
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
15567
16048
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
15568
|
-
const write = await
|
|
16049
|
+
const write = await stripeRequest13(
|
|
15569
16050
|
{ secretKey: stripeSecret },
|
|
15570
16051
|
"POST",
|
|
15571
16052
|
path,
|
|
@@ -15787,7 +16268,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
15787
16268
|
}
|
|
15788
16269
|
|
|
15789
16270
|
// src/membership-provider-lifecycle.ts
|
|
15790
|
-
import { stripeRequest as
|
|
16271
|
+
import { stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
|
|
15791
16272
|
|
|
15792
16273
|
// src/membership-renewal-authority.ts
|
|
15793
16274
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -15909,6 +16390,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15909
16390
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
15910
16391
|
const fact = row2?.fact;
|
|
15911
16392
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
16393
|
+
if (fact?.objectKind === "dispute") {
|
|
16394
|
+
const secret = await getVaultSecret(db, stripeSecretName);
|
|
16395
|
+
if (!secret) membershipFail("membership_billing_provider_unavailable");
|
|
16396
|
+
return reconcileMembershipDispute(db, input, now, secret);
|
|
16397
|
+
}
|
|
15912
16398
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15913
16399
|
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({
|
|
15914
16400
|
appId: input.appId,
|
|
@@ -15951,7 +16437,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15951
16437
|
}
|
|
15952
16438
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15953
16439
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15954
|
-
const result = await
|
|
16440
|
+
const result = await stripeRequest14({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15955
16441
|
const sub = result.body;
|
|
15956
16442
|
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)) {
|
|
15957
16443
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15989,7 +16475,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15989
16475
|
}
|
|
15990
16476
|
|
|
15991
16477
|
// src/membership-seat-stripe.ts
|
|
15992
|
-
import { stripeRequest as
|
|
16478
|
+
import { stripeRequest as stripeRequest15 } from "@odla-ai/stripe";
|
|
15993
16479
|
var reject7 = () => {
|
|
15994
16480
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15995
16481
|
};
|
|
@@ -16001,7 +16487,7 @@ var metadataFor = (seat) => ({
|
|
|
16001
16487
|
});
|
|
16002
16488
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
16003
16489
|
async function read(sk, path, params) {
|
|
16004
|
-
const response = await
|
|
16490
|
+
const response = await stripeRequest15({ secretKey: sk }, "GET", path, params);
|
|
16005
16491
|
if (!response.ok) return reject7();
|
|
16006
16492
|
return response.body;
|
|
16007
16493
|
}
|
|
@@ -16038,11 +16524,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
16038
16524
|
}
|
|
16039
16525
|
await subscriptionForSeat(sk, seat);
|
|
16040
16526
|
const metadata = metadataFor(seat);
|
|
16041
|
-
const result = seat.providerPaymentIntentId ? await
|
|
16527
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest15(
|
|
16042
16528
|
{ secretKey: sk },
|
|
16043
16529
|
"GET",
|
|
16044
16530
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
16045
|
-
) : await
|
|
16531
|
+
) : await stripeRequest15({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
16046
16532
|
amount: seat.amountCents,
|
|
16047
16533
|
currency: seat.currency,
|
|
16048
16534
|
customer: seat.providerCustomerId,
|
|
@@ -16064,7 +16550,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
16064
16550
|
return String(existing.id);
|
|
16065
16551
|
}
|
|
16066
16552
|
if (seat.combinedInvoice) reject7();
|
|
16067
|
-
const result = await
|
|
16553
|
+
const result = await stripeRequest15({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
16068
16554
|
subscription: seat.providerSubscriptionId,
|
|
16069
16555
|
price: seat.priceId,
|
|
16070
16556
|
quantity: 1,
|
|
@@ -16726,6 +17212,19 @@ var id5 = () => attr("string", false, true, true);
|
|
|
16726
17212
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
16727
17213
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
16728
17214
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
17215
|
+
var dispute = () => ({
|
|
17216
|
+
disputeId: key(true),
|
|
17217
|
+
disputeStatus: attr("string", true),
|
|
17218
|
+
disputeReason: attr("string", true),
|
|
17219
|
+
disputeAmountCents: attr("number", true),
|
|
17220
|
+
disputedAt: attr("number", true),
|
|
17221
|
+
disputeClosedAt: attr("number", true),
|
|
17222
|
+
disputePriorStatus: attr("string", true),
|
|
17223
|
+
disputeEventId: key(true),
|
|
17224
|
+
rechargePaymentIntentId: key(true),
|
|
17225
|
+
rechargedAt: attr("number", true),
|
|
17226
|
+
rechargeAttempt: attr("json", true)
|
|
17227
|
+
});
|
|
16729
17228
|
var membershipAuthorityEntities = {
|
|
16730
17229
|
networkMembership: { attrs: {
|
|
16731
17230
|
id: id5(),
|
|
@@ -16765,7 +17264,8 @@ var membershipAuthorityEntities = {
|
|
|
16765
17264
|
graceEndsAt: attr("number", true),
|
|
16766
17265
|
...times(),
|
|
16767
17266
|
refundRequestedAt: attr("number", true),
|
|
16768
|
-
restartId: key(true)
|
|
17267
|
+
restartId: key(true),
|
|
17268
|
+
...dispute()
|
|
16769
17269
|
} },
|
|
16770
17270
|
membershipRestart: { attrs: {
|
|
16771
17271
|
id: id5(),
|
|
@@ -16841,7 +17341,8 @@ var membershipAuthorityEntities = {
|
|
|
16841
17341
|
refundedCents: attr("number", true),
|
|
16842
17342
|
providerRefundIds: attr("json", true),
|
|
16843
17343
|
regiftedAt: attr("number", true),
|
|
16844
|
-
formerRecipients: attr("json", true)
|
|
17344
|
+
formerRecipients: attr("json", true),
|
|
17345
|
+
...dispute()
|
|
16845
17346
|
} },
|
|
16846
17347
|
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
16847
17348
|
membershipEffect: { attrs: {
|
|
@@ -16962,7 +17463,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16962
17463
|
}
|
|
16963
17464
|
|
|
16964
17465
|
// src/membership-seat-cancel.ts
|
|
16965
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
17466
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest16 } from "@odla-ai/stripe";
|
|
16966
17467
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16967
17468
|
membershipClock(now);
|
|
16968
17469
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16990,7 +17491,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16990
17491
|
}
|
|
16991
17492
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16992
17493
|
const request = async (method, path, params) => {
|
|
16993
|
-
const result = await
|
|
17494
|
+
const result = await stripeRequest16(
|
|
16994
17495
|
{ secretKey },
|
|
16995
17496
|
method,
|
|
16996
17497
|
path,
|
|
@@ -17178,7 +17679,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17178
17679
|
};
|
|
17179
17680
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17180
17681
|
let size = 0;
|
|
17181
|
-
let
|
|
17682
|
+
let text10 = "";
|
|
17182
17683
|
const decoder = new TextDecoder();
|
|
17183
17684
|
try {
|
|
17184
17685
|
while (true) {
|
|
@@ -17188,10 +17689,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17188
17689
|
if (done) break;
|
|
17189
17690
|
size += value.byteLength;
|
|
17190
17691
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17191
|
-
|
|
17692
|
+
text10 += decoder.decode(value, { stream: true });
|
|
17192
17693
|
}
|
|
17193
17694
|
try {
|
|
17194
|
-
return JSON.parse(
|
|
17695
|
+
return JSON.parse(text10 + decoder.decode());
|
|
17195
17696
|
} catch {
|
|
17196
17697
|
throw new TypeError(`invalid ${label}`);
|
|
17197
17698
|
}
|
|
@@ -17526,7 +18027,7 @@ async function requestMembershipRefund(options) {
|
|
|
17526
18027
|
async function requestMembershipRenewal(options) {
|
|
17527
18028
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
17528
18029
|
const row2 = result.renewal;
|
|
17529
|
-
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)) {
|
|
18030
|
+
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)) {
|
|
17530
18031
|
throw new Error("invalid membership renewal receipt");
|
|
17531
18032
|
}
|
|
17532
18033
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -17878,6 +18379,7 @@ var BUILTIN_ROUTES = [
|
|
|
17878
18379
|
handlePaymentRecovery,
|
|
17879
18380
|
handleAdminNamedSeat,
|
|
17880
18381
|
handleAdminMembershipSeats,
|
|
18382
|
+
handleAdminMembershipDisputes,
|
|
17881
18383
|
handleAdminMembershipPolicyVersions,
|
|
17882
18384
|
handleAdminMembershipReterm,
|
|
17883
18385
|
handleAdminMeetings,
|
|
@@ -17926,8 +18428,9 @@ function chapterWorker(options) {
|
|
|
17926
18428
|
const ctx = createWorkerContext(options);
|
|
17927
18429
|
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
17928
18430
|
return {
|
|
17929
|
-
async fetch(req, env) {
|
|
18431
|
+
async fetch(req, env, execution) {
|
|
17930
18432
|
const url = new URL(req.url);
|
|
18433
|
+
ctx.bindExecution(req, execution);
|
|
17931
18434
|
for (const route of routes) {
|
|
17932
18435
|
let res;
|
|
17933
18436
|
try {
|
|
@@ -17949,6 +18452,7 @@ export {
|
|
|
17949
18452
|
DECLINABLE_FROM,
|
|
17950
18453
|
MEMBERSHIP_AUTHORITY_PATH,
|
|
17951
18454
|
MEMBERSHIP_CHAPTER_EFFECT_KINDS,
|
|
18455
|
+
MEMBERSHIP_DISPUTED_STATUS,
|
|
17952
18456
|
NetworkTargetTransportError,
|
|
17953
18457
|
REGIFTABLE_SEAT_STATUSES,
|
|
17954
18458
|
chapterMembershipAccess,
|
|
@@ -17966,6 +18470,7 @@ export {
|
|
|
17966
18470
|
deliverMemberContent,
|
|
17967
18471
|
deliverMembershipChapterEffect,
|
|
17968
18472
|
deliverSignupControl,
|
|
18473
|
+
disputedChargeCoversSeat,
|
|
17969
18474
|
drainMembershipEffects,
|
|
17970
18475
|
grantIncludedNamedSeat,
|
|
17971
18476
|
liftSeatPurchaseWindow,
|
|
@@ -17974,7 +18479,12 @@ export {
|
|
|
17974
18479
|
namedSeatInvitationOpen,
|
|
17975
18480
|
namedSeatRecipientState,
|
|
17976
18481
|
namedSeatRegiftable,
|
|
18482
|
+
readDisputedCharge,
|
|
17977
18483
|
readNamedSeatOffer,
|
|
18484
|
+
rechargeDisputedMembership,
|
|
18485
|
+
rechargeDisputedSeat,
|
|
18486
|
+
reconcileMembershipDispute,
|
|
18487
|
+
reconcileNamedSeatDispute,
|
|
17978
18488
|
refundNamedSeatByOperator,
|
|
17979
18489
|
regiftNamedSeat,
|
|
17980
18490
|
releaseNamedSeatRecipientApplications,
|