@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/ui/index.js
CHANGED
|
@@ -262,6 +262,10 @@ interface JoinTier {
|
|
|
262
262
|
blurb: string;
|
|
263
263
|
/** True when the tier costs nothing, so the flow skips the payment step. */
|
|
264
264
|
free: boolean;
|
|
265
|
+
/** Current configured additional-member price; zero means included. Checkout confirms the offer. */
|
|
266
|
+
seatOffer?: {
|
|
267
|
+
amountCents: number;
|
|
268
|
+
};
|
|
265
269
|
/** Present when the network authority currently qualifies applicants as founding members at a reduced rate. */
|
|
266
270
|
founding?: {
|
|
267
271
|
cohortLabel: string;
|
|
@@ -278,7 +282,7 @@ interface JoinTier {
|
|
|
278
282
|
* is an unstyled placeholder, not a design.
|
|
279
283
|
*/
|
|
280
284
|
interface JoinTierRenderContext {
|
|
281
|
-
/** Offered tiers, in
|
|
285
|
+
/** Offered tiers, with the selected tier first and the others in catalog order. */
|
|
282
286
|
tiers: readonly JoinTier[];
|
|
283
287
|
/** The chosen tier, or null while nothing is chosen. */
|
|
284
288
|
selected: JoinTier | null;
|
package/dist/ui/member/index.js
CHANGED
package/dist/worker/index.cjs
CHANGED
|
@@ -1762,7 +1762,7 @@ function findTier(tiers, tierId) {
|
|
|
1762
1762
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1763
1763
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1764
1764
|
}
|
|
1765
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1765
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map(), seats = /* @__PURE__ */ new Map()) {
|
|
1766
1766
|
return tiers.map((tier) => ({
|
|
1767
1767
|
id: tier.id,
|
|
1768
1768
|
name: tier.name,
|
|
@@ -1771,7 +1771,8 @@ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Ma
|
|
|
1771
1771
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1772
1772
|
blurb: tier.blurb ?? "",
|
|
1773
1773
|
free: tierIsFree(tier),
|
|
1774
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1774
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {},
|
|
1775
|
+
...seats.has(tier.id) ? { seatOffer: seats.get(tier.id) } : {}
|
|
1775
1776
|
}));
|
|
1776
1777
|
}
|
|
1777
1778
|
|
|
@@ -1829,7 +1830,7 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1829
1830
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1830
1831
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1831
1832
|
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1832
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1833
|
+
const amount = quote?.lineItems?.dueTodayCents ?? contract?.dueTodayCents ?? intent?.priceCents;
|
|
1833
1834
|
const currency = offering?.currency ?? intent?.currency;
|
|
1834
1835
|
let formatted = "";
|
|
1835
1836
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
@@ -2264,6 +2265,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
2264
2265
|
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
2265
2266
|
dueTodayCents: contract.dueTodayCents,
|
|
2266
2267
|
renewalCents: contract.renewalCents,
|
|
2268
|
+
...offering.seatPriceCents !== void 0 ? { seatPriceCents: offering.seatPriceCents } : {},
|
|
2267
2269
|
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
2268
2270
|
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
2269
2271
|
};
|
|
@@ -3235,6 +3237,48 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
3235
3237
|
}
|
|
3236
3238
|
}
|
|
3237
3239
|
|
|
3240
|
+
// src/payment-receipt.ts
|
|
3241
|
+
async function retainPaymentReceipt(db, intent, rawBody) {
|
|
3242
|
+
const quote = intent.paymentQuote;
|
|
3243
|
+
if (!quote || intent.paymentReceipt) return;
|
|
3244
|
+
const event2 = JSON.parse(rawBody), invoice = event2.data?.object;
|
|
3245
|
+
const paidAt = invoice?.status_transitions?.paid_at ?? event2.created;
|
|
3246
|
+
if (!/^in_[A-Za-z0-9_]+$/.test(invoice?.id ?? "") || !Number.isSafeInteger(paidAt) || paidAt <= 0) return;
|
|
3247
|
+
const receipt = {
|
|
3248
|
+
invoiceId: invoice.id,
|
|
3249
|
+
paidAt: paidAt * 1e3,
|
|
3250
|
+
currency: quote.currency,
|
|
3251
|
+
tierName: quote.tierName,
|
|
3252
|
+
merchant: quote.merchantDisclosureText,
|
|
3253
|
+
membershipCents: quote.lineItems.dueTodayCents - (quote.lineItems.namedSeatCents ?? 0),
|
|
3254
|
+
totalCents: quote.lineItems.dueTodayCents,
|
|
3255
|
+
...quote.namedSeat ? { namedSeat: { recipientName: quote.namedSeat.recipientName, amountCents: quote.lineItems.namedSeatCents } } : {}
|
|
3256
|
+
};
|
|
3257
|
+
await db.transact([{ t: "update", ns: "subscriptionCheckoutIntents", id: String(intent.id), attrs: { paymentReceipt: receipt } }], {
|
|
3258
|
+
mutationId: `payment-receipt:${invoice.id}`,
|
|
3259
|
+
guards: [{
|
|
3260
|
+
ns: "subscriptionCheckoutIntents",
|
|
3261
|
+
id: String(intent.id),
|
|
3262
|
+
exists: true,
|
|
3263
|
+
equals: { providerSubscriptionId: intent.providerSubscriptionId, providerCustomerId: intent.providerCustomerId },
|
|
3264
|
+
absent: ["paymentReceipt"]
|
|
3265
|
+
}]
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
function paymentReceiptText(receipt, chapterName) {
|
|
3269
|
+
const money = (cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: receipt.currency }).format(cents / 100);
|
|
3270
|
+
return [
|
|
3271
|
+
chapterName,
|
|
3272
|
+
"Payment receipt",
|
|
3273
|
+
`Invoice: ${receipt.invoiceId}`,
|
|
3274
|
+
`Paid: ${new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" }).format(receipt.paidAt)}`,
|
|
3275
|
+
receipt.merchant,
|
|
3276
|
+
`${receipt.tierName}: ${money(receipt.membershipCents)}`,
|
|
3277
|
+
...receipt.namedSeat ? [`Additional member \u2014 ${receipt.namedSeat.recipientName}: ${receipt.namedSeat.amountCents === 0 ? "Included \u2014 " : ""}${money(receipt.namedSeat.amountCents)}`] : [],
|
|
3278
|
+
`Total paid: ${money(receipt.totalCents)}`
|
|
3279
|
+
].filter(Boolean).join("\n");
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3238
3282
|
// src/payment-notifications.ts
|
|
3239
3283
|
var groupFor = async (db, app) => {
|
|
3240
3284
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3797,6 +3841,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3797
3841
|
} catch {
|
|
3798
3842
|
return json({ error: "network membership payment reconciliation pending" }, 503);
|
|
3799
3843
|
}
|
|
3844
|
+
if (intent) await retainPaymentReceipt(db, intent, rawBody);
|
|
3800
3845
|
}
|
|
3801
3846
|
}
|
|
3802
3847
|
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
@@ -5583,7 +5628,7 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
5583
5628
|
}
|
|
5584
5629
|
|
|
5585
5630
|
// src/join-founding-preview.ts
|
|
5586
|
-
async function foundingPreviews(tiers, env, ctx) {
|
|
5631
|
+
async function foundingPreviews(tiers, env, ctx, seats = /* @__PURE__ */ new Map()) {
|
|
5587
5632
|
const result = /* @__PURE__ */ new Map();
|
|
5588
5633
|
const preview = ctx.membershipAuthority?.previewOffer;
|
|
5589
5634
|
if (!preview) return result;
|
|
@@ -5591,6 +5636,10 @@ async function foundingPreviews(tiers, env, ctx) {
|
|
|
5591
5636
|
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
5592
5637
|
try {
|
|
5593
5638
|
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
5639
|
+
if (offer.skuId !== tier.id || offer.basePriceCents !== tier.priceCents) return;
|
|
5640
|
+
if (ctx.membershipAuthority?.namedSeats && Number.isSafeInteger(offer.seatPriceCents) && Number(offer.seatPriceCents) >= 0) {
|
|
5641
|
+
seats.set(tier.id, { amountCents: Number(offer.seatPriceCents) });
|
|
5642
|
+
}
|
|
5594
5643
|
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
5595
5644
|
result.set(tier.id, {
|
|
5596
5645
|
cohortLabel: offer.cohortLabel,
|
|
@@ -7167,11 +7216,12 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7167
7216
|
group,
|
|
7168
7217
|
Boolean(stripeKey)
|
|
7169
7218
|
);
|
|
7170
|
-
const
|
|
7219
|
+
const seats = /* @__PURE__ */ new Map();
|
|
7220
|
+
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
7171
7221
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7172
7222
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
7173
7223
|
return json({
|
|
7174
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
7224
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
7175
7225
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
7176
7226
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
7177
7227
|
...activeControl ? { signupControl: {
|
|
@@ -9954,6 +10004,58 @@ async function startSubscription(req, env, ctx) {
|
|
|
9954
10004
|
return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: quote.lineItems });
|
|
9955
10005
|
}
|
|
9956
10006
|
|
|
10007
|
+
// src/worker-payment-receipt.ts
|
|
10008
|
+
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10009
|
+
var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10010
|
+
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10011
|
+
const id7 = url.searchParams.get("application");
|
|
10012
|
+
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
10013
|
+
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
10014
|
+
const db = ctx.makeDb(env);
|
|
10015
|
+
const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
10016
|
+
const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
|
|
10017
|
+
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);
|
|
10018
|
+
const receipt = intent.paymentReceipt;
|
|
10019
|
+
if (!receipt) return json2({ pending: true }, 202);
|
|
10020
|
+
const text8 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10021
|
+
if (req.method === "POST") {
|
|
10022
|
+
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10023
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
10024
|
+
if (!group) return json2({ error: "Receipt email is unavailable." }, 503);
|
|
10025
|
+
const config = emailGroupFrom(group);
|
|
10026
|
+
const debugEmail = (env.ODLA_DEBUG_EMAIL ?? config.debugEmail)?.trim();
|
|
10027
|
+
if (env.ODLA_ENV !== "prod" && (!debugEmail || !/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(debugEmail))) {
|
|
10028
|
+
return json2({ error: "A test inbox is required to email receipts in this environment." }, 503);
|
|
10029
|
+
}
|
|
10030
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
10031
|
+
group: { ...config, emailTemplates: {
|
|
10032
|
+
...config.emailTemplates,
|
|
10033
|
+
paymentReceipt: { enabled: true, subject: "Your {{chapterName}} payment receipt", text: "{{receiptText}}" }
|
|
10034
|
+
} },
|
|
10035
|
+
template: "paymentReceipt",
|
|
10036
|
+
to: String(intent.purchaserEmailNormalized),
|
|
10037
|
+
applicationId: id7,
|
|
10038
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text8 },
|
|
10039
|
+
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10040
|
+
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10041
|
+
});
|
|
10042
|
+
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);
|
|
10043
|
+
}
|
|
10044
|
+
if (url.searchParams.get("format") !== "html") return json2({ receipt, chapterName: ctx.chapter.name });
|
|
10045
|
+
const nonce = crypto.randomUUID();
|
|
10046
|
+
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10047
|
+
<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>
|
|
10048
|
+
<h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
|
|
10049
|
+
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10050
|
+
headers: {
|
|
10051
|
+
"content-type": "text/html; charset=utf-8",
|
|
10052
|
+
"cache-control": "no-store",
|
|
10053
|
+
"referrer-policy": "no-referrer",
|
|
10054
|
+
"content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; frame-ancestors 'none'; base-uri 'none'`
|
|
10055
|
+
}
|
|
10056
|
+
});
|
|
10057
|
+
}
|
|
10058
|
+
|
|
9957
10059
|
// src/worker-routes-payments.ts
|
|
9958
10060
|
async function ingestWebhook(req, env, ctx) {
|
|
9959
10061
|
const db = ctx.makeDb(env);
|
|
@@ -9966,6 +10068,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9966
10068
|
}
|
|
9967
10069
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9968
10070
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
10071
|
+
if (["GET", "POST"].includes(req.method) && url.pathname === "/api/payments/receipt") return paymentReceiptRoute(req, url, env, ctx);
|
|
9969
10072
|
if (req.method === "GET" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx);
|
|
9970
10073
|
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9971
10074
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
@@ -16865,7 +16968,7 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
16865
16968
|
async function requestMembershipOfferPreview(options) {
|
|
16866
16969
|
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
16867
16970
|
const preview = result.preview;
|
|
16868
|
-
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");
|
|
16971
|
+
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");
|
|
16869
16972
|
return preview;
|
|
16870
16973
|
}
|
|
16871
16974
|
async function requestMembershipCheckoutBinding(options) {
|