@odla-ai/chapter 0.55.0 → 0.56.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/dist/{chunk-VEOGTSVX.js → chunk-IQZWCG42.js} +173 -102
- package/dist/chunk-IQZWCG42.js.map +1 -0
- package/dist/index.cjs +7 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +5 -1
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +110 -7
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +2 -0
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/index.js +110 -7
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-VEOGTSVX.js.map +0 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -946,6 +946,8 @@ interface MembershipOfferPreview {
|
|
|
946
946
|
renewalCents: number;
|
|
947
947
|
discountDuration?: "first_term" | "while_active";
|
|
948
948
|
initialTermEndsAt?: number;
|
|
949
|
+
/** Current additional-member price, including zero for an included member. */
|
|
950
|
+
seatPriceCents?: number;
|
|
949
951
|
}
|
|
950
952
|
|
|
951
953
|
/** Server-owned source identity; the browser never supplies an offer or its price. */
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -946,6 +946,8 @@ interface MembershipOfferPreview {
|
|
|
946
946
|
renewalCents: number;
|
|
947
947
|
discountDuration?: "first_term" | "while_active";
|
|
948
948
|
initialTermEndsAt?: number;
|
|
949
|
+
/** Current additional-member price, including zero for an included member. */
|
|
950
|
+
seatPriceCents?: number;
|
|
949
951
|
}
|
|
950
952
|
|
|
951
953
|
/** Server-owned source identity; the browser never supplies an offer or its price. */
|
package/dist/worker/index.js
CHANGED
|
@@ -1713,7 +1713,7 @@ function findTier(tiers, tierId) {
|
|
|
1713
1713
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1714
1714
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1715
1715
|
}
|
|
1716
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1716
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map(), seats = /* @__PURE__ */ new Map()) {
|
|
1717
1717
|
return tiers.map((tier) => ({
|
|
1718
1718
|
id: tier.id,
|
|
1719
1719
|
name: tier.name,
|
|
@@ -1722,7 +1722,8 @@ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Ma
|
|
|
1722
1722
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1723
1723
|
blurb: tier.blurb ?? "",
|
|
1724
1724
|
free: tierIsFree(tier),
|
|
1725
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1725
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {},
|
|
1726
|
+
...seats.has(tier.id) ? { seatOffer: seats.get(tier.id) } : {}
|
|
1726
1727
|
}));
|
|
1727
1728
|
}
|
|
1728
1729
|
|
|
@@ -1780,7 +1781,7 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1780
1781
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1781
1782
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1782
1783
|
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1783
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1784
|
+
const amount = quote?.lineItems?.dueTodayCents ?? contract?.dueTodayCents ?? intent?.priceCents;
|
|
1784
1785
|
const currency = offering?.currency ?? intent?.currency;
|
|
1785
1786
|
let formatted = "";
|
|
1786
1787
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
@@ -2215,6 +2216,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
2215
2216
|
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
2216
2217
|
dueTodayCents: contract.dueTodayCents,
|
|
2217
2218
|
renewalCents: contract.renewalCents,
|
|
2219
|
+
...offering.seatPriceCents !== void 0 ? { seatPriceCents: offering.seatPriceCents } : {},
|
|
2218
2220
|
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
2219
2221
|
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
2220
2222
|
};
|
|
@@ -3186,6 +3188,48 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
3186
3188
|
}
|
|
3187
3189
|
}
|
|
3188
3190
|
|
|
3191
|
+
// src/payment-receipt.ts
|
|
3192
|
+
async function retainPaymentReceipt(db, intent, rawBody) {
|
|
3193
|
+
const quote = intent.paymentQuote;
|
|
3194
|
+
if (!quote || intent.paymentReceipt) return;
|
|
3195
|
+
const event2 = JSON.parse(rawBody), invoice = event2.data?.object;
|
|
3196
|
+
const paidAt = invoice?.status_transitions?.paid_at ?? event2.created;
|
|
3197
|
+
if (!/^in_[A-Za-z0-9_]+$/.test(invoice?.id ?? "") || !Number.isSafeInteger(paidAt) || paidAt <= 0) return;
|
|
3198
|
+
const receipt = {
|
|
3199
|
+
invoiceId: invoice.id,
|
|
3200
|
+
paidAt: paidAt * 1e3,
|
|
3201
|
+
currency: quote.currency,
|
|
3202
|
+
tierName: quote.tierName,
|
|
3203
|
+
merchant: quote.merchantDisclosureText,
|
|
3204
|
+
membershipCents: quote.lineItems.dueTodayCents - (quote.lineItems.namedSeatCents ?? 0),
|
|
3205
|
+
totalCents: quote.lineItems.dueTodayCents,
|
|
3206
|
+
...quote.namedSeat ? { namedSeat: { recipientName: quote.namedSeat.recipientName, amountCents: quote.lineItems.namedSeatCents } } : {}
|
|
3207
|
+
};
|
|
3208
|
+
await db.transact([{ t: "update", ns: "subscriptionCheckoutIntents", id: String(intent.id), attrs: { paymentReceipt: receipt } }], {
|
|
3209
|
+
mutationId: `payment-receipt:${invoice.id}`,
|
|
3210
|
+
guards: [{
|
|
3211
|
+
ns: "subscriptionCheckoutIntents",
|
|
3212
|
+
id: String(intent.id),
|
|
3213
|
+
exists: true,
|
|
3214
|
+
equals: { providerSubscriptionId: intent.providerSubscriptionId, providerCustomerId: intent.providerCustomerId },
|
|
3215
|
+
absent: ["paymentReceipt"]
|
|
3216
|
+
}]
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
function paymentReceiptText(receipt, chapterName) {
|
|
3220
|
+
const money = (cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: receipt.currency }).format(cents / 100);
|
|
3221
|
+
return [
|
|
3222
|
+
chapterName,
|
|
3223
|
+
"Payment receipt",
|
|
3224
|
+
`Invoice: ${receipt.invoiceId}`,
|
|
3225
|
+
`Paid: ${new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" }).format(receipt.paidAt)}`,
|
|
3226
|
+
receipt.merchant,
|
|
3227
|
+
`${receipt.tierName}: ${money(receipt.membershipCents)}`,
|
|
3228
|
+
...receipt.namedSeat ? [`Additional member \u2014 ${receipt.namedSeat.recipientName}: ${receipt.namedSeat.amountCents === 0 ? "Included \u2014 " : ""}${money(receipt.namedSeat.amountCents)}`] : [],
|
|
3229
|
+
`Total paid: ${money(receipt.totalCents)}`
|
|
3230
|
+
].filter(Boolean).join("\n");
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3189
3233
|
// src/payment-notifications.ts
|
|
3190
3234
|
var groupFor = async (db, app) => {
|
|
3191
3235
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3748,6 +3792,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3748
3792
|
} catch {
|
|
3749
3793
|
return json({ error: "network membership payment reconciliation pending" }, 503);
|
|
3750
3794
|
}
|
|
3795
|
+
if (intent) await retainPaymentReceipt(db, intent, rawBody);
|
|
3751
3796
|
}
|
|
3752
3797
|
}
|
|
3753
3798
|
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
@@ -5534,7 +5579,7 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
5534
5579
|
}
|
|
5535
5580
|
|
|
5536
5581
|
// src/join-founding-preview.ts
|
|
5537
|
-
async function foundingPreviews(tiers, env, ctx) {
|
|
5582
|
+
async function foundingPreviews(tiers, env, ctx, seats = /* @__PURE__ */ new Map()) {
|
|
5538
5583
|
const result = /* @__PURE__ */ new Map();
|
|
5539
5584
|
const preview = ctx.membershipAuthority?.previewOffer;
|
|
5540
5585
|
if (!preview) return result;
|
|
@@ -5542,6 +5587,10 @@ async function foundingPreviews(tiers, env, ctx) {
|
|
|
5542
5587
|
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
5543
5588
|
try {
|
|
5544
5589
|
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
5590
|
+
if (offer.skuId !== tier.id || offer.basePriceCents !== tier.priceCents) return;
|
|
5591
|
+
if (ctx.membershipAuthority?.namedSeats && Number.isSafeInteger(offer.seatPriceCents) && Number(offer.seatPriceCents) >= 0) {
|
|
5592
|
+
seats.set(tier.id, { amountCents: Number(offer.seatPriceCents) });
|
|
5593
|
+
}
|
|
5545
5594
|
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
5546
5595
|
result.set(tier.id, {
|
|
5547
5596
|
cohortLabel: offer.cohortLabel,
|
|
@@ -7118,11 +7167,12 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7118
7167
|
group,
|
|
7119
7168
|
Boolean(stripeKey)
|
|
7120
7169
|
);
|
|
7121
|
-
const
|
|
7170
|
+
const seats = /* @__PURE__ */ new Map();
|
|
7171
|
+
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7122
7172
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7123
7173
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7124
7174
|
return json({
|
|
7125
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
7175
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
7126
7176
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
7127
7177
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
7128
7178
|
...activeControl ? { signupControl: {
|
|
@@ -9905,6 +9955,58 @@ async function startSubscription(req, env, ctx) {
|
|
|
9905
9955
|
return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: quote.lineItems });
|
|
9906
9956
|
}
|
|
9907
9957
|
|
|
9958
|
+
// src/worker-payment-receipt.ts
|
|
9959
|
+
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
9960
|
+
var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
9961
|
+
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
9962
|
+
const id7 = url.searchParams.get("application");
|
|
9963
|
+
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
9964
|
+
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
9965
|
+
const db = ctx.makeDb(env);
|
|
9966
|
+
const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
9967
|
+
const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
|
|
9968
|
+
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);
|
|
9969
|
+
const receipt = intent.paymentReceipt;
|
|
9970
|
+
if (!receipt) return json2({ pending: true }, 202);
|
|
9971
|
+
const text8 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
9972
|
+
if (req.method === "POST") {
|
|
9973
|
+
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
9974
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
9975
|
+
if (!group) return json2({ error: "Receipt email is unavailable." }, 503);
|
|
9976
|
+
const config = emailGroupFrom(group);
|
|
9977
|
+
const debugEmail = (env.ODLA_DEBUG_EMAIL ?? config.debugEmail)?.trim();
|
|
9978
|
+
if (env.ODLA_ENV !== "prod" && (!debugEmail || !/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(debugEmail))) {
|
|
9979
|
+
return json2({ error: "A test inbox is required to email receipts in this environment." }, 503);
|
|
9980
|
+
}
|
|
9981
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
9982
|
+
group: { ...config, emailTemplates: {
|
|
9983
|
+
...config.emailTemplates,
|
|
9984
|
+
paymentReceipt: { enabled: true, subject: "Your {{chapterName}} payment receipt", text: "{{receiptText}}" }
|
|
9985
|
+
} },
|
|
9986
|
+
template: "paymentReceipt",
|
|
9987
|
+
to: String(intent.purchaserEmailNormalized),
|
|
9988
|
+
applicationId: id7,
|
|
9989
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text8 },
|
|
9990
|
+
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
9991
|
+
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
9992
|
+
});
|
|
9993
|
+
return result.sent ? json2({ sent: true, preview: env.ODLA_ENV !== "prod" }) : json2({ error: "We could not confirm email delivery. You can print or save your receipt." }, 503);
|
|
9994
|
+
}
|
|
9995
|
+
if (url.searchParams.get("format") !== "html") return json2({ receipt, chapterName: ctx.chapter.name });
|
|
9996
|
+
const nonce = crypto.randomUUID();
|
|
9997
|
+
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
9998
|
+
<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>
|
|
9999
|
+
<h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
|
|
10000
|
+
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10001
|
+
headers: {
|
|
10002
|
+
"content-type": "text/html; charset=utf-8",
|
|
10003
|
+
"cache-control": "no-store",
|
|
10004
|
+
"referrer-policy": "no-referrer",
|
|
10005
|
+
"content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; frame-ancestors 'none'; base-uri 'none'`
|
|
10006
|
+
}
|
|
10007
|
+
});
|
|
10008
|
+
}
|
|
10009
|
+
|
|
9908
10010
|
// src/worker-routes-payments.ts
|
|
9909
10011
|
async function ingestWebhook(req, env, ctx) {
|
|
9910
10012
|
const db = ctx.makeDb(env);
|
|
@@ -9917,6 +10019,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9917
10019
|
}
|
|
9918
10020
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9919
10021
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
10022
|
+
if (["GET", "POST"].includes(req.method) && url.pathname === "/api/payments/receipt") return paymentReceiptRoute(req, url, env, ctx);
|
|
9920
10023
|
if (req.method === "GET" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx);
|
|
9921
10024
|
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9922
10025
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
@@ -16825,7 +16928,7 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
16825
16928
|
async function requestMembershipOfferPreview(options) {
|
|
16826
16929
|
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
16827
16930
|
const preview = result.preview;
|
|
16828
|
-
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16931
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.seatPriceCents !== void 0 && (!Number.isSafeInteger(preview.seatPriceCents) || preview.seatPriceCents < 0) || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16829
16932
|
return preview;
|
|
16830
16933
|
}
|
|
16831
16934
|
async function requestMembershipCheckoutBinding(options) {
|