@odla-ai/chapter 0.59.0 → 0.60.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 +31 -2
- package/dist/{chunk-Y2H4VURI.js → chunk-D5UR5H6R.js} +4 -3
- package/dist/chunk-D5UR5H6R.js.map +1 -0
- package/dist/{chunk-WFTWT3V7.js → chunk-J7ES2OWN.js} +22 -6
- package/dist/{chunk-WFTWT3V7.js.map → chunk-J7ES2OWN.js.map} +1 -1
- package/dist/{chunk-GADLTNJF.js → chunk-QNP3UEIL.js} +2 -1
- package/dist/chunk-QNP3UEIL.js.map +1 -0
- package/dist/{copy-context-BuJBQafl.d.ts → copy-context-BP3G1iBd.d.ts} +1 -1
- package/dist/index.cjs +302 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.js +302 -98
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +6 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +3 -3
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +407 -186
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +123 -15
- package/dist/worker/index.d.ts +123 -15
- package/dist/worker/index.js +407 -186
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GADLTNJF.js.map +0 -1
- package/dist/chunk-Y2H4VURI.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -1633,9 +1633,9 @@ async function backfillCrm(deps) {
|
|
|
1633
1633
|
}
|
|
1634
1634
|
|
|
1635
1635
|
// src/email-retirement.ts
|
|
1636
|
-
function isRetiredEmailCopy(subject,
|
|
1636
|
+
function isRetiredEmailCopy(subject, text10) {
|
|
1637
1637
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1638
|
-
const bodyCopy =
|
|
1638
|
+
const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1639
1639
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1640
1640
|
}
|
|
1641
1641
|
|
|
@@ -1713,10 +1713,51 @@ function planDelivery(input) {
|
|
|
1713
1713
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1714
1714
|
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1715
1715
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1716
|
-
const
|
|
1716
|
+
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1717
1717
|
|
|
1718
1718
|
` + renderedText : renderedText;
|
|
1719
|
-
return { deliver: true, transport, to, subject, text:
|
|
1719
|
+
return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// src/email-html.ts
|
|
1723
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1724
|
+
function escapeHtml(s) {
|
|
1725
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1726
|
+
}
|
|
1727
|
+
function lineHtml(line) {
|
|
1728
|
+
let out = "", last = 0;
|
|
1729
|
+
for (const match of line.matchAll(URL_RE)) {
|
|
1730
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
1731
|
+
const url = escapeHtml(match[0]);
|
|
1732
|
+
out += `<a href="${url}">${url}</a>`;
|
|
1733
|
+
last = match.index + match[0].length;
|
|
1734
|
+
}
|
|
1735
|
+
return out + escapeHtml(line.slice(last));
|
|
1736
|
+
}
|
|
1737
|
+
function textToHtml(text10) {
|
|
1738
|
+
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1739
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1740
|
+
return `<!doctype html>
|
|
1741
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1742
|
+
<body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
|
|
1743
|
+
${body}
|
|
1744
|
+
</body></html>`;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/email-deliverability.ts
|
|
1748
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
1749
|
+
function isRecipientFacing(event2) {
|
|
1750
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
1751
|
+
}
|
|
1752
|
+
function listUnsubscribeHeader(replyTo) {
|
|
1753
|
+
const address = replyTo.trim();
|
|
1754
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
1755
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
1756
|
+
}
|
|
1757
|
+
function deliverabilityHeaders(input) {
|
|
1758
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
1759
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
1760
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
1720
1761
|
}
|
|
1721
1762
|
|
|
1722
1763
|
// src/notify.ts
|
|
@@ -1864,8 +1905,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1864
1905
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1865
1906
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1866
1907
|
function applicantVariables(app) {
|
|
1867
|
-
const
|
|
1868
|
-
return { firstName:
|
|
1908
|
+
const text10 = (v) => typeof v === "string" ? v : "";
|
|
1909
|
+
return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
|
|
1869
1910
|
}
|
|
1870
1911
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1871
1912
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -1990,11 +2031,16 @@ async function sendTemplated(deps, input) {
|
|
|
1990
2031
|
if (decision.transport === "log-only") return { sent: true };
|
|
1991
2032
|
let messageId;
|
|
1992
2033
|
try {
|
|
2034
|
+
const text10 = String(row2.body);
|
|
2035
|
+
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
|
|
2036
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
1993
2037
|
const response = await deps.sender.send({
|
|
1994
2038
|
from: fromAddress(deps.from, input.group.fromName),
|
|
1995
2039
|
to: [decision.to],
|
|
1996
2040
|
subject: String(row2.subject),
|
|
1997
|
-
text:
|
|
2041
|
+
text: text10,
|
|
2042
|
+
...html ? { html } : {},
|
|
2043
|
+
...headers ? { headers } : {},
|
|
1998
2044
|
replyTo: input.group.replyTo
|
|
1999
2045
|
});
|
|
2000
2046
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
@@ -3599,12 +3645,87 @@ function paymentLineItems(group, tier) {
|
|
|
3599
3645
|
|
|
3600
3646
|
// src/membership-dispute-authority.ts
|
|
3601
3647
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3648
|
+
|
|
3649
|
+
// src/membership-dispute-transition.ts
|
|
3602
3650
|
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3603
3651
|
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3604
3652
|
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3605
|
-
|
|
3653
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
3654
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3655
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
3656
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
3657
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3658
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
3659
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
3660
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3661
|
+
return {
|
|
3662
|
+
status: next,
|
|
3663
|
+
disputeId: fact.objectId,
|
|
3664
|
+
disputeStatus: status2,
|
|
3665
|
+
disputeEventId: eventId,
|
|
3666
|
+
disputeClosedAt: closed ? now : 0,
|
|
3667
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3668
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3669
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3670
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
3671
|
+
revision: Number(row2.revision) + 1,
|
|
3672
|
+
updatedAt: now
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/membership-seat-dispute.ts
|
|
3606
3677
|
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3607
|
-
var
|
|
3678
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
3679
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
|
|
3680
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
3681
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
3682
|
+
}
|
|
3683
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
3684
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
3685
|
+
const membership = async (applied, seat) => {
|
|
3686
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3687
|
+
requireMembershipScope(member, input);
|
|
3688
|
+
return {
|
|
3689
|
+
membershipId: id7,
|
|
3690
|
+
status: String(member.status),
|
|
3691
|
+
termEndsAt: Number(member.termEndsAt),
|
|
3692
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
3693
|
+
autoRenew: member.autoRenew === true,
|
|
3694
|
+
applied,
|
|
3695
|
+
...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
|
|
3696
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
3697
|
+
};
|
|
3698
|
+
};
|
|
3699
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3700
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
3701
|
+
if (!seat) return membership(false);
|
|
3702
|
+
requireMembershipScope(seat, input);
|
|
3703
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3704
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
3705
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
3706
|
+
if (!attrs) return membership(false, seat);
|
|
3707
|
+
try {
|
|
3708
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
3709
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
3710
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3711
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
3712
|
+
evidence.guard
|
|
3713
|
+
] });
|
|
3714
|
+
} catch (error) {
|
|
3715
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
3719
|
+
requireMembershipScope(saved, input);
|
|
3720
|
+
return membership(true, saved);
|
|
3721
|
+
}
|
|
3722
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
// src/membership-dispute-authority.ts
|
|
3726
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3727
|
+
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3728
|
+
var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
|
|
3608
3729
|
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3609
3730
|
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3610
3731
|
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
@@ -3618,7 +3739,7 @@ async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
|
3618
3739
|
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3619
3740
|
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3620
3741
|
}
|
|
3621
|
-
const seatId =
|
|
3742
|
+
const seatId = text3(charge.body.metadata?.seatId);
|
|
3622
3743
|
return {
|
|
3623
3744
|
chargeId,
|
|
3624
3745
|
customerId,
|
|
@@ -3644,37 +3765,23 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
3644
3765
|
termEndsAt: Number(value.termEndsAt),
|
|
3645
3766
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3646
3767
|
autoRenew: value.autoRenew === true,
|
|
3647
|
-
...
|
|
3768
|
+
...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
|
|
3648
3769
|
applied
|
|
3649
3770
|
});
|
|
3650
3771
|
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3651
3772
|
requireMembershipScope(known, input);
|
|
3652
3773
|
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3774
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
3775
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
3653
3776
|
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3777
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3778
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
3654
3779
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3655
3780
|
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3656
3781
|
requireMembershipScope(member, input);
|
|
3657
|
-
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3658
3782
|
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3659
|
-
const
|
|
3660
|
-
|
|
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
|
-
};
|
|
3783
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
3784
|
+
if (!attrs) return receipt(member, false);
|
|
3678
3785
|
try {
|
|
3679
3786
|
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3680
3787
|
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
@@ -4796,6 +4903,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4796
4903
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4797
4904
|
case "canceled":
|
|
4798
4905
|
return "canceled";
|
|
4906
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4907
|
+
case "payment_disputed":
|
|
4908
|
+
return "disputed";
|
|
4799
4909
|
default:
|
|
4800
4910
|
return "payment_pending";
|
|
4801
4911
|
}
|
|
@@ -5124,6 +5234,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5124
5234
|
|
|
5125
5235
|
// src/membership-dispute-recharge.ts
|
|
5126
5236
|
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
5237
|
+
var NS = { membership: "networkMembership", seat: "networkSeat" };
|
|
5127
5238
|
var fail6 = (code, extra = {}) => {
|
|
5128
5239
|
throw Object.assign(new Error(code), { code, ...extra });
|
|
5129
5240
|
};
|
|
@@ -5137,65 +5248,64 @@ async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
|
5137
5248
|
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5138
5249
|
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5139
5250
|
}
|
|
5140
|
-
async function
|
|
5251
|
+
async function rechargeDisputed(db, kind, id7, secretKey, now) {
|
|
5141
5252
|
membershipClock(now);
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
const
|
|
5253
|
+
const ns = NS[kind];
|
|
5254
|
+
let row2 = await membershipLoad(db, ns, id7);
|
|
5255
|
+
if (!row2) fail6("membership_not_found");
|
|
5256
|
+
const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
|
|
5257
|
+
const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
|
|
5258
|
+
const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
|
|
5146
5259
|
const done = (value, replayed) => ({
|
|
5147
5260
|
membershipId,
|
|
5261
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5148
5262
|
status: String(value.status),
|
|
5149
5263
|
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5150
5264
|
amountCents,
|
|
5151
5265
|
currency,
|
|
5152
5266
|
...replayed ? { replayed } : {}
|
|
5153
5267
|
});
|
|
5154
|
-
if (
|
|
5155
|
-
if (
|
|
5268
|
+
if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5269
|
+
if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
|
|
5156
5270
|
fail6("membership_not_disputed");
|
|
5157
5271
|
}
|
|
5158
|
-
if (
|
|
5272
|
+
if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5159
5273
|
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5160
|
-
let attempt =
|
|
5274
|
+
let attempt = row2.rechargeAttempt;
|
|
5161
5275
|
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5162
|
-
attempt = { key:
|
|
5276
|
+
attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
|
|
5163
5277
|
await db.transact(
|
|
5164
|
-
[membershipUpdate(
|
|
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
|
-
}] }
|
|
5278
|
+
[membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
|
|
5279
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
|
|
5171
5280
|
);
|
|
5172
|
-
|
|
5173
|
-
if (!
|
|
5281
|
+
row2 = await membershipLoad(db, ns, id7);
|
|
5282
|
+
if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5174
5283
|
}
|
|
5175
5284
|
const config = { secretKey };
|
|
5176
|
-
const paymentMethod = await defaultPaymentMethod(config, String(
|
|
5285
|
+
const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
|
|
5177
5286
|
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5178
5287
|
const intent = await stripeRequest7(config, "POST", "/v1/payment_intents", {
|
|
5179
5288
|
amount: amountCents,
|
|
5180
5289
|
currency,
|
|
5181
|
-
customer: String(
|
|
5290
|
+
customer: String(row2.providerCustomerId),
|
|
5182
5291
|
payment_method: paymentMethod,
|
|
5183
5292
|
off_session: "true",
|
|
5184
5293
|
confirm: "true",
|
|
5185
|
-
description:
|
|
5294
|
+
description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
|
|
5186
5295
|
metadata: {
|
|
5187
|
-
...stripePartitionMetadata({ appId: String(
|
|
5188
|
-
applicationId: String(
|
|
5296
|
+
...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
|
|
5297
|
+
applicationId: String(row2.applicationId),
|
|
5189
5298
|
membershipId,
|
|
5190
|
-
|
|
5299
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5300
|
+
disputeId: String(row2.disputeId),
|
|
5191
5301
|
membershipRecharge: "true"
|
|
5192
5302
|
}
|
|
5193
5303
|
}, { idempotencyKey: attempt.key });
|
|
5194
5304
|
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5195
5305
|
const finish = async (outcome) => {
|
|
5196
|
-
const current = await membershipLoad(db,
|
|
5306
|
+
const current = await membershipLoad(db, ns, id7);
|
|
5197
5307
|
if (!current) return;
|
|
5198
|
-
await db.transact([membershipUpdate(
|
|
5308
|
+
await db.transact([membershipUpdate(ns, id7, {
|
|
5199
5309
|
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5200
5310
|
revision: Number(current.revision) + 1,
|
|
5201
5311
|
updatedAt: now
|
|
@@ -5213,15 +5323,15 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5213
5323
|
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5214
5324
|
}
|
|
5215
5325
|
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5216
|
-
if (refOf2(body.customer) !==
|
|
5326
|
+
if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5217
5327
|
for (let retry = 0; retry < 8; retry++) {
|
|
5218
|
-
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db,
|
|
5328
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
|
|
5219
5329
|
if (!current) fail6("membership_not_found");
|
|
5220
5330
|
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"));
|
|
5331
|
+
const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5222
5332
|
try {
|
|
5223
5333
|
await db.transact(
|
|
5224
|
-
[membershipUpdate(
|
|
5334
|
+
[membershipUpdate(ns, id7, {
|
|
5225
5335
|
status: status2,
|
|
5226
5336
|
disputeStatus: "recharged",
|
|
5227
5337
|
rechargePaymentIntentId: String(body.id),
|
|
@@ -5231,7 +5341,7 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5231
5341
|
updatedAt: now
|
|
5232
5342
|
})],
|
|
5233
5343
|
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5234
|
-
{ ns
|
|
5344
|
+
{ ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5235
5345
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5236
5346
|
] }
|
|
5237
5347
|
);
|
|
@@ -5239,10 +5349,12 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5239
5349
|
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5240
5350
|
continue;
|
|
5241
5351
|
}
|
|
5242
|
-
return done(await membershipLoad(db,
|
|
5352
|
+
return done(await membershipLoad(db, ns, id7));
|
|
5243
5353
|
}
|
|
5244
5354
|
return fail6("membership_dispute_retry_required");
|
|
5245
5355
|
}
|
|
5356
|
+
var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
|
|
5357
|
+
var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
|
|
5246
5358
|
|
|
5247
5359
|
// src/worker-routes-admin-membership-disputes.ts
|
|
5248
5360
|
var LIST2 = "/api/admin/membership-disputes";
|
|
@@ -5259,6 +5371,9 @@ var PUBLIC2 = [
|
|
|
5259
5371
|
"memberNumber",
|
|
5260
5372
|
"amountCents",
|
|
5261
5373
|
"currency",
|
|
5374
|
+
"recipientName",
|
|
5375
|
+
"primaryMembershipId",
|
|
5376
|
+
"purchasedAt",
|
|
5262
5377
|
"disputeId",
|
|
5263
5378
|
"disputeStatus",
|
|
5264
5379
|
"disputeReason",
|
|
@@ -5306,14 +5421,17 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5306
5421
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5307
5422
|
}
|
|
5308
5423
|
if (!one) {
|
|
5309
|
-
const
|
|
5424
|
+
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5425
|
+
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5426
|
+
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5427
|
+
const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5310
5428
|
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5311
|
-
return json({ disputes
|
|
5429
|
+
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5312
5430
|
}
|
|
5313
5431
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5314
5432
|
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5315
5433
|
try {
|
|
5316
|
-
const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
|
|
5434
|
+
const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
|
|
5317
5435
|
return json({ ok: true, ...receipt });
|
|
5318
5436
|
} catch (error) {
|
|
5319
5437
|
const failure = error;
|
|
@@ -5333,13 +5451,13 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5333
5451
|
|
|
5334
5452
|
// src/membership-policy-versions.ts
|
|
5335
5453
|
var LIMIT = 200;
|
|
5336
|
-
var
|
|
5454
|
+
var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
5337
5455
|
var instant = (value) => {
|
|
5338
5456
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
5339
5457
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
5340
5458
|
};
|
|
5341
5459
|
function provenance(row2) {
|
|
5342
|
-
const source =
|
|
5460
|
+
const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
5343
5461
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
5344
5462
|
}
|
|
5345
5463
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -6340,8 +6458,8 @@ var APPLICATION_LABELS = {
|
|
|
6340
6458
|
state: "State"
|
|
6341
6459
|
};
|
|
6342
6460
|
function humanize(id7) {
|
|
6343
|
-
const
|
|
6344
|
-
return
|
|
6461
|
+
const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6462
|
+
return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
|
|
6345
6463
|
}
|
|
6346
6464
|
function applicationDefs(chapter) {
|
|
6347
6465
|
try {
|
|
@@ -6408,7 +6526,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6408
6526
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6409
6527
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6410
6528
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6411
|
-
var
|
|
6529
|
+
var text5 = (value, path, errors, max) => {
|
|
6412
6530
|
if (typeof value !== "string" || !value.trim()) {
|
|
6413
6531
|
errors.push(`${path} is required`);
|
|
6414
6532
|
return "";
|
|
@@ -6436,23 +6554,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6436
6554
|
}
|
|
6437
6555
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6438
6556
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6439
|
-
const id7 =
|
|
6557
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6440
6558
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6441
6559
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6442
6560
|
ids2.add(id7);
|
|
6443
6561
|
const targetRaw = record(item.target);
|
|
6444
6562
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6445
6563
|
if (targetRaw?.type === "tier") {
|
|
6446
|
-
const tierId =
|
|
6564
|
+
const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6447
6565
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6448
6566
|
target = { type: "tier", tierId };
|
|
6449
6567
|
} else if (targetRaw?.type === "entitlement") {
|
|
6450
|
-
const entitlementKey =
|
|
6568
|
+
const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6451
6569
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6452
6570
|
target = { type: "entitlement", entitlementKey };
|
|
6453
6571
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6454
6572
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6455
|
-
const stripePriceId =
|
|
6573
|
+
const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6456
6574
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6457
6575
|
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`);
|
|
6458
6576
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6474,9 +6592,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6474
6592
|
redemptionLifetimeHours: lifetime,
|
|
6475
6593
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6476
6594
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6477
|
-
title:
|
|
6478
|
-
description:
|
|
6479
|
-
ctaLabel:
|
|
6595
|
+
title: text5(item.title, `${path}.title`, errors, 160),
|
|
6596
|
+
description: text5(item.description, `${path}.description`, errors, 2e3),
|
|
6597
|
+
ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6480
6598
|
});
|
|
6481
6599
|
}
|
|
6482
6600
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6892,17 +7010,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6892
7010
|
if (!reader) return "";
|
|
6893
7011
|
const decoder = new TextDecoder();
|
|
6894
7012
|
let bytes = 0;
|
|
6895
|
-
let
|
|
7013
|
+
let text10 = "";
|
|
6896
7014
|
for (; ; ) {
|
|
6897
7015
|
const { done, value } = await reader.read();
|
|
6898
7016
|
if (req.signal.aborted) return invalid2();
|
|
6899
7017
|
if (done) {
|
|
6900
7018
|
complete = true;
|
|
6901
|
-
return
|
|
7019
|
+
return text10 + decoder.decode();
|
|
6902
7020
|
}
|
|
6903
7021
|
bytes += value.byteLength;
|
|
6904
7022
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6905
|
-
|
|
7023
|
+
text10 += decoder.decode(value, { stream: true });
|
|
6906
7024
|
}
|
|
6907
7025
|
} catch {
|
|
6908
7026
|
return invalid2();
|
|
@@ -7006,9 +7124,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
7006
7124
|
let selection;
|
|
7007
7125
|
if (request?.body) {
|
|
7008
7126
|
try {
|
|
7009
|
-
const
|
|
7010
|
-
if (
|
|
7011
|
-
const body = JSON.parse(
|
|
7127
|
+
const text10 = await readBoundedRequestText(request, 2048);
|
|
7128
|
+
if (text10 instanceof Response) return text10;
|
|
7129
|
+
const body = JSON.parse(text10);
|
|
7012
7130
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
7013
7131
|
selection = body.namedSeat;
|
|
7014
7132
|
} catch {
|
|
@@ -7890,10 +8008,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7890
8008
|
errors.push(`${path} must be a string`);
|
|
7891
8009
|
return void 0;
|
|
7892
8010
|
}
|
|
7893
|
-
const
|
|
7894
|
-
if (required && !
|
|
7895
|
-
if (
|
|
7896
|
-
return
|
|
8011
|
+
const text10 = value.trim();
|
|
8012
|
+
if (required && !text10) errors.push(`${path} is required`);
|
|
8013
|
+
if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
8014
|
+
return text10;
|
|
7897
8015
|
}
|
|
7898
8016
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7899
8017
|
const errors = [];
|
|
@@ -7942,8 +8060,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
7942
8060
|
else {
|
|
7943
8061
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
7944
8062
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
7945
|
-
const
|
|
7946
|
-
return
|
|
8063
|
+
const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8064
|
+
return text10 ? [text10] : [];
|
|
7947
8065
|
});
|
|
7948
8066
|
}
|
|
7949
8067
|
}
|
|
@@ -8037,7 +8155,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
8037
8155
|
function unknownKeys(value, allowed2, path, errors) {
|
|
8038
8156
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8039
8157
|
}
|
|
8040
|
-
function
|
|
8158
|
+
function text6(value, path, errors, max = 65536) {
|
|
8041
8159
|
if (typeof value !== "string" || !value.trim()) {
|
|
8042
8160
|
errors.push(`${path} is required`);
|
|
8043
8161
|
return "";
|
|
@@ -8060,11 +8178,11 @@ function tiersOf(value, errors) {
|
|
|
8060
8178
|
return [];
|
|
8061
8179
|
}
|
|
8062
8180
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
8063
|
-
const id7 =
|
|
8181
|
+
const id7 = text6(item.id, `${path}.id`, errors, 80);
|
|
8064
8182
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8065
8183
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
8066
8184
|
seen.add(id7);
|
|
8067
|
-
const name =
|
|
8185
|
+
const name = text6(item.name, `${path}.name`, errors, 160);
|
|
8068
8186
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8069
8187
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
8070
8188
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -8082,7 +8200,7 @@ function tiersOf(value, errors) {
|
|
|
8082
8200
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
8083
8201
|
errors.push(`${path}.interval must be year or month`);
|
|
8084
8202
|
}
|
|
8085
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8203
|
+
const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8086
8204
|
return [{
|
|
8087
8205
|
id: id7,
|
|
8088
8206
|
name,
|
|
@@ -8140,7 +8258,7 @@ function policyOf(value, errors, tiers) {
|
|
|
8140
8258
|
const result = {};
|
|
8141
8259
|
for (const key3 of allowed2) {
|
|
8142
8260
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
8143
|
-
result[key3] =
|
|
8261
|
+
result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
|
|
8144
8262
|
}
|
|
8145
8263
|
const tierPolicies = {};
|
|
8146
8264
|
if (input.tierPolicies !== void 0) {
|
|
@@ -8155,8 +8273,8 @@ function policyOf(value, errors, tiers) {
|
|
|
8155
8273
|
}
|
|
8156
8274
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
8157
8275
|
tierPolicies[id7] = {
|
|
8158
|
-
refundPolicyText:
|
|
8159
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8276
|
+
refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8277
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8160
8278
|
};
|
|
8161
8279
|
}
|
|
8162
8280
|
}
|
|
@@ -8196,9 +8314,9 @@ function validateSignupControlDocument(input) {
|
|
|
8196
8314
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
8197
8315
|
const source = recordOf(root.source);
|
|
8198
8316
|
const target = recordOf(root.target);
|
|
8199
|
-
const appId =
|
|
8200
|
-
const chapterId =
|
|
8201
|
-
const environment =
|
|
8317
|
+
const appId = text6(source?.appId, "document.source.appId", errors, 63);
|
|
8318
|
+
const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8319
|
+
const environment = text6(target?.environment, "document.target.environment", errors, 63);
|
|
8202
8320
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8203
8321
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
8204
8322
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -10469,7 +10587,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10469
10587
|
|
|
10470
10588
|
// src/worker-payment-receipt.ts
|
|
10471
10589
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10472
|
-
var escape = (
|
|
10590
|
+
var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10473
10591
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10474
10592
|
const id7 = url.searchParams.get("application");
|
|
10475
10593
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10480,7 +10598,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10480
10598
|
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);
|
|
10481
10599
|
const receipt = intent.paymentReceipt;
|
|
10482
10600
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10483
|
-
const
|
|
10601
|
+
const text10 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10484
10602
|
if (req.method === "POST") {
|
|
10485
10603
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10486
10604
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10498,7 +10616,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10498
10616
|
template: "paymentReceipt",
|
|
10499
10617
|
to: String(intent.purchaserEmailNormalized),
|
|
10500
10618
|
applicationId: id7,
|
|
10501
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10619
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text10 },
|
|
10502
10620
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10503
10621
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10504
10622
|
});
|
|
@@ -10508,7 +10626,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10508
10626
|
const nonce = crypto.randomUUID();
|
|
10509
10627
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10510
10628
|
<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>
|
|
10511
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
10629
|
+
<h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
|
|
10512
10630
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10513
10631
|
headers: {
|
|
10514
10632
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10978,16 +11096,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
10978
11096
|
};
|
|
10979
11097
|
|
|
10980
11098
|
// src/named-seat-accepted-notice.ts
|
|
10981
|
-
var
|
|
11099
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
10982
11100
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
10983
11101
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
10984
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
11102
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
|
|
10985
11103
|
return { sent: false, reason: "purchaser-missing" };
|
|
10986
11104
|
}
|
|
10987
11105
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
10988
11106
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
10989
11107
|
const config = emailGroupFrom(group);
|
|
10990
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
11108
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
|
|
10991
11109
|
return sendTemplated({
|
|
10992
11110
|
db,
|
|
10993
11111
|
envName: env.ODLA_ENV,
|
|
@@ -10999,12 +11117,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
10999
11117
|
}, {
|
|
11000
11118
|
group: config,
|
|
11001
11119
|
template: "seatAccepted",
|
|
11002
|
-
to:
|
|
11120
|
+
to: text7(purchaser.email),
|
|
11003
11121
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
11004
11122
|
applicationId: input.primaryApplicationId,
|
|
11005
11123
|
tierId: input.skuId,
|
|
11006
11124
|
vars: {
|
|
11007
|
-
firstName:
|
|
11125
|
+
firstName: text7(purchaser.firstName),
|
|
11008
11126
|
purchaserName,
|
|
11009
11127
|
recipientName: input.recipientName,
|
|
11010
11128
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -12391,6 +12509,84 @@ import { getConfig, saveConfig, validateEmailCopy as validateEmailCopy4 } from "
|
|
|
12391
12509
|
|
|
12392
12510
|
// src/email-readiness.ts
|
|
12393
12511
|
import { isEmailTierName as isEmailTierName2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
12512
|
+
|
|
12513
|
+
// src/email-spam-signals.ts
|
|
12514
|
+
var TRIGGER_PHRASES = [
|
|
12515
|
+
"act now",
|
|
12516
|
+
"click here",
|
|
12517
|
+
"limited time",
|
|
12518
|
+
"guaranteed",
|
|
12519
|
+
"risk-free",
|
|
12520
|
+
"risk free",
|
|
12521
|
+
"no obligation",
|
|
12522
|
+
"100% free",
|
|
12523
|
+
"winner",
|
|
12524
|
+
"congratulations",
|
|
12525
|
+
"urgent",
|
|
12526
|
+
"double your",
|
|
12527
|
+
"earn money",
|
|
12528
|
+
"make money",
|
|
12529
|
+
"cash bonus",
|
|
12530
|
+
"once in a lifetime",
|
|
12531
|
+
"exclusive deal",
|
|
12532
|
+
"order now",
|
|
12533
|
+
"buy now",
|
|
12534
|
+
"this is not spam",
|
|
12535
|
+
"dear friend",
|
|
12536
|
+
"dear customer"
|
|
12537
|
+
];
|
|
12538
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
12539
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
12540
|
+
var SHORTENERS = /^(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|cutt\.ly|tiny\.cc|shorturl\.at|rb\.gy)$/i;
|
|
12541
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
|
|
12542
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
12543
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
12544
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
12545
|
+
function emailSpamSignals(copy) {
|
|
12546
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12547
|
+
const text10 = typeof copy.text === "string" ? copy.text : "";
|
|
12548
|
+
const out = [];
|
|
12549
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12550
|
+
const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
|
|
12551
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
12552
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
12553
|
+
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
12554
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
12555
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
12556
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
|
|
12557
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12558
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text10))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
12559
|
+
if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
12560
|
+
const urls = [...text10.matchAll(URL_RE2)].map((match) => match[0]);
|
|
12561
|
+
const hosts = urls.map((url) => {
|
|
12562
|
+
try {
|
|
12563
|
+
return new URL(url).hostname;
|
|
12564
|
+
} catch {
|
|
12565
|
+
return "";
|
|
12566
|
+
}
|
|
12567
|
+
});
|
|
12568
|
+
if (urls.some((url) => /^http:\/\//i.test(url))) out.push({ code: "link-insecure", message: "A link uses http:// instead of https://. Filters flag insecure links." });
|
|
12569
|
+
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
12570
|
+
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
12571
|
+
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
12572
|
+
if (urls.length && words(text10).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
12573
|
+
return out;
|
|
12574
|
+
}
|
|
12575
|
+
function emailSenderWarnings(input) {
|
|
12576
|
+
const out = [];
|
|
12577
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
12578
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
12579
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
12580
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
12581
|
+
out.push({ code: "sender-no-reply", message: "Mail is sent from a no-reply address. Gmail's sender guidelines advise against it; send from a monitored address so replies and unsubscribe requests reach someone." });
|
|
12582
|
+
}
|
|
12583
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
12584
|
+
out.push({ code: "reply-to-domain", message: `Replies go to ${domain(reply)} while mail is sent from ${domain(from)}. A Reply-To on another domain is a mild spam signal; prefer an address on the sending domain.` });
|
|
12585
|
+
}
|
|
12586
|
+
return out;
|
|
12587
|
+
}
|
|
12588
|
+
|
|
12589
|
+
// src/email-readiness.ts
|
|
12394
12590
|
function validateEmailReadiness(input) {
|
|
12395
12591
|
const issues = [];
|
|
12396
12592
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -12415,9 +12611,15 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
12415
12611
|
}
|
|
12416
12612
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
12417
12613
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
12418
|
-
return {
|
|
12614
|
+
return {
|
|
12615
|
+
...event2,
|
|
12616
|
+
key: key3,
|
|
12617
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
12618
|
+
issues: problems,
|
|
12619
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
12620
|
+
};
|
|
12419
12621
|
});
|
|
12420
|
-
return { checkedAt: input.now ?? Date.now(), events, issues };
|
|
12622
|
+
return { checkedAt: input.now ?? Date.now(), events, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
12421
12623
|
}
|
|
12422
12624
|
|
|
12423
12625
|
// src/email-admin-config.ts
|
|
@@ -12443,7 +12645,14 @@ async function emailSettings(db, ctx, env) {
|
|
|
12443
12645
|
}
|
|
12444
12646
|
}
|
|
12445
12647
|
const groupValues = Object.fromEntries(["refundPolicyText", "commitmentText", "normsText"].map((key3) => [key3, str2(group?.[key3])]));
|
|
12446
|
-
const validation = validateEmailReadiness({
|
|
12648
|
+
const validation = validateEmailReadiness({
|
|
12649
|
+
events,
|
|
12650
|
+
templates: stored,
|
|
12651
|
+
groupValues,
|
|
12652
|
+
tiers: hub ? void 0 : tiers,
|
|
12653
|
+
tiersUnavailable,
|
|
12654
|
+
sender: { fromEmail: env.EMAIL_FROM ?? null, replyTo: str2(group?.replyTo) }
|
|
12655
|
+
});
|
|
12447
12656
|
return { group, events, data: {
|
|
12448
12657
|
groupId: ctx.chapter.id,
|
|
12449
12658
|
name: ctx.chapter.name,
|
|
@@ -12471,12 +12680,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12471
12680
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12472
12681
|
const t = value;
|
|
12473
12682
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12474
|
-
const subject = str2(t.subject).trim(),
|
|
12475
|
-
const issues = validateEmailCopy4({ subject, text:
|
|
12683
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12684
|
+
const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12476
12685
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12477
12686
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12478
12687
|
}
|
|
12479
|
-
clean[key3] = { subject, text:
|
|
12688
|
+
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12480
12689
|
}
|
|
12481
12690
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12482
12691
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12497,7 +12706,8 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12497
12706
|
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
12498
12707
|
]);
|
|
12499
12708
|
}
|
|
12500
|
-
|
|
12709
|
+
const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
|
|
12710
|
+
return json({ ok: true, warnings });
|
|
12501
12711
|
}
|
|
12502
12712
|
|
|
12503
12713
|
// src/worker-routes-admin-comms.ts
|
|
@@ -14689,9 +14899,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14689
14899
|
if (req.method === "POST") {
|
|
14690
14900
|
let body;
|
|
14691
14901
|
try {
|
|
14692
|
-
const
|
|
14693
|
-
if (new TextEncoder().encode(
|
|
14694
|
-
const parsed = JSON.parse(
|
|
14902
|
+
const text10 = await req.text();
|
|
14903
|
+
if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14904
|
+
const parsed = JSON.parse(text10);
|
|
14695
14905
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14696
14906
|
body = parsed;
|
|
14697
14907
|
} catch {
|
|
@@ -14860,7 +15070,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
|
|
|
14860
15070
|
// src/network-commercial-parity.ts
|
|
14861
15071
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14862
15072
|
var MAX_RECORDS = 500;
|
|
14863
|
-
var
|
|
15073
|
+
var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14864
15074
|
function parityMessage(partition, kind, value) {
|
|
14865
15075
|
return [
|
|
14866
15076
|
"chapter-commercial-parity",
|
|
@@ -14892,18 +15102,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14892
15102
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14893
15103
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14894
15104
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14895
|
-
const eligible = input.rows.filter((row2) =>
|
|
15105
|
+
const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
|
|
14896
15106
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14897
15107
|
const key3 = await parityKey(input.secret);
|
|
14898
15108
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14899
|
-
const applicationId =
|
|
14900
|
-
const customerId =
|
|
14901
|
-
const subscriptionId =
|
|
15109
|
+
const applicationId = text8(row2.id);
|
|
15110
|
+
const customerId = text8(row2.stripeCustomerId);
|
|
15111
|
+
const subscriptionId = text8(row2.stripeSubscriptionId);
|
|
14902
15112
|
return {
|
|
14903
15113
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14904
15114
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14905
15115
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14906
|
-
status:
|
|
15116
|
+
status: text8(row2.status, 64),
|
|
14907
15117
|
canceled: row2.canceled === true
|
|
14908
15118
|
};
|
|
14909
15119
|
}));
|
|
@@ -14929,7 +15139,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14929
15139
|
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;
|
|
14930
15140
|
const seen = /* @__PURE__ */ new Set();
|
|
14931
15141
|
for (const record4 of records) {
|
|
14932
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
15142
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
14933
15143
|
const key3 = recordKey(record4);
|
|
14934
15144
|
if (seen.has(key3)) return false;
|
|
14935
15145
|
seen.add(key3);
|
|
@@ -14940,16 +15150,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14940
15150
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14941
15151
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14942
15152
|
const key3 = await parityKey(input.secret);
|
|
14943
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
14944
|
-
const customerId =
|
|
14945
|
-
const subscriptionId =
|
|
15153
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
|
|
15154
|
+
const customerId = text8(row2.customerId);
|
|
15155
|
+
const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
|
|
14946
15156
|
return {
|
|
14947
15157
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
14948
15158
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
14949
15159
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
14950
15160
|
status: null,
|
|
14951
15161
|
canceled: false,
|
|
14952
|
-
stripeStatus:
|
|
15162
|
+
stripeStatus: text8(row2.status, 64),
|
|
14953
15163
|
stripeCanceled: row2.status === "canceled"
|
|
14954
15164
|
};
|
|
14955
15165
|
}));
|
|
@@ -15159,18 +15369,24 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
|
15159
15369
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15160
15370
|
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15161
15371
|
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15372
|
+
const scopeOf = (row2) => ({
|
|
15373
|
+
appId: String(row2.appId),
|
|
15374
|
+
chapterId: String(row2.chapterId),
|
|
15375
|
+
environment: String(row2.environment),
|
|
15376
|
+
runtime: String(row2.runtime),
|
|
15377
|
+
applicationId: String(row2.applicationId),
|
|
15378
|
+
skuId: String(row2.skuId)
|
|
15379
|
+
});
|
|
15380
|
+
if (charge.seatId) {
|
|
15381
|
+
const seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
15382
|
+
if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
|
|
15383
|
+
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15384
|
+
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15385
|
+
}
|
|
15162
15386
|
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15163
15387
|
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15164
15388
|
if (!member) return { disposition: "not_applicable" };
|
|
15165
|
-
const
|
|
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);
|
|
15389
|
+
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15174
15390
|
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15175
15391
|
}
|
|
15176
15392
|
|
|
@@ -15238,11 +15454,11 @@ function safeProjection(projection) {
|
|
|
15238
15454
|
amountReceivedCents: seat.amountReceivedCents,
|
|
15239
15455
|
refundedCents: seat.refundedCents ?? 0
|
|
15240
15456
|
})),
|
|
15241
|
-
disputes: (projection.disputes ?? []).map((
|
|
15242
|
-
...row2(
|
|
15243
|
-
chargeId:
|
|
15244
|
-
paymentIntentId:
|
|
15245
|
-
reason:
|
|
15457
|
+
disputes: (projection.disputes ?? []).map((dispute2) => ({
|
|
15458
|
+
...row2(dispute2),
|
|
15459
|
+
chargeId: dispute2.chargeId ?? null,
|
|
15460
|
+
paymentIntentId: dispute2.paymentIntentId ?? null,
|
|
15461
|
+
reason: dispute2.disputeReason ?? null
|
|
15246
15462
|
})),
|
|
15247
15463
|
exceptions: projection.exceptions
|
|
15248
15464
|
};
|
|
@@ -15276,10 +15492,10 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
15276
15492
|
if (result.status === "collision") {
|
|
15277
15493
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
15278
15494
|
}
|
|
15279
|
-
let
|
|
15495
|
+
let dispute2;
|
|
15280
15496
|
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15281
15497
|
try {
|
|
15282
|
-
|
|
15498
|
+
dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15283
15499
|
} catch (error) {
|
|
15284
15500
|
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15285
15501
|
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
@@ -15289,7 +15505,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
15289
15505
|
ok: true,
|
|
15290
15506
|
status: result.status,
|
|
15291
15507
|
eventId: fact.eventId,
|
|
15292
|
-
...
|
|
15508
|
+
...dispute2 ? { dispute: dispute2.disposition } : {},
|
|
15293
15509
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
15294
15510
|
});
|
|
15295
15511
|
} catch (error) {
|
|
@@ -15487,7 +15703,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15487
15703
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15488
15704
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15489
15705
|
}
|
|
15490
|
-
function
|
|
15706
|
+
function text9(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15491
15707
|
if (value === void 0 && !required) return void 0;
|
|
15492
15708
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15493
15709
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15513,7 +15729,7 @@ function scopeValue(value, path, errors) {
|
|
|
15513
15729
|
return { kind: raw2.kind };
|
|
15514
15730
|
}
|
|
15515
15731
|
if (raw2.kind === "chapter") {
|
|
15516
|
-
const chapterId =
|
|
15732
|
+
const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15517
15733
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15518
15734
|
return { kind: "chapter", chapterId };
|
|
15519
15735
|
}
|
|
@@ -15527,22 +15743,22 @@ function resourceValue(value, path, errors) {
|
|
|
15527
15743
|
return null;
|
|
15528
15744
|
}
|
|
15529
15745
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15530
|
-
const id7 =
|
|
15746
|
+
const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15531
15747
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15532
|
-
const kind =
|
|
15748
|
+
const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15533
15749
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15534
|
-
const title =
|
|
15535
|
-
const summary =
|
|
15536
|
-
const status2 =
|
|
15750
|
+
const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15751
|
+
const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15752
|
+
const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
|
|
15537
15753
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15538
15754
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15539
15755
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15540
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15756
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15541
15757
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15542
15758
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15543
15759
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15544
|
-
const bodyMarkdown =
|
|
15545
|
-
const url =
|
|
15760
|
+
const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15761
|
+
const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15546
15762
|
if (url) {
|
|
15547
15763
|
try {
|
|
15548
15764
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15554,9 +15770,9 @@ function resourceValue(value, path, errors) {
|
|
|
15554
15770
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15555
15771
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15556
15772
|
const brandAsset = asset ? {
|
|
15557
|
-
bookId:
|
|
15558
|
-
assetId:
|
|
15559
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15773
|
+
bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15774
|
+
assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15775
|
+
...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15560
15776
|
} : void 0;
|
|
15561
15777
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15562
15778
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15571,17 +15787,17 @@ function resourceValue(value, path, errors) {
|
|
|
15571
15787
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15572
15788
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15573
15789
|
const promotedFrom = promotedRaw ? {
|
|
15574
|
-
chapterId:
|
|
15575
|
-
resourceId:
|
|
15790
|
+
chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15791
|
+
resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15576
15792
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15577
15793
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15578
|
-
promotedBy:
|
|
15794
|
+
promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15579
15795
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15580
15796
|
} : void 0;
|
|
15581
15797
|
const provenance2 = {
|
|
15582
|
-
createdBy:
|
|
15798
|
+
createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15583
15799
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15584
|
-
updatedBy:
|
|
15800
|
+
updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15585
15801
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15586
15802
|
...promotedFrom ? { promotedFrom } : {}
|
|
15587
15803
|
};
|
|
@@ -15600,7 +15816,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15600
15816
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15601
15817
|
const sourceType = sourceRaw?.type;
|
|
15602
15818
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15603
|
-
const sourceAppId =
|
|
15819
|
+
const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15604
15820
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15605
15821
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15606
15822
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15608,8 +15824,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15608
15824
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15609
15825
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15610
15826
|
const target = targetRaw ? {
|
|
15611
|
-
appId:
|
|
15612
|
-
environment:
|
|
15827
|
+
appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15828
|
+
environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15613
15829
|
} : void 0;
|
|
15614
15830
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15615
15831
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -17134,6 +17350,19 @@ var id5 = () => attr("string", false, true, true);
|
|
|
17134
17350
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
17135
17351
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
17136
17352
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
17353
|
+
var dispute = () => ({
|
|
17354
|
+
disputeId: key(true),
|
|
17355
|
+
disputeStatus: attr("string", true),
|
|
17356
|
+
disputeReason: attr("string", true),
|
|
17357
|
+
disputeAmountCents: attr("number", true),
|
|
17358
|
+
disputedAt: attr("number", true),
|
|
17359
|
+
disputeClosedAt: attr("number", true),
|
|
17360
|
+
disputePriorStatus: attr("string", true),
|
|
17361
|
+
disputeEventId: key(true),
|
|
17362
|
+
rechargePaymentIntentId: key(true),
|
|
17363
|
+
rechargedAt: attr("number", true),
|
|
17364
|
+
rechargeAttempt: attr("json", true)
|
|
17365
|
+
});
|
|
17137
17366
|
var membershipAuthorityEntities = {
|
|
17138
17367
|
networkMembership: { attrs: {
|
|
17139
17368
|
id: id5(),
|
|
@@ -17174,19 +17403,7 @@ var membershipAuthorityEntities = {
|
|
|
17174
17403
|
...times(),
|
|
17175
17404
|
refundRequestedAt: attr("number", true),
|
|
17176
17405
|
restartId: key(true),
|
|
17177
|
-
|
|
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)
|
|
17406
|
+
...dispute()
|
|
17190
17407
|
} },
|
|
17191
17408
|
membershipRestart: { attrs: {
|
|
17192
17409
|
id: id5(),
|
|
@@ -17262,7 +17479,8 @@ var membershipAuthorityEntities = {
|
|
|
17262
17479
|
refundedCents: attr("number", true),
|
|
17263
17480
|
providerRefundIds: attr("json", true),
|
|
17264
17481
|
regiftedAt: attr("number", true),
|
|
17265
|
-
formerRecipients: attr("json", true)
|
|
17482
|
+
formerRecipients: attr("json", true),
|
|
17483
|
+
...dispute()
|
|
17266
17484
|
} },
|
|
17267
17485
|
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
17268
17486
|
membershipEffect: { attrs: {
|
|
@@ -17599,7 +17817,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17599
17817
|
};
|
|
17600
17818
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17601
17819
|
let size = 0;
|
|
17602
|
-
let
|
|
17820
|
+
let text10 = "";
|
|
17603
17821
|
const decoder = new TextDecoder();
|
|
17604
17822
|
try {
|
|
17605
17823
|
while (true) {
|
|
@@ -17609,10 +17827,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17609
17827
|
if (done) break;
|
|
17610
17828
|
size += value.byteLength;
|
|
17611
17829
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17612
|
-
|
|
17830
|
+
text10 += decoder.decode(value, { stream: true });
|
|
17613
17831
|
}
|
|
17614
17832
|
try {
|
|
17615
|
-
return JSON.parse(
|
|
17833
|
+
return JSON.parse(text10 + decoder.decode());
|
|
17616
17834
|
} catch {
|
|
17617
17835
|
throw new TypeError(`invalid ${label}`);
|
|
17618
17836
|
}
|
|
@@ -18390,6 +18608,7 @@ export {
|
|
|
18390
18608
|
deliverMemberContent,
|
|
18391
18609
|
deliverMembershipChapterEffect,
|
|
18392
18610
|
deliverSignupControl,
|
|
18611
|
+
disputedChargeCoversSeat,
|
|
18393
18612
|
drainMembershipEffects,
|
|
18394
18613
|
grantIncludedNamedSeat,
|
|
18395
18614
|
liftSeatPurchaseWindow,
|
|
@@ -18401,7 +18620,9 @@ export {
|
|
|
18401
18620
|
readDisputedCharge,
|
|
18402
18621
|
readNamedSeatOffer,
|
|
18403
18622
|
rechargeDisputedMembership,
|
|
18623
|
+
rechargeDisputedSeat,
|
|
18404
18624
|
reconcileMembershipDispute,
|
|
18625
|
+
reconcileNamedSeatDispute,
|
|
18405
18626
|
refundNamedSeatByOperator,
|
|
18406
18627
|
regiftNamedSeat,
|
|
18407
18628
|
releaseNamedSeatRecipientApplications,
|