@odla-ai/chapter 0.55.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VEOGTSVX.js → chunk-NK267YQZ.js} +504 -342
- package/dist/chunk-NK267YQZ.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.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +38 -2
- 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.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { APPLICATION_STAGES, AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceFrameProps, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, CandidateStagePanel, CandidateStagePanelProps, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderAdmissionAction, LeaderAdmissionActionProps, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, MembershipPolicyVersions, MembershipPolicyVersionsProps, NamedSeatRecordPanel, NamedSeatRecordPanelProps, NetworkRecordEditor, NetworkRecordsWorkspace, NetworkSeatBilling, NetworkShareActions, NetworkShareActionsProps, Panel, PaymentRecoveryQueue, PeopleSectionOptions, RecordActions, RecordActionsProps, RecordAdmissionPanel, RecordAdmissionPanelProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
2
|
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-CaUXIxUI.js';
|
|
3
|
-
export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, JourneyFrame, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MemberRestart, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, NAMED_SEAT_RECIPIENT_WORDS, NamedSeatPurchase, NamedSeatRecipientLine, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
|
|
3
|
+
export { ApiFn, BrandStyle, BrandStyleProps, CheckoutSeatEditor, CheckoutSeatEditorContext, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, JourneyFrame, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MemberRestart, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, NAMED_SEAT_RECIPIENT_WORDS, NamedSeatPurchase, NamedSeatRecipientLine, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
|
|
4
4
|
import 'preact';
|
|
5
5
|
import '@odla-ai/crm';
|
|
6
6
|
import '@odla-ai/crm/ui';
|
package/dist/ui/index.js
CHANGED
|
@@ -209,6 +209,8 @@ interface PaymentPriceLines {
|
|
|
209
209
|
dueTodayCents: number;
|
|
210
210
|
/** Included in dueTodayCents; zero means the selected SKU includes the additional member. */
|
|
211
211
|
namedSeatCents?: number;
|
|
212
|
+
/** True when an included additional member is available but no recipient was selected. Display Pick later. */
|
|
213
|
+
namedSeatPickLater?: boolean;
|
|
212
214
|
founding?: {
|
|
213
215
|
cohortLabel: string;
|
|
214
216
|
};
|
|
@@ -262,6 +264,10 @@ interface JoinTier {
|
|
|
262
264
|
blurb: string;
|
|
263
265
|
/** True when the tier costs nothing, so the flow skips the payment step. */
|
|
264
266
|
free: boolean;
|
|
267
|
+
/** Current configured additional-member price; zero means included. Checkout confirms the offer. */
|
|
268
|
+
seatOffer?: {
|
|
269
|
+
amountCents: number;
|
|
270
|
+
};
|
|
265
271
|
/** Present when the network authority currently qualifies applicants as founding members at a reduced rate. */
|
|
266
272
|
founding?: {
|
|
267
273
|
cohortLabel: string;
|
|
@@ -278,7 +284,7 @@ interface JoinTier {
|
|
|
278
284
|
* is an unstyled placeholder, not a design.
|
|
279
285
|
*/
|
|
280
286
|
interface JoinTierRenderContext {
|
|
281
|
-
/** Offered tiers, in
|
|
287
|
+
/** Offered tiers, with the selected tier first and the others in catalog order. */
|
|
282
288
|
tiers: readonly JoinTier[];
|
|
283
289
|
/** The chosen tier, or null while nothing is chosen. */
|
|
284
290
|
selected: JoinTier | null;
|
|
@@ -370,6 +376,30 @@ interface ChapterFlowFieldContract {
|
|
|
370
376
|
fields: readonly ChapterFlowField[];
|
|
371
377
|
}
|
|
372
378
|
|
|
379
|
+
/** The optional recipient selected before the applicant consents to payment. */
|
|
380
|
+
interface CheckoutNamedSeatSelection {
|
|
381
|
+
recipientName: string;
|
|
382
|
+
recipientEmail: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Reopens the host's original recipient fields before payment begins. */
|
|
386
|
+
interface CheckoutSeatEditorContext {
|
|
387
|
+
/** Server-saved selection; null means the applicant has not chosen a recipient. */
|
|
388
|
+
selection: CheckoutNamedSeatSelection | null;
|
|
389
|
+
/** The current authority-owned additional-member price and billing interval. */
|
|
390
|
+
offer: {
|
|
391
|
+
amountCents: number;
|
|
392
|
+
currency: string;
|
|
393
|
+
interval: string;
|
|
394
|
+
};
|
|
395
|
+
/** Validate and save a recipient, or null to remove them, then return to checkout. */
|
|
396
|
+
save: (selection: CheckoutNamedSeatSelection | null) => Promise<void>;
|
|
397
|
+
/** Return to checkout without changing its saved selection. */
|
|
398
|
+
cancel: () => void;
|
|
399
|
+
}
|
|
400
|
+
/** Host-owned recipient fields reused from the application instead of a second checkout editor. */
|
|
401
|
+
type CheckoutSeatEditor = (context: CheckoutSeatEditorContext) => ComponentChildren;
|
|
402
|
+
|
|
373
403
|
/** The public join config (the shape `GET /api/join-config` returns). */
|
|
374
404
|
interface JoinConfig {
|
|
375
405
|
id: string;
|
|
@@ -422,6 +452,8 @@ type JoinFlowState = {
|
|
|
422
452
|
/** Canonical flow state supplied to a site's per-step heading render slot. */
|
|
423
453
|
interface JoinStepRenderContext {
|
|
424
454
|
state: JoinFlowState;
|
|
455
|
+
/** True while returning to the application's family-member fields; the saved application stays on its payment step. */
|
|
456
|
+
editingNamedSeat?: boolean;
|
|
425
457
|
invitation?: JoinInvitation;
|
|
426
458
|
applicationId?: string;
|
|
427
459
|
}
|
|
@@ -480,6 +512,10 @@ interface JoinIslandProps {
|
|
|
480
512
|
recipientName: string;
|
|
481
513
|
recipientEmail: string;
|
|
482
514
|
};
|
|
515
|
+
/** Reuse the application's recipient fields for edits. Checkout automatically quotes initialNamedSeat and shows a read-only review. */
|
|
516
|
+
renderNamedSeatEditor?: CheckoutSeatEditor;
|
|
517
|
+
/** Heading for the read-only recipient review. Defaults to Additional member. */
|
|
518
|
+
namedSeatLabel?: string;
|
|
483
519
|
appearance?: Record<string, unknown>;
|
|
484
520
|
fonts?: readonly Record<string, unknown>[];
|
|
485
521
|
/** Render the server-owned price lines; `lines.founding` says the difference is a policy-backed founding discount,
|
|
@@ -707,4 +743,4 @@ declare function JourneyFrame({ children }: {
|
|
|
707
743
|
children: ComponentChildren;
|
|
708
744
|
}): preact.JSX.Element;
|
|
709
745
|
|
|
710
|
-
export { type ApiFn, BrandStyle, type BrandStyleProps, type GiftCodeRedeemResult, GiftCodeRedeemer, type GiftCodeRedeemerCopy, type GiftCodeRedeemerProps, GiftOfferCards, type GiftOfferCardsMode, type GiftOfferCardsProps, GiftPurchases, type GiftPurchasesProps, type JoinBookingSimulation, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinSimulation, type JoinStepRenderContext, type JoinSubmitRenderContext, type JoinTier, type JoinTierRenderContext, JourneyFrame, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, type MemberContentAreaProps, MemberGifts, type MemberProvisionalRenderContext, MemberRestart, MembersArea, type MembersAreaProps, type MembershipCardTokenName, MembershipCards, type MembershipCardsMode, type MembershipCardsProps, NAMED_SEAT_RECIPIENT_WORDS, NamedSeatPurchase, NamedSeatRecipientLine, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type PaymentStepSimulation, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, type SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection };
|
|
746
|
+
export { type ApiFn, BrandStyle, type BrandStyleProps, type CheckoutSeatEditor, type CheckoutSeatEditorContext, type GiftCodeRedeemResult, GiftCodeRedeemer, type GiftCodeRedeemerCopy, type GiftCodeRedeemerProps, GiftOfferCards, type GiftOfferCardsMode, type GiftOfferCardsProps, GiftPurchases, type GiftPurchasesProps, type JoinBookingSimulation, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinSimulation, type JoinStepRenderContext, type JoinSubmitRenderContext, type JoinTier, type JoinTierRenderContext, JourneyFrame, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, type MemberContentAreaProps, MemberGifts, type MemberProvisionalRenderContext, MemberRestart, MembersArea, type MembersAreaProps, type MembershipCardTokenName, MembershipCards, type MembershipCardsMode, type MembershipCardsProps, NAMED_SEAT_RECIPIENT_WORDS, NamedSeatPurchase, NamedSeatRecipientLine, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type PaymentStepSimulation, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, type SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection };
|
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) {
|