@odla-ai/chapter 0.58.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/{chunk-FHKZVF5G.js → chunk-GADLTNJF.js} +4 -2
- package/dist/{chunk-FHKZVF5G.js.map → chunk-GADLTNJF.js.map} +1 -1
- package/dist/{chunk-L2IPS2FQ.js → chunk-WFTWT3V7.js} +141 -56
- package/dist/chunk-WFTWT3V7.js.map +1 -0
- package/dist/{chunk-CKC6X5H2.js → chunk-Y2H4VURI.js} +399 -308
- package/dist/chunk-Y2H4VURI.js.map +1 -0
- package/dist/{copy-context-DE4YMAbz.d.ts → copy-context-BuJBQafl.d.ts} +4 -0
- package/dist/index.cjs +218 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +214 -80
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +36 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +15 -3
- package/dist/ui/member/index.d.ts +33 -5
- package/dist/ui/member/index.js +10 -2
- package/dist/worker/index.cjs +611 -184
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +64 -2
- package/dist/worker/index.d.ts +64 -2
- package/dist/worker/index.js +604 -177
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CKC6X5H2.js.map +0 -1
- package/dist/chunk-L2IPS2FQ.js.map +0 -1
package/dist/worker/index.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, text9) {
|
|
1592
1637
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1593
|
-
const bodyCopy =
|
|
1638
|
+
const bodyCopy = text9.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 text9 = 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: text9, 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 text9 = (v) => typeof v === "string" ? v : "";
|
|
1868
|
+
return { firstName: text9(app.firstName), lastName: text9(app.lastName), email: text9(app.email), phone: text9(app.phone), state: text9(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,102 @@ 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
|
+
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3603
|
+
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3604
|
+
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3605
|
+
var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3606
|
+
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3607
|
+
var refOf = (value) => typeof value === "string" ? text2(value) : text2(value?.id);
|
|
3608
|
+
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3609
|
+
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3610
|
+
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
3611
|
+
const charge = await stripeRequest4(config, "GET", `/v1/charges/${chargeId}`);
|
|
3612
|
+
if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
|
|
3613
|
+
const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
|
|
3614
|
+
if (!customerId) membershipFail("membership_dispute_provider_unavailable");
|
|
3615
|
+
let subscriptionId;
|
|
3616
|
+
if (invoiceId) {
|
|
3617
|
+
const invoice = await stripeRequest4(config, "GET", `/v1/invoices/${invoiceId}`);
|
|
3618
|
+
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3619
|
+
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3620
|
+
}
|
|
3621
|
+
const seatId = text2(charge.body.metadata?.seatId);
|
|
3622
|
+
return {
|
|
3623
|
+
chargeId,
|
|
3624
|
+
customerId,
|
|
3625
|
+
...invoiceId ? { invoiceId } : {},
|
|
3626
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
3627
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
3628
|
+
...seatId ? { seatId } : {}
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
function disputedChargeCoversMembership(charge, member) {
|
|
3632
|
+
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);
|
|
3633
|
+
}
|
|
3634
|
+
async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
3635
|
+
membershipClock(now);
|
|
3636
|
+
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3637
|
+
const fact = row2?.fact;
|
|
3638
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3639
|
+
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");
|
|
3640
|
+
const id7 = await networkMembershipId(input);
|
|
3641
|
+
const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
|
|
3642
|
+
membershipId: id7,
|
|
3643
|
+
status: String(value.status),
|
|
3644
|
+
termEndsAt: Number(value.termEndsAt),
|
|
3645
|
+
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3646
|
+
autoRenew: value.autoRenew === true,
|
|
3647
|
+
...text2(value.disputeStatus) ? { disputeStatus: text2(value.disputeStatus) } : {},
|
|
3648
|
+
applied
|
|
3649
|
+
});
|
|
3650
|
+
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3651
|
+
requireMembershipScope(known, input);
|
|
3652
|
+
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3653
|
+
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3654
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3655
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3656
|
+
requireMembershipScope(member, input);
|
|
3657
|
+
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3658
|
+
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3659
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3660
|
+
const sameDispute = member.disputeId === fact.objectId;
|
|
3661
|
+
if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
|
|
3662
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3663
|
+
const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
|
|
3664
|
+
const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3665
|
+
const attrs = {
|
|
3666
|
+
status: next,
|
|
3667
|
+
disputeId: fact.objectId,
|
|
3668
|
+
disputeStatus: status2,
|
|
3669
|
+
disputeEventId: input.eventId,
|
|
3670
|
+
disputeClosedAt: closed ? now : 0,
|
|
3671
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3672
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3673
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3674
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
|
|
3675
|
+
revision: Number(member.revision) + 1,
|
|
3676
|
+
updatedAt: now
|
|
3677
|
+
};
|
|
3678
|
+
try {
|
|
3679
|
+
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3680
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
3681
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3682
|
+
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3683
|
+
evidenceGuard
|
|
3684
|
+
] });
|
|
3685
|
+
} catch (error) {
|
|
3686
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
3690
|
+
requireMembershipScope(saved, input);
|
|
3691
|
+
return receipt(saved, true);
|
|
3692
|
+
}
|
|
3693
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3555
3696
|
// src/member-system-contract.ts
|
|
3556
3697
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3557
3698
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3676,7 +3817,8 @@ async function firstRow2(db, ns, q) {
|
|
|
3676
3817
|
}
|
|
3677
3818
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3678
3819
|
const db = ctx.makeDb(env);
|
|
3679
|
-
const { rawBody, eventId
|
|
3820
|
+
const { rawBody, eventId } = input;
|
|
3821
|
+
let { event: event2 } = input;
|
|
3680
3822
|
if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
|
|
3681
3823
|
const digest = await stripeEventDigest(rawBody);
|
|
3682
3824
|
const legacyDigest = await stripePayloadDigest(rawBody);
|
|
@@ -3732,6 +3874,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3732
3874
|
}
|
|
3733
3875
|
return json({ ok: true, matched: Boolean(claim) });
|
|
3734
3876
|
}
|
|
3877
|
+
if (event2.kind === "disputed" && !event2.customerId) {
|
|
3878
|
+
const secret = await getVaultSecret(db, "stripe_secret_key");
|
|
3879
|
+
const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
|
|
3880
|
+
if (charge) event2 = { ...event2, customerId: charge.customerId };
|
|
3881
|
+
}
|
|
3735
3882
|
const app = await findPaymentApplication(db, event2);
|
|
3736
3883
|
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
3737
3884
|
reportStripePartition("warn", eventId, event2, "ignore_sibling");
|
|
@@ -3769,11 +3916,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3769
3916
|
}
|
|
3770
3917
|
let authoritativeEvent = event2;
|
|
3771
3918
|
let membershipProjection = {};
|
|
3772
|
-
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
|
|
3919
|
+
if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
|
|
3773
3920
|
try {
|
|
3774
3921
|
const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
|
|
3775
3922
|
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 };
|
|
3923
|
+
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
3924
|
} catch {
|
|
3778
3925
|
return json({ error: "network membership renewal reconciliation pending" }, 503);
|
|
3779
3926
|
}
|
|
@@ -4422,10 +4569,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
4422
4569
|
}
|
|
4423
4570
|
|
|
4424
4571
|
// src/membership-seat-refund-stripe.ts
|
|
4425
|
-
import { stripeRequest as
|
|
4572
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4426
4573
|
|
|
4427
4574
|
// src/membership-checkout-seat-refund.ts
|
|
4428
|
-
import { stripeRequest as
|
|
4575
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4429
4576
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4430
4577
|
throw Object.assign(new Error(code), { code });
|
|
4431
4578
|
};
|
|
@@ -4434,7 +4581,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
4434
4581
|
const invoiceRef = seat.combinedInvoice;
|
|
4435
4582
|
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
4583
|
const call = async (method, path, params, key3) => {
|
|
4437
|
-
const result = await
|
|
4584
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4438
4585
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4439
4586
|
return result.body;
|
|
4440
4587
|
};
|
|
@@ -4507,7 +4654,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
4507
4654
|
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4508
4655
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4509
4656
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4510
|
-
const result = await
|
|
4657
|
+
const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4511
4658
|
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4512
4659
|
return result.body;
|
|
4513
4660
|
};
|
|
@@ -4975,15 +5122,224 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4975
5122
|
}
|
|
4976
5123
|
};
|
|
4977
5124
|
|
|
5125
|
+
// src/membership-dispute-recharge.ts
|
|
5126
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
5127
|
+
var fail6 = (code, extra = {}) => {
|
|
5128
|
+
throw Object.assign(new Error(code), { code, ...extra });
|
|
5129
|
+
};
|
|
5130
|
+
var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
|
|
5131
|
+
async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
5132
|
+
const subscription = await stripeRequest7(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
5133
|
+
if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
|
|
5134
|
+
const onSubscription = refOf2(subscription.body.default_payment_method);
|
|
5135
|
+
if (onSubscription) return onSubscription;
|
|
5136
|
+
const customer = await stripeRequest7(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
|
|
5137
|
+
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5138
|
+
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5139
|
+
}
|
|
5140
|
+
async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
5141
|
+
membershipClock(now);
|
|
5142
|
+
let member = await membershipLoad(db, "networkMembership", membershipId);
|
|
5143
|
+
if (!member) fail6("membership_not_found");
|
|
5144
|
+
const scope2 = { chapterId: String(member.chapterId), environment: String(member.environment) };
|
|
5145
|
+
const amountCents = Number(member.disputeAmountCents ?? member.amountCents), currency = String(member.currency);
|
|
5146
|
+
const done = (value, replayed) => ({
|
|
5147
|
+
membershipId,
|
|
5148
|
+
status: String(value.status),
|
|
5149
|
+
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5150
|
+
amountCents,
|
|
5151
|
+
currency,
|
|
5152
|
+
...replayed ? { replayed } : {}
|
|
5153
|
+
});
|
|
5154
|
+
if (member.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5155
|
+
if (member.disputeStatus === "recharged" && member.rechargePaymentIntentId) return done(member, true);
|
|
5156
|
+
fail6("membership_not_disputed");
|
|
5157
|
+
}
|
|
5158
|
+
if (member.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5159
|
+
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5160
|
+
let attempt = member.rechargeAttempt;
|
|
5161
|
+
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5162
|
+
attempt = { key: `membership-recharge:${membershipId}:${String(member.disputeId)}:${now}`, startedAt: now };
|
|
5163
|
+
await db.transact(
|
|
5164
|
+
[membershipUpdate("networkMembership", membershipId, { rechargeAttempt: attempt, revision: Number(member.revision) + 1, updatedAt: now })],
|
|
5165
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{
|
|
5166
|
+
ns: "networkMembership",
|
|
5167
|
+
id: membershipId,
|
|
5168
|
+
exists: true,
|
|
5169
|
+
equals: { revision: member.revision, status: MEMBERSHIP_DISPUTED_STATUS }
|
|
5170
|
+
}] }
|
|
5171
|
+
);
|
|
5172
|
+
member = await membershipLoad(db, "networkMembership", membershipId);
|
|
5173
|
+
if (!member || member.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5174
|
+
}
|
|
5175
|
+
const config = { secretKey };
|
|
5176
|
+
const paymentMethod = await defaultPaymentMethod(config, String(member.providerSubscriptionId), String(member.providerCustomerId));
|
|
5177
|
+
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5178
|
+
const intent = await stripeRequest7(config, "POST", "/v1/payment_intents", {
|
|
5179
|
+
amount: amountCents,
|
|
5180
|
+
currency,
|
|
5181
|
+
customer: String(member.providerCustomerId),
|
|
5182
|
+
payment_method: paymentMethod,
|
|
5183
|
+
off_session: "true",
|
|
5184
|
+
confirm: "true",
|
|
5185
|
+
description: `Membership payment reissued after dispute ${String(member.disputeId)}`,
|
|
5186
|
+
metadata: {
|
|
5187
|
+
...stripePartitionMetadata({ appId: String(member.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(member.runtime) }),
|
|
5188
|
+
applicationId: String(member.applicationId),
|
|
5189
|
+
membershipId,
|
|
5190
|
+
disputeId: String(member.disputeId),
|
|
5191
|
+
membershipRecharge: "true"
|
|
5192
|
+
}
|
|
5193
|
+
}, { idempotencyKey: attempt.key });
|
|
5194
|
+
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5195
|
+
const finish = async (outcome) => {
|
|
5196
|
+
const current = await membershipLoad(db, "networkMembership", membershipId);
|
|
5197
|
+
if (!current) return;
|
|
5198
|
+
await db.transact([membershipUpdate("networkMembership", membershipId, {
|
|
5199
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5200
|
+
revision: Number(current.revision) + 1,
|
|
5201
|
+
updatedAt: now
|
|
5202
|
+
})], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
|
|
5203
|
+
};
|
|
5204
|
+
if (!intent.ok) {
|
|
5205
|
+
if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
|
|
5206
|
+
await finish("declined");
|
|
5207
|
+
fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
|
|
5208
|
+
}
|
|
5209
|
+
fail6("membership_dispute_provider_unavailable");
|
|
5210
|
+
}
|
|
5211
|
+
if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
|
|
5212
|
+
await finish("declined");
|
|
5213
|
+
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5214
|
+
}
|
|
5215
|
+
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5216
|
+
if (refOf2(body.customer) !== member.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5217
|
+
for (let retry = 0; retry < 8; retry++) {
|
|
5218
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, "networkMembership", membershipId);
|
|
5219
|
+
if (!current) fail6("membership_not_found");
|
|
5220
|
+
if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
|
|
5221
|
+
const status2 = String(current.disputePriorStatus ?? (current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5222
|
+
try {
|
|
5223
|
+
await db.transact(
|
|
5224
|
+
[membershipUpdate("networkMembership", membershipId, {
|
|
5225
|
+
status: status2,
|
|
5226
|
+
disputeStatus: "recharged",
|
|
5227
|
+
rechargePaymentIntentId: String(body.id),
|
|
5228
|
+
rechargedAt: now,
|
|
5229
|
+
rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
|
|
5230
|
+
revision: Number(current.revision) + 1,
|
|
5231
|
+
updatedAt: now
|
|
5232
|
+
})],
|
|
5233
|
+
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5234
|
+
{ ns: "networkMembership", id: membershipId, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5235
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5236
|
+
] }
|
|
5237
|
+
);
|
|
5238
|
+
} catch (cause) {
|
|
5239
|
+
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5240
|
+
continue;
|
|
5241
|
+
}
|
|
5242
|
+
return done(await membershipLoad(db, "networkMembership", membershipId));
|
|
5243
|
+
}
|
|
5244
|
+
return fail6("membership_dispute_retry_required");
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
// src/worker-routes-admin-membership-disputes.ts
|
|
5248
|
+
var LIST2 = "/api/admin/membership-disputes";
|
|
5249
|
+
var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
|
|
5250
|
+
var PUBLIC2 = [
|
|
5251
|
+
"id",
|
|
5252
|
+
"appId",
|
|
5253
|
+
"chapterId",
|
|
5254
|
+
"environment",
|
|
5255
|
+
"runtime",
|
|
5256
|
+
"applicationId",
|
|
5257
|
+
"skuId",
|
|
5258
|
+
"status",
|
|
5259
|
+
"memberNumber",
|
|
5260
|
+
"amountCents",
|
|
5261
|
+
"currency",
|
|
5262
|
+
"disputeId",
|
|
5263
|
+
"disputeStatus",
|
|
5264
|
+
"disputeReason",
|
|
5265
|
+
"disputeAmountCents",
|
|
5266
|
+
"disputedAt",
|
|
5267
|
+
"disputeClosedAt",
|
|
5268
|
+
"disputePriorStatus",
|
|
5269
|
+
"rechargePaymentIntentId",
|
|
5270
|
+
"rechargedAt",
|
|
5271
|
+
"termEndsAt",
|
|
5272
|
+
"updatedAt"
|
|
5273
|
+
];
|
|
5274
|
+
var project2 = (row2) => ({
|
|
5275
|
+
...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
|
|
5276
|
+
...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
|
|
5277
|
+
startedAt: row2.rechargeAttempt.startedAt ?? null,
|
|
5278
|
+
finishedAt: row2.rechargeAttempt.finishedAt ?? null,
|
|
5279
|
+
outcome: row2.rechargeAttempt.outcome ?? null
|
|
5280
|
+
} } : {}
|
|
5281
|
+
});
|
|
5282
|
+
var FINAL = /* @__PURE__ */ new Set([
|
|
5283
|
+
"membership_not_found",
|
|
5284
|
+
"membership_not_disputed",
|
|
5285
|
+
"membership_dispute_open",
|
|
5286
|
+
"membership_dispute_amount_invalid",
|
|
5287
|
+
"recharge_payment_method_unavailable",
|
|
5288
|
+
"recharge_declined",
|
|
5289
|
+
"recharge_pending",
|
|
5290
|
+
"recharge_mismatch",
|
|
5291
|
+
"membership_dispute_changed"
|
|
5292
|
+
]);
|
|
5293
|
+
var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
5294
|
+
const one = url.pathname.match(ONE2);
|
|
5295
|
+
if (url.pathname !== LIST2 && !one) return null;
|
|
5296
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5297
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5298
|
+
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
5299
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5300
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5301
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5302
|
+
let db;
|
|
5303
|
+
try {
|
|
5304
|
+
db = operator.makeDb(env);
|
|
5305
|
+
} catch {
|
|
5306
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5307
|
+
}
|
|
5308
|
+
if (!one) {
|
|
5309
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } } })).networkMembership ?? [];
|
|
5310
|
+
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5311
|
+
return json({ disputes: disputes.map(project2), truncated: rows5.length >= 500 });
|
|
5312
|
+
}
|
|
5313
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5314
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5315
|
+
try {
|
|
5316
|
+
const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
|
|
5317
|
+
return json({ ok: true, ...receipt });
|
|
5318
|
+
} catch (error) {
|
|
5319
|
+
const failure = error;
|
|
5320
|
+
const code = typeof failure.code === "string" ? failure.code : void 0;
|
|
5321
|
+
if (code === "membership_not_found") return json({ error: "not found", code }, 404);
|
|
5322
|
+
if (code && FINAL.has(code)) {
|
|
5323
|
+
return json({
|
|
5324
|
+
error: "the charge could not be re-issued",
|
|
5325
|
+
code,
|
|
5326
|
+
...failure.declineCode ? { declineCode: failure.declineCode } : {},
|
|
5327
|
+
...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
|
|
5328
|
+
}, 409);
|
|
5329
|
+
}
|
|
5330
|
+
return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
|
|
5331
|
+
}
|
|
5332
|
+
};
|
|
5333
|
+
|
|
4978
5334
|
// src/membership-policy-versions.ts
|
|
4979
5335
|
var LIMIT = 200;
|
|
4980
|
-
var
|
|
5336
|
+
var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4981
5337
|
var instant = (value) => {
|
|
4982
5338
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4983
5339
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
4984
5340
|
};
|
|
4985
5341
|
function provenance(row2) {
|
|
4986
|
-
const source =
|
|
5342
|
+
const source = text3(row2.source), actor = text3(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
4987
5343
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
4988
5344
|
}
|
|
4989
5345
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -5077,16 +5433,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
5077
5433
|
};
|
|
5078
5434
|
|
|
5079
5435
|
// src/membership-discount-reterm.ts
|
|
5080
|
-
import { stripeRequest as
|
|
5436
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
5081
5437
|
|
|
5082
5438
|
// src/subscription-provider-contract.ts
|
|
5083
|
-
import { stripeRequest as
|
|
5439
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
5084
5440
|
function subscriptionContractMismatch() {
|
|
5085
5441
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
5086
5442
|
}
|
|
5087
5443
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
5088
5444
|
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
|
|
5445
|
+
const result = await stripeRequest8({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
5090
5446
|
const price = result.body;
|
|
5091
5447
|
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
5448
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -5124,7 +5480,7 @@ async function retermPolicy(db, scope2, id7) {
|
|
|
5124
5480
|
}
|
|
5125
5481
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
5126
5482
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
5127
|
-
const read2 = await
|
|
5483
|
+
const read2 = await stripeRequest9({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
5128
5484
|
const sub = read2.body, items = sub?.items?.data;
|
|
5129
5485
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
5130
5486
|
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 +5491,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
5135
5491
|
const present = discounts.map(couponOf);
|
|
5136
5492
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
5137
5493
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
5138
|
-
const written = await
|
|
5494
|
+
const written = await stripeRequest9({ secretKey: sk }, "POST", path, {
|
|
5139
5495
|
"discounts[0][coupon]": couponId,
|
|
5140
5496
|
"expand[0]": "discounts",
|
|
5141
5497
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -5984,8 +6340,8 @@ var APPLICATION_LABELS = {
|
|
|
5984
6340
|
state: "State"
|
|
5985
6341
|
};
|
|
5986
6342
|
function humanize(id7) {
|
|
5987
|
-
const
|
|
5988
|
-
return
|
|
6343
|
+
const text9 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6344
|
+
return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id7;
|
|
5989
6345
|
}
|
|
5990
6346
|
function applicationDefs(chapter) {
|
|
5991
6347
|
try {
|
|
@@ -6052,7 +6408,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6052
6408
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6053
6409
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6054
6410
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6055
|
-
var
|
|
6411
|
+
var text4 = (value, path, errors, max) => {
|
|
6056
6412
|
if (typeof value !== "string" || !value.trim()) {
|
|
6057
6413
|
errors.push(`${path} is required`);
|
|
6058
6414
|
return "";
|
|
@@ -6080,23 +6436,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6080
6436
|
}
|
|
6081
6437
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6082
6438
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6083
|
-
const id7 =
|
|
6439
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
6084
6440
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6085
6441
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6086
6442
|
ids2.add(id7);
|
|
6087
6443
|
const targetRaw = record(item.target);
|
|
6088
6444
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6089
6445
|
if (targetRaw?.type === "tier") {
|
|
6090
|
-
const tierId =
|
|
6446
|
+
const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6091
6447
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6092
6448
|
target = { type: "tier", tierId };
|
|
6093
6449
|
} else if (targetRaw?.type === "entitlement") {
|
|
6094
|
-
const entitlementKey =
|
|
6450
|
+
const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6095
6451
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6096
6452
|
target = { type: "entitlement", entitlementKey };
|
|
6097
6453
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6098
6454
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6099
|
-
const stripePriceId =
|
|
6455
|
+
const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6100
6456
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6101
6457
|
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
6458
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6118,9 +6474,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6118
6474
|
redemptionLifetimeHours: lifetime,
|
|
6119
6475
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6120
6476
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6121
|
-
title:
|
|
6122
|
-
description:
|
|
6123
|
-
ctaLabel:
|
|
6477
|
+
title: text4(item.title, `${path}.title`, errors, 160),
|
|
6478
|
+
description: text4(item.description, `${path}.description`, errors, 2e3),
|
|
6479
|
+
ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6124
6480
|
});
|
|
6125
6481
|
}
|
|
6126
6482
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6347,23 +6703,23 @@ async function createPaymentQuote(input) {
|
|
|
6347
6703
|
// src/membership-quote-projection.ts
|
|
6348
6704
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
6349
6705
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
6350
|
-
var
|
|
6706
|
+
var fail7 = (code) => {
|
|
6351
6707
|
throw Object.assign(new Error(code), { code });
|
|
6352
6708
|
};
|
|
6353
6709
|
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
6354
6710
|
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
|
|
6711
|
+
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");
|
|
6712
|
+
if (row2.status !== "ready") return fail7("membership_quote_pending");
|
|
6357
6713
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
6358
6714
|
...target,
|
|
6359
6715
|
requestId: row2.requestId
|
|
6360
6716
|
}), now, lifetimeMs: 6e4 });
|
|
6361
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6717
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
|
|
6362
6718
|
const tier = row2.tier, quote = row2.quote;
|
|
6363
6719
|
const offering = offer.contract.offering;
|
|
6364
|
-
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return
|
|
6720
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
|
|
6365
6721
|
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
|
|
6722
|
+
return fail7("membership_catalog_mismatch");
|
|
6367
6723
|
}
|
|
6368
6724
|
const expected = await createPaymentQuote({
|
|
6369
6725
|
...target,
|
|
@@ -6376,7 +6732,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
6376
6732
|
seatOffer: quote.seatOffer,
|
|
6377
6733
|
namedSeat: quote.namedSeat
|
|
6378
6734
|
});
|
|
6379
|
-
if (quote.digest !== expected.digest) return
|
|
6735
|
+
if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
|
|
6380
6736
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
6381
6737
|
}
|
|
6382
6738
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -6384,9 +6740,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
6384
6740
|
}
|
|
6385
6741
|
async function prepareMembershipQuote(db, input, issue) {
|
|
6386
6742
|
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
|
|
6743
|
+
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
6744
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6389
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6745
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
|
|
6390
6746
|
if (row2?.status === "ready") {
|
|
6391
6747
|
try {
|
|
6392
6748
|
return await restore2(row2, input, now);
|
|
@@ -6431,7 +6787,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6431
6787
|
}
|
|
6432
6788
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6433
6789
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6434
|
-
return
|
|
6790
|
+
return fail7("membership_quote_pending");
|
|
6435
6791
|
}
|
|
6436
6792
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6437
6793
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6480,7 +6836,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6480
6836
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6481
6837
|
}
|
|
6482
6838
|
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6483
|
-
if (!input.enabled) return
|
|
6839
|
+
if (!input.enabled) return fail7("named_seat_purchase_unavailable");
|
|
6484
6840
|
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6485
6841
|
const current = await restore2(row2, target, input.now);
|
|
6486
6842
|
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
@@ -6536,17 +6892,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6536
6892
|
if (!reader) return "";
|
|
6537
6893
|
const decoder = new TextDecoder();
|
|
6538
6894
|
let bytes = 0;
|
|
6539
|
-
let
|
|
6895
|
+
let text9 = "";
|
|
6540
6896
|
for (; ; ) {
|
|
6541
6897
|
const { done, value } = await reader.read();
|
|
6542
6898
|
if (req.signal.aborted) return invalid2();
|
|
6543
6899
|
if (done) {
|
|
6544
6900
|
complete = true;
|
|
6545
|
-
return
|
|
6901
|
+
return text9 + decoder.decode();
|
|
6546
6902
|
}
|
|
6547
6903
|
bytes += value.byteLength;
|
|
6548
6904
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6549
|
-
|
|
6905
|
+
text9 += decoder.decode(value, { stream: true });
|
|
6550
6906
|
}
|
|
6551
6907
|
} catch {
|
|
6552
6908
|
return invalid2();
|
|
@@ -6650,9 +7006,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
6650
7006
|
let selection;
|
|
6651
7007
|
if (request?.body) {
|
|
6652
7008
|
try {
|
|
6653
|
-
const
|
|
6654
|
-
if (
|
|
6655
|
-
const body = JSON.parse(
|
|
7009
|
+
const text9 = await readBoundedRequestText(request, 2048);
|
|
7010
|
+
if (text9 instanceof Response) return text9;
|
|
7011
|
+
const body = JSON.parse(text9);
|
|
6656
7012
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6657
7013
|
selection = body.namedSeat;
|
|
6658
7014
|
} catch {
|
|
@@ -7307,10 +7663,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7307
7663
|
const db = ctx.makeDb(env);
|
|
7308
7664
|
const groupId = url.searchParams.get("group") ?? chapter.id;
|
|
7309
7665
|
if (groupId !== chapter.id) return json({ error: "not found" }, 404);
|
|
7310
|
-
const { groups } = await
|
|
7666
|
+
const [{ groups }, stripeKey, activeControl] = await Promise.all([
|
|
7667
|
+
db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
|
|
7668
|
+
getVaultSecret(db, "stripe_secret_key"),
|
|
7669
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
|
|
7670
|
+
]);
|
|
7311
7671
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7312
7672
|
if (!group) return json({ error: "not found" }, 404);
|
|
7313
|
-
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7314
7673
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
7315
7674
|
const offered = await loadOfferableTiers(
|
|
7316
7675
|
db,
|
|
@@ -7319,7 +7678,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7319
7678
|
);
|
|
7320
7679
|
const seats = /* @__PURE__ */ new Map();
|
|
7321
7680
|
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7322
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7323
7681
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7324
7682
|
return json({
|
|
7325
7683
|
...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 +7715,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7357
7715
|
parsed.tierId = named.invitation.skuId;
|
|
7358
7716
|
submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
|
|
7359
7717
|
}
|
|
7360
|
-
const eligibility
|
|
7718
|
+
const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
|
|
7719
|
+
named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
|
|
7720
|
+
chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
|
|
7721
|
+
typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
|
|
7722
|
+
db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
|
|
7723
|
+
]);
|
|
7361
7724
|
if (eligibility?.conflict) return eligibility.conflict;
|
|
7362
|
-
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7363
7725
|
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
7726
|
const now = Date.now();
|
|
7366
7727
|
const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
|
|
7367
7728
|
const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
|
|
@@ -7400,18 +7761,16 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7400
7761
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
7401
7762
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
7402
7763
|
}
|
|
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;
|
|
7764
|
+
if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
|
|
7406
7765
|
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
7407
|
-
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
7766
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
|
|
7408
7767
|
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
7409
7768
|
if (!state) return json({
|
|
7410
7769
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
7411
7770
|
code: "application_continuation_unavailable",
|
|
7412
7771
|
id: result.id
|
|
7413
7772
|
}, 503);
|
|
7414
|
-
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
7773
|
+
if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
|
|
7415
7774
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
7416
7775
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
7417
7776
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -7531,10 +7890,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7531
7890
|
errors.push(`${path} must be a string`);
|
|
7532
7891
|
return void 0;
|
|
7533
7892
|
}
|
|
7534
|
-
const
|
|
7535
|
-
if (required && !
|
|
7536
|
-
if (
|
|
7537
|
-
return
|
|
7893
|
+
const text9 = value.trim();
|
|
7894
|
+
if (required && !text9) errors.push(`${path} is required`);
|
|
7895
|
+
if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
7896
|
+
return text9;
|
|
7538
7897
|
}
|
|
7539
7898
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7540
7899
|
const errors = [];
|
|
@@ -7583,8 +7942,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
7583
7942
|
else {
|
|
7584
7943
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
7585
7944
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
7586
|
-
const
|
|
7587
|
-
return
|
|
7945
|
+
const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
7946
|
+
return text9 ? [text9] : [];
|
|
7588
7947
|
});
|
|
7589
7948
|
}
|
|
7590
7949
|
}
|
|
@@ -7678,7 +8037,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
7678
8037
|
function unknownKeys(value, allowed2, path, errors) {
|
|
7679
8038
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7680
8039
|
}
|
|
7681
|
-
function
|
|
8040
|
+
function text5(value, path, errors, max = 65536) {
|
|
7682
8041
|
if (typeof value !== "string" || !value.trim()) {
|
|
7683
8042
|
errors.push(`${path} is required`);
|
|
7684
8043
|
return "";
|
|
@@ -7701,11 +8060,11 @@ function tiersOf(value, errors) {
|
|
|
7701
8060
|
return [];
|
|
7702
8061
|
}
|
|
7703
8062
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7704
|
-
const id7 =
|
|
8063
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
7705
8064
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7706
8065
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7707
8066
|
seen.add(id7);
|
|
7708
|
-
const name =
|
|
8067
|
+
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
7709
8068
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7710
8069
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
7711
8070
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -7723,7 +8082,7 @@ function tiersOf(value, errors) {
|
|
|
7723
8082
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
7724
8083
|
errors.push(`${path}.interval must be year or month`);
|
|
7725
8084
|
}
|
|
7726
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8085
|
+
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7727
8086
|
return [{
|
|
7728
8087
|
id: id7,
|
|
7729
8088
|
name,
|
|
@@ -7781,7 +8140,7 @@ function policyOf(value, errors, tiers) {
|
|
|
7781
8140
|
const result = {};
|
|
7782
8141
|
for (const key3 of allowed2) {
|
|
7783
8142
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
7784
|
-
result[key3] =
|
|
8143
|
+
result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
|
|
7785
8144
|
}
|
|
7786
8145
|
const tierPolicies = {};
|
|
7787
8146
|
if (input.tierPolicies !== void 0) {
|
|
@@ -7796,8 +8155,8 @@ function policyOf(value, errors, tiers) {
|
|
|
7796
8155
|
}
|
|
7797
8156
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7798
8157
|
tierPolicies[id7] = {
|
|
7799
|
-
refundPolicyText:
|
|
7800
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8158
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8159
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7801
8160
|
};
|
|
7802
8161
|
}
|
|
7803
8162
|
}
|
|
@@ -7837,9 +8196,9 @@ function validateSignupControlDocument(input) {
|
|
|
7837
8196
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
7838
8197
|
const source = recordOf(root.source);
|
|
7839
8198
|
const target = recordOf(root.target);
|
|
7840
|
-
const appId =
|
|
7841
|
-
const chapterId =
|
|
7842
|
-
const environment =
|
|
8199
|
+
const appId = text5(source?.appId, "document.source.appId", errors, 63);
|
|
8200
|
+
const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8201
|
+
const environment = text5(target?.environment, "document.target.environment", errors, 63);
|
|
7843
8202
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7844
8203
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
7845
8204
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9351,8 +9710,8 @@ async function bookSlot(req, env, ctx) {
|
|
|
9351
9710
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
9352
9711
|
await db.transact([meetingOp, appOp]);
|
|
9353
9712
|
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 } });
|
|
9713
|
+
await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
|
|
9714
|
+
await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
|
|
9356
9715
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
9357
9716
|
}
|
|
9358
9717
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -9376,28 +9735,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
9376
9735
|
};
|
|
9377
9736
|
|
|
9378
9737
|
// src/payments-stripe.ts
|
|
9379
|
-
import { stripeRequest as
|
|
9738
|
+
import { stripeRequest as stripeRequest12, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
9380
9739
|
|
|
9381
9740
|
// src/payments-refund.ts
|
|
9382
|
-
import { stripeRequest as
|
|
9741
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
9383
9742
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9384
9743
|
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
9385
9744
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
9386
|
-
function
|
|
9745
|
+
function fail8(code = "refund_target_mismatch") {
|
|
9387
9746
|
throw Object.assign(new Error(code), { code });
|
|
9388
9747
|
}
|
|
9389
9748
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
9390
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9749
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
|
|
9391
9750
|
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))
|
|
9751
|
+
const result = await stripeRequest10({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9752
|
+
if (!result.ok) fail8("stripe_error");
|
|
9753
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
|
|
9395
9754
|
return result.body;
|
|
9396
9755
|
};
|
|
9397
9756
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9398
|
-
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId)
|
|
9757
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
|
|
9399
9758
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
9400
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9759
|
+
if (row(sub.metadata)[key3] !== value) fail8();
|
|
9401
9760
|
}
|
|
9402
9761
|
let cursor;
|
|
9403
9762
|
let earliest;
|
|
@@ -9410,66 +9769,66 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
9410
9769
|
limit: 100,
|
|
9411
9770
|
...cursor ? { starting_after: cursor } : {}
|
|
9412
9771
|
});
|
|
9413
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9772
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
|
|
9414
9773
|
for (const value of list.data) {
|
|
9415
9774
|
const invoice2 = row(value);
|
|
9416
9775
|
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)
|
|
9776
|
+
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
9777
|
seen.add(id3(invoice2));
|
|
9419
9778
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
9420
9779
|
}
|
|
9421
9780
|
if (!list.has_more) break;
|
|
9422
|
-
if (!list.data.length || page === 9)
|
|
9781
|
+
if (!list.data.length || page === 9) fail8("refund_history_incomplete");
|
|
9423
9782
|
cursor = id3(list.data[list.data.length - 1]);
|
|
9424
9783
|
}
|
|
9425
|
-
if (!earliest)
|
|
9784
|
+
if (!earliest) fail8("no_charge");
|
|
9426
9785
|
const invoice = earliest;
|
|
9427
|
-
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
9786
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
|
|
9428
9787
|
let chargeId = id3(invoice.charge);
|
|
9429
9788
|
let paymentIntentId = id3(invoice.payment_intent);
|
|
9430
9789
|
if (!chargeId && !paymentIntentId) {
|
|
9431
9790
|
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)
|
|
9791
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
|
|
9433
9792
|
const payment = row(payments.data[0]);
|
|
9434
|
-
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
9793
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
|
|
9435
9794
|
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
9436
9795
|
}
|
|
9437
9796
|
if (!chargeId) {
|
|
9438
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9797
|
+
if (!providerId2(paymentIntentId, "pi")) fail8();
|
|
9439
9798
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
9440
|
-
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
9799
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
|
|
9441
9800
|
chargeId = id3(payment.latest_charge);
|
|
9442
9801
|
}
|
|
9443
|
-
if (!providerId2(chargeId, "ch"))
|
|
9802
|
+
if (!providerId2(chargeId, "ch")) fail8();
|
|
9444
9803
|
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))
|
|
9804
|
+
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();
|
|
9805
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
|
|
9447
9806
|
if (charge.amount_refunded !== charge.amount) {
|
|
9448
9807
|
const refund = await call("POST", "/v1/refunds", {
|
|
9449
9808
|
charge: chargeId,
|
|
9450
9809
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
9451
9810
|
}, `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))
|
|
9811
|
+
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
9812
|
}
|
|
9454
9813
|
let subscriptionCanceled = sub.status === "canceled";
|
|
9455
9814
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
9456
9815
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
9457
|
-
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
9816
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
|
|
9458
9817
|
subscriptionCanceled = true;
|
|
9459
9818
|
}
|
|
9460
9819
|
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)
|
|
9820
|
+
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
9821
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
9463
9822
|
}
|
|
9464
9823
|
|
|
9465
9824
|
// src/subscription-payment-identity.ts
|
|
9466
|
-
import { stripeRequest as
|
|
9825
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
9467
9826
|
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
9468
9827
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
9469
9828
|
let paymentId = id4(invoice.payment_intent);
|
|
9470
9829
|
if (!paymentId) {
|
|
9471
9830
|
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9472
|
-
const result2 = await
|
|
9831
|
+
const result2 = await stripeRequest11({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
9473
9832
|
const list = result2.body;
|
|
9474
9833
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
9475
9834
|
const link = list.data[0];
|
|
@@ -9478,11 +9837,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9478
9837
|
}
|
|
9479
9838
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
9480
9839
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
9481
|
-
const result = await
|
|
9840
|
+
const result = await stripeRequest11({ secretKey }, "GET", path);
|
|
9482
9841
|
const payment = result.body;
|
|
9483
9842
|
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
9843
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
9485
|
-
const tagged = await
|
|
9844
|
+
const tagged = await stripeRequest11(
|
|
9486
9845
|
{ secretKey },
|
|
9487
9846
|
"POST",
|
|
9488
9847
|
path,
|
|
@@ -9494,7 +9853,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9494
9853
|
|
|
9495
9854
|
// src/payments-stripe.ts
|
|
9496
9855
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9497
|
-
return
|
|
9856
|
+
return stripeRequest12(
|
|
9498
9857
|
{ secretKey: sk },
|
|
9499
9858
|
method,
|
|
9500
9859
|
path,
|
|
@@ -9502,7 +9861,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9502
9861
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9503
9862
|
);
|
|
9504
9863
|
}
|
|
9505
|
-
function
|
|
9864
|
+
function fail9(op, r) {
|
|
9506
9865
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9507
9866
|
err.code = "stripe_error";
|
|
9508
9867
|
throw err;
|
|
@@ -9561,7 +9920,7 @@ function createStripeProvider(config) {
|
|
|
9561
9920
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9562
9921
|
`cus:${input.applicationId}`
|
|
9563
9922
|
);
|
|
9564
|
-
if (!cust.ok)
|
|
9923
|
+
if (!cust.ok) fail9("customer create", cust);
|
|
9565
9924
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9566
9925
|
customerId = cust.body.id;
|
|
9567
9926
|
}
|
|
@@ -9606,7 +9965,7 @@ function createStripeProvider(config) {
|
|
|
9606
9965
|
// retry between create and the db write can't orphan a second one.
|
|
9607
9966
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9608
9967
|
);
|
|
9609
|
-
if (!sub.ok)
|
|
9968
|
+
if (!sub.ok) fail9("subscription create", sub);
|
|
9610
9969
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9611
9970
|
const subscriptionId = sub.body.id;
|
|
9612
9971
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9616,14 +9975,14 @@ function createStripeProvider(config) {
|
|
|
9616
9975
|
"expand[2]": "discounts",
|
|
9617
9976
|
"expand[3]": "latest_invoice.discounts"
|
|
9618
9977
|
});
|
|
9619
|
-
if (!expanded.ok)
|
|
9978
|
+
if (!expanded.ok) fail9("subscription invoice evidence", expanded);
|
|
9620
9979
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9621
9980
|
sub = expanded;
|
|
9622
9981
|
}
|
|
9623
9982
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9624
9983
|
const invoice = sub.body.latest_invoice;
|
|
9625
9984
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9626
|
-
if (!clientSecret2)
|
|
9985
|
+
if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
|
|
9627
9986
|
await tagSubscriptionPayment(
|
|
9628
9987
|
sk,
|
|
9629
9988
|
invoice,
|
|
@@ -10110,7 +10469,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10110
10469
|
|
|
10111
10470
|
// src/worker-payment-receipt.ts
|
|
10112
10471
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10113
|
-
var escape = (
|
|
10472
|
+
var escape = (text9) => text9.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10114
10473
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10115
10474
|
const id7 = url.searchParams.get("application");
|
|
10116
10475
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10121,7 +10480,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10121
10480
|
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
10481
|
const receipt = intent.paymentReceipt;
|
|
10123
10482
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10124
|
-
const
|
|
10483
|
+
const text9 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10125
10484
|
if (req.method === "POST") {
|
|
10126
10485
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10127
10486
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10139,7 +10498,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10139
10498
|
template: "paymentReceipt",
|
|
10140
10499
|
to: String(intent.purchaserEmailNormalized),
|
|
10141
10500
|
applicationId: id7,
|
|
10142
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10501
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text9 },
|
|
10143
10502
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10144
10503
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10145
10504
|
});
|
|
@@ -10149,7 +10508,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10149
10508
|
const nonce = crypto.randomUUID();
|
|
10150
10509
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10151
10510
|
<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(
|
|
10511
|
+
<h1>Payment receipt</h1><pre>${escape(text9)}</pre><button id="print">Print / save PDF</button>
|
|
10153
10512
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10154
10513
|
headers: {
|
|
10155
10514
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10619,16 +10978,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
10619
10978
|
};
|
|
10620
10979
|
|
|
10621
10980
|
// src/named-seat-accepted-notice.ts
|
|
10622
|
-
var
|
|
10981
|
+
var text6 = (value) => typeof value === "string" ? value : "";
|
|
10623
10982
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
10624
10983
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
10625
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
10984
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text6(purchaser.email)) {
|
|
10626
10985
|
return { sent: false, reason: "purchaser-missing" };
|
|
10627
10986
|
}
|
|
10628
10987
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
10629
10988
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
10630
10989
|
const config = emailGroupFrom(group);
|
|
10631
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
10990
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text6).join(" ").replace(/\s+/g, " ").trim();
|
|
10632
10991
|
return sendTemplated({
|
|
10633
10992
|
db,
|
|
10634
10993
|
envName: env.ODLA_ENV,
|
|
@@ -10640,12 +10999,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
10640
10999
|
}, {
|
|
10641
11000
|
group: config,
|
|
10642
11001
|
template: "seatAccepted",
|
|
10643
|
-
to:
|
|
11002
|
+
to: text6(purchaser.email),
|
|
10644
11003
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
10645
11004
|
applicationId: input.primaryApplicationId,
|
|
10646
11005
|
tierId: input.skuId,
|
|
10647
11006
|
vars: {
|
|
10648
|
-
firstName:
|
|
11007
|
+
firstName: text6(purchaser.firstName),
|
|
10649
11008
|
purchaserName,
|
|
10650
11009
|
recipientName: input.recipientName,
|
|
10651
11010
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -11089,7 +11448,7 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
|
11089
11448
|
const checked = validateScheduling({ ...stored, ...body });
|
|
11090
11449
|
if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
|
|
11091
11450
|
try {
|
|
11092
|
-
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
|
|
11451
|
+
await db.transact([{ t: "update", ns: "groups", id: { ns: "groups", attr: "id", value: String(group.id) }, attrs: { schedulingJson: checked.value } }]);
|
|
11093
11452
|
} catch (error) {
|
|
11094
11453
|
const code = error.code;
|
|
11095
11454
|
console.error("admin_scheduling_write_failed", { code: typeof code === "string" ? code : "unknown" });
|
|
@@ -12112,12 +12471,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12112
12471
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12113
12472
|
const t = value;
|
|
12114
12473
|
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:
|
|
12474
|
+
const subject = str2(t.subject).trim(), text9 = str2(t.text);
|
|
12475
|
+
const issues = validateEmailCopy4({ subject, text: text9 }, { allowedVariables: events[key3].variables });
|
|
12117
12476
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12118
12477
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12119
12478
|
}
|
|
12120
|
-
clean[key3] = { subject, text:
|
|
12479
|
+
clean[key3] = { subject, text: text9, enabled: t.enabled };
|
|
12121
12480
|
}
|
|
12122
12481
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12123
12482
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -14330,9 +14689,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14330
14689
|
if (req.method === "POST") {
|
|
14331
14690
|
let body;
|
|
14332
14691
|
try {
|
|
14333
|
-
const
|
|
14334
|
-
if (new TextEncoder().encode(
|
|
14335
|
-
const parsed = JSON.parse(
|
|
14692
|
+
const text9 = await req.text();
|
|
14693
|
+
if (new TextEncoder().encode(text9).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14694
|
+
const parsed = JSON.parse(text9);
|
|
14336
14695
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14337
14696
|
body = parsed;
|
|
14338
14697
|
} catch {
|
|
@@ -14501,7 +14860,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
|
|
|
14501
14860
|
// src/network-commercial-parity.ts
|
|
14502
14861
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14503
14862
|
var MAX_RECORDS = 500;
|
|
14504
|
-
var
|
|
14863
|
+
var text7 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14505
14864
|
function parityMessage(partition, kind, value) {
|
|
14506
14865
|
return [
|
|
14507
14866
|
"chapter-commercial-parity",
|
|
@@ -14533,18 +14892,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14533
14892
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14534
14893
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14535
14894
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14536
|
-
const eligible = input.rows.filter((row2) =>
|
|
14895
|
+
const eligible = input.rows.filter((row2) => text7(row2.id) && (text7(row2.stripeCustomerId) || text7(row2.stripeSubscriptionId)));
|
|
14537
14896
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14538
14897
|
const key3 = await parityKey(input.secret);
|
|
14539
14898
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14540
|
-
const applicationId =
|
|
14541
|
-
const customerId =
|
|
14542
|
-
const subscriptionId =
|
|
14899
|
+
const applicationId = text7(row2.id);
|
|
14900
|
+
const customerId = text7(row2.stripeCustomerId);
|
|
14901
|
+
const subscriptionId = text7(row2.stripeSubscriptionId);
|
|
14543
14902
|
return {
|
|
14544
14903
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14545
14904
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14546
14905
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14547
|
-
status:
|
|
14906
|
+
status: text7(row2.status, 64),
|
|
14548
14907
|
canceled: row2.canceled === true
|
|
14549
14908
|
};
|
|
14550
14909
|
}));
|
|
@@ -14570,7 +14929,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14570
14929
|
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
14930
|
const seen = /* @__PURE__ */ new Set();
|
|
14572
14931
|
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 &&
|
|
14932
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text7(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
14574
14933
|
const key3 = recordKey(record4);
|
|
14575
14934
|
if (seen.has(key3)) return false;
|
|
14576
14935
|
seen.add(key3);
|
|
@@ -14581,16 +14940,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14581
14940
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14582
14941
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14583
14942
|
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 =
|
|
14943
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text7(row2.applicationId) && text7(row2.objectId)).map(async (row2) => {
|
|
14944
|
+
const customerId = text7(row2.customerId);
|
|
14945
|
+
const subscriptionId = text7(row2.subscriptionId) ?? text7(row2.objectId);
|
|
14587
14946
|
return {
|
|
14588
14947
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
14589
14948
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
14590
14949
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
14591
14950
|
status: null,
|
|
14592
14951
|
canceled: false,
|
|
14593
|
-
stripeStatus:
|
|
14952
|
+
stripeStatus: text7(row2.status, 64),
|
|
14594
14953
|
stripeCanceled: row2.status === "canceled"
|
|
14595
14954
|
};
|
|
14596
14955
|
}));
|
|
@@ -14787,6 +15146,34 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
|
14787
15146
|
};
|
|
14788
15147
|
}
|
|
14789
15148
|
|
|
15149
|
+
// src/worker-network-commercial-dispute.ts
|
|
15150
|
+
async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
15151
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
15152
|
+
if (!operator || !fact.chargeId) return { disposition: "unhosted" };
|
|
15153
|
+
let db;
|
|
15154
|
+
try {
|
|
15155
|
+
db = operator.makeDb(env);
|
|
15156
|
+
} catch {
|
|
15157
|
+
return { disposition: "unhosted" };
|
|
15158
|
+
}
|
|
15159
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15160
|
+
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15161
|
+
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15162
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15163
|
+
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15164
|
+
if (!member) return { disposition: "not_applicable" };
|
|
15165
|
+
const target = {
|
|
15166
|
+
appId: String(member.appId),
|
|
15167
|
+
chapterId: String(member.chapterId),
|
|
15168
|
+
environment: String(member.environment),
|
|
15169
|
+
runtime: String(member.runtime),
|
|
15170
|
+
applicationId: String(member.applicationId),
|
|
15171
|
+
skuId: String(member.skuId)
|
|
15172
|
+
};
|
|
15173
|
+
const receipt = await reconcileMembershipDispute(db, { ...target, eventId: fact.eventId }, now, secret, charge);
|
|
15174
|
+
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15175
|
+
}
|
|
15176
|
+
|
|
14790
15177
|
// src/worker-routes-network-commercial.ts
|
|
14791
15178
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
14792
15179
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -14851,6 +15238,12 @@ function safeProjection(projection) {
|
|
|
14851
15238
|
amountReceivedCents: seat.amountReceivedCents,
|
|
14852
15239
|
refundedCents: seat.refundedCents ?? 0
|
|
14853
15240
|
})),
|
|
15241
|
+
disputes: (projection.disputes ?? []).map((dispute) => ({
|
|
15242
|
+
...row2(dispute),
|
|
15243
|
+
chargeId: dispute.chargeId ?? null,
|
|
15244
|
+
paymentIntentId: dispute.paymentIntentId ?? null,
|
|
15245
|
+
reason: dispute.disputeReason ?? null
|
|
15246
|
+
})),
|
|
14854
15247
|
exceptions: projection.exceptions
|
|
14855
15248
|
};
|
|
14856
15249
|
}
|
|
@@ -14883,10 +15276,20 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14883
15276
|
if (result.status === "collision") {
|
|
14884
15277
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
14885
15278
|
}
|
|
15279
|
+
let dispute;
|
|
15280
|
+
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15281
|
+
try {
|
|
15282
|
+
dispute = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15283
|
+
} catch (error) {
|
|
15284
|
+
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15285
|
+
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
15286
|
+
}
|
|
15287
|
+
}
|
|
14886
15288
|
return json({
|
|
14887
15289
|
ok: true,
|
|
14888
15290
|
status: result.status,
|
|
14889
15291
|
eventId: fact.eventId,
|
|
15292
|
+
...dispute ? { dispute: dispute.disposition } : {},
|
|
14890
15293
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14891
15294
|
});
|
|
14892
15295
|
} catch (error) {
|
|
@@ -15084,7 +15487,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15084
15487
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15085
15488
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15086
15489
|
}
|
|
15087
|
-
function
|
|
15490
|
+
function text8(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15088
15491
|
if (value === void 0 && !required) return void 0;
|
|
15089
15492
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15090
15493
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15110,7 +15513,7 @@ function scopeValue(value, path, errors) {
|
|
|
15110
15513
|
return { kind: raw2.kind };
|
|
15111
15514
|
}
|
|
15112
15515
|
if (raw2.kind === "chapter") {
|
|
15113
|
-
const chapterId =
|
|
15516
|
+
const chapterId = text8(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15114
15517
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15115
15518
|
return { kind: "chapter", chapterId };
|
|
15116
15519
|
}
|
|
@@ -15124,22 +15527,22 @@ function resourceValue(value, path, errors) {
|
|
|
15124
15527
|
return null;
|
|
15125
15528
|
}
|
|
15126
15529
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15127
|
-
const id7 =
|
|
15530
|
+
const id7 = text8(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15128
15531
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15129
|
-
const kind =
|
|
15532
|
+
const kind = text8(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15130
15533
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15131
|
-
const title =
|
|
15132
|
-
const summary =
|
|
15133
|
-
const status2 =
|
|
15534
|
+
const title = text8(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15535
|
+
const summary = text8(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15536
|
+
const status2 = text8(raw2.status, `${path}.status`, errors) ?? "";
|
|
15134
15537
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15135
15538
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15136
15539
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15137
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15540
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text8(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15138
15541
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15139
15542
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15140
15543
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15141
|
-
const bodyMarkdown =
|
|
15142
|
-
const url =
|
|
15544
|
+
const bodyMarkdown = text8(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15545
|
+
const url = text8(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15143
15546
|
if (url) {
|
|
15144
15547
|
try {
|
|
15145
15548
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15151,9 +15554,9 @@ function resourceValue(value, path, errors) {
|
|
|
15151
15554
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15152
15555
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15153
15556
|
const brandAsset = asset ? {
|
|
15154
|
-
bookId:
|
|
15155
|
-
assetId:
|
|
15156
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15557
|
+
bookId: text8(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15558
|
+
assetId: text8(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15559
|
+
...asset.filename === void 0 ? {} : { filename: text8(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15157
15560
|
} : void 0;
|
|
15158
15561
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15159
15562
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15168,17 +15571,17 @@ function resourceValue(value, path, errors) {
|
|
|
15168
15571
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15169
15572
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15170
15573
|
const promotedFrom = promotedRaw ? {
|
|
15171
|
-
chapterId:
|
|
15172
|
-
resourceId:
|
|
15574
|
+
chapterId: text8(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15575
|
+
resourceId: text8(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15173
15576
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15174
15577
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15175
|
-
promotedBy:
|
|
15578
|
+
promotedBy: text8(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15176
15579
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15177
15580
|
} : void 0;
|
|
15178
15581
|
const provenance2 = {
|
|
15179
|
-
createdBy:
|
|
15582
|
+
createdBy: text8(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15180
15583
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15181
|
-
updatedBy:
|
|
15584
|
+
updatedBy: text8(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15182
15585
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15183
15586
|
...promotedFrom ? { promotedFrom } : {}
|
|
15184
15587
|
};
|
|
@@ -15197,7 +15600,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15197
15600
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15198
15601
|
const sourceType = sourceRaw?.type;
|
|
15199
15602
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15200
|
-
const sourceAppId =
|
|
15603
|
+
const sourceAppId = text8(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15201
15604
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15202
15605
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15203
15606
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15205,8 +15608,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15205
15608
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15206
15609
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15207
15610
|
const target = targetRaw ? {
|
|
15208
|
-
appId:
|
|
15209
|
-
environment:
|
|
15611
|
+
appId: text8(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15612
|
+
environment: text8(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15210
15613
|
} : void 0;
|
|
15211
15614
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15212
15615
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15547,7 +15950,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
15547
15950
|
};
|
|
15548
15951
|
|
|
15549
15952
|
// src/membership-cancel-authority.ts
|
|
15550
|
-
import { stripeRequest as
|
|
15953
|
+
import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15551
15954
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
15552
15955
|
membershipClock(now);
|
|
15553
15956
|
const id7 = await networkMembershipId(target);
|
|
@@ -15558,14 +15961,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
15558
15961
|
membershipFail("membership_cancellation_unavailable");
|
|
15559
15962
|
}
|
|
15560
15963
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
15561
|
-
const read2 = await
|
|
15964
|
+
const read2 = await stripeRequest13({ secretKey: stripeSecret }, "GET", path);
|
|
15562
15965
|
const sub = read2.body;
|
|
15563
15966
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
15564
15967
|
...stripePartitionMetadata(target),
|
|
15565
15968
|
applicationId: target.applicationId
|
|
15566
15969
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
15567
15970
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
15568
|
-
const write = await
|
|
15971
|
+
const write = await stripeRequest13(
|
|
15569
15972
|
{ secretKey: stripeSecret },
|
|
15570
15973
|
"POST",
|
|
15571
15974
|
path,
|
|
@@ -15787,7 +16190,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
15787
16190
|
}
|
|
15788
16191
|
|
|
15789
16192
|
// src/membership-provider-lifecycle.ts
|
|
15790
|
-
import { stripeRequest as
|
|
16193
|
+
import { stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
|
|
15791
16194
|
|
|
15792
16195
|
// src/membership-renewal-authority.ts
|
|
15793
16196
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -15909,6 +16312,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15909
16312
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
15910
16313
|
const fact = row2?.fact;
|
|
15911
16314
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
16315
|
+
if (fact?.objectKind === "dispute") {
|
|
16316
|
+
const secret = await getVaultSecret(db, stripeSecretName);
|
|
16317
|
+
if (!secret) membershipFail("membership_billing_provider_unavailable");
|
|
16318
|
+
return reconcileMembershipDispute(db, input, now, secret);
|
|
16319
|
+
}
|
|
15912
16320
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15913
16321
|
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
16322
|
appId: input.appId,
|
|
@@ -15951,7 +16359,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15951
16359
|
}
|
|
15952
16360
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15953
16361
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15954
|
-
const result = await
|
|
16362
|
+
const result = await stripeRequest14({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15955
16363
|
const sub = result.body;
|
|
15956
16364
|
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
16365
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15989,7 +16397,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15989
16397
|
}
|
|
15990
16398
|
|
|
15991
16399
|
// src/membership-seat-stripe.ts
|
|
15992
|
-
import { stripeRequest as
|
|
16400
|
+
import { stripeRequest as stripeRequest15 } from "@odla-ai/stripe";
|
|
15993
16401
|
var reject7 = () => {
|
|
15994
16402
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15995
16403
|
};
|
|
@@ -16001,7 +16409,7 @@ var metadataFor = (seat) => ({
|
|
|
16001
16409
|
});
|
|
16002
16410
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
16003
16411
|
async function read(sk, path, params) {
|
|
16004
|
-
const response = await
|
|
16412
|
+
const response = await stripeRequest15({ secretKey: sk }, "GET", path, params);
|
|
16005
16413
|
if (!response.ok) return reject7();
|
|
16006
16414
|
return response.body;
|
|
16007
16415
|
}
|
|
@@ -16038,11 +16446,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
16038
16446
|
}
|
|
16039
16447
|
await subscriptionForSeat(sk, seat);
|
|
16040
16448
|
const metadata = metadataFor(seat);
|
|
16041
|
-
const result = seat.providerPaymentIntentId ? await
|
|
16449
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest15(
|
|
16042
16450
|
{ secretKey: sk },
|
|
16043
16451
|
"GET",
|
|
16044
16452
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
16045
|
-
) : await
|
|
16453
|
+
) : await stripeRequest15({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
16046
16454
|
amount: seat.amountCents,
|
|
16047
16455
|
currency: seat.currency,
|
|
16048
16456
|
customer: seat.providerCustomerId,
|
|
@@ -16064,7 +16472,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
16064
16472
|
return String(existing.id);
|
|
16065
16473
|
}
|
|
16066
16474
|
if (seat.combinedInvoice) reject7();
|
|
16067
|
-
const result = await
|
|
16475
|
+
const result = await stripeRequest15({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
16068
16476
|
subscription: seat.providerSubscriptionId,
|
|
16069
16477
|
price: seat.priceId,
|
|
16070
16478
|
quantity: 1,
|
|
@@ -16765,7 +17173,20 @@ var membershipAuthorityEntities = {
|
|
|
16765
17173
|
graceEndsAt: attr("number", true),
|
|
16766
17174
|
...times(),
|
|
16767
17175
|
refundRequestedAt: attr("number", true),
|
|
16768
|
-
restartId: key(true)
|
|
17176
|
+
restartId: key(true),
|
|
17177
|
+
// A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
|
|
17178
|
+
// the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
|
|
17179
|
+
disputeId: key(true),
|
|
17180
|
+
disputeStatus: attr("string", true),
|
|
17181
|
+
disputeReason: attr("string", true),
|
|
17182
|
+
disputeAmountCents: attr("number", true),
|
|
17183
|
+
disputedAt: attr("number", true),
|
|
17184
|
+
disputeClosedAt: attr("number", true),
|
|
17185
|
+
disputePriorStatus: attr("string", true),
|
|
17186
|
+
disputeEventId: key(true),
|
|
17187
|
+
rechargePaymentIntentId: key(true),
|
|
17188
|
+
rechargedAt: attr("number", true),
|
|
17189
|
+
rechargeAttempt: attr("json", true)
|
|
16769
17190
|
} },
|
|
16770
17191
|
membershipRestart: { attrs: {
|
|
16771
17192
|
id: id5(),
|
|
@@ -16962,7 +17383,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16962
17383
|
}
|
|
16963
17384
|
|
|
16964
17385
|
// src/membership-seat-cancel.ts
|
|
16965
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
17386
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest16 } from "@odla-ai/stripe";
|
|
16966
17387
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16967
17388
|
membershipClock(now);
|
|
16968
17389
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16990,7 +17411,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16990
17411
|
}
|
|
16991
17412
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16992
17413
|
const request = async (method, path, params) => {
|
|
16993
|
-
const result = await
|
|
17414
|
+
const result = await stripeRequest16(
|
|
16994
17415
|
{ secretKey },
|
|
16995
17416
|
method,
|
|
16996
17417
|
path,
|
|
@@ -17178,7 +17599,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17178
17599
|
};
|
|
17179
17600
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17180
17601
|
let size = 0;
|
|
17181
|
-
let
|
|
17602
|
+
let text9 = "";
|
|
17182
17603
|
const decoder = new TextDecoder();
|
|
17183
17604
|
try {
|
|
17184
17605
|
while (true) {
|
|
@@ -17188,10 +17609,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17188
17609
|
if (done) break;
|
|
17189
17610
|
size += value.byteLength;
|
|
17190
17611
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17191
|
-
|
|
17612
|
+
text9 += decoder.decode(value, { stream: true });
|
|
17192
17613
|
}
|
|
17193
17614
|
try {
|
|
17194
|
-
return JSON.parse(
|
|
17615
|
+
return JSON.parse(text9 + decoder.decode());
|
|
17195
17616
|
} catch {
|
|
17196
17617
|
throw new TypeError(`invalid ${label}`);
|
|
17197
17618
|
}
|
|
@@ -17526,7 +17947,7 @@ async function requestMembershipRefund(options) {
|
|
|
17526
17947
|
async function requestMembershipRenewal(options) {
|
|
17527
17948
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
17528
17949
|
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)) {
|
|
17950
|
+
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
17951
|
throw new Error("invalid membership renewal receipt");
|
|
17531
17952
|
}
|
|
17532
17953
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -17878,6 +18299,7 @@ var BUILTIN_ROUTES = [
|
|
|
17878
18299
|
handlePaymentRecovery,
|
|
17879
18300
|
handleAdminNamedSeat,
|
|
17880
18301
|
handleAdminMembershipSeats,
|
|
18302
|
+
handleAdminMembershipDisputes,
|
|
17881
18303
|
handleAdminMembershipPolicyVersions,
|
|
17882
18304
|
handleAdminMembershipReterm,
|
|
17883
18305
|
handleAdminMeetings,
|
|
@@ -17926,8 +18348,9 @@ function chapterWorker(options) {
|
|
|
17926
18348
|
const ctx = createWorkerContext(options);
|
|
17927
18349
|
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
17928
18350
|
return {
|
|
17929
|
-
async fetch(req, env) {
|
|
18351
|
+
async fetch(req, env, execution) {
|
|
17930
18352
|
const url = new URL(req.url);
|
|
18353
|
+
ctx.bindExecution(req, execution);
|
|
17931
18354
|
for (const route of routes) {
|
|
17932
18355
|
let res;
|
|
17933
18356
|
try {
|
|
@@ -17949,6 +18372,7 @@ export {
|
|
|
17949
18372
|
DECLINABLE_FROM,
|
|
17950
18373
|
MEMBERSHIP_AUTHORITY_PATH,
|
|
17951
18374
|
MEMBERSHIP_CHAPTER_EFFECT_KINDS,
|
|
18375
|
+
MEMBERSHIP_DISPUTED_STATUS,
|
|
17952
18376
|
NetworkTargetTransportError,
|
|
17953
18377
|
REGIFTABLE_SEAT_STATUSES,
|
|
17954
18378
|
chapterMembershipAccess,
|
|
@@ -17974,7 +18398,10 @@ export {
|
|
|
17974
18398
|
namedSeatInvitationOpen,
|
|
17975
18399
|
namedSeatRecipientState,
|
|
17976
18400
|
namedSeatRegiftable,
|
|
18401
|
+
readDisputedCharge,
|
|
17977
18402
|
readNamedSeatOffer,
|
|
18403
|
+
rechargeDisputedMembership,
|
|
18404
|
+
reconcileMembershipDispute,
|
|
17978
18405
|
refundNamedSeatByOperator,
|
|
17979
18406
|
regiftNamedSeat,
|
|
17980
18407
|
releaseNamedSeatRecipientApplications,
|