@softure-ai/billing 0.0.0-stage → 0.1.6
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +695 -2
- package/dist/calendar.d.ts +26 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +81 -0
- package/dist/calendar.js.map +1 -0
- package/dist/contract.d.ts +187 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +6 -0
- package/dist/contract.js.map +1 -0
- package/dist/currency-digits.d.ts +3 -0
- package/dist/currency-digits.d.ts.map +1 -0
- package/dist/currency-digits.js +32 -0
- package/dist/currency-digits.js.map +1 -0
- package/dist/entitlement.d.ts +25 -0
- package/dist/entitlement.d.ts.map +1 -0
- package/dist/entitlement.js +75 -0
- package/dist/entitlement.js.map +1 -0
- package/dist/fields.d.ts +25 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +27 -0
- package/dist/fields.js.map +1 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/invoice.d.ts +29 -0
- package/dist/invoice.d.ts.map +1 -0
- package/dist/invoice.js +35 -0
- package/dist/invoice.js.map +1 -0
- package/dist/mailing/index.d.ts +2 -0
- package/dist/mailing/index.d.ts.map +1 -0
- package/dist/mailing/index.js +4 -0
- package/dist/mailing/index.js.map +1 -0
- package/dist/mailing/reminder-mail.d.ts +50 -0
- package/dist/mailing/reminder-mail.d.ts.map +1 -0
- package/dist/mailing/reminder-mail.js +71 -0
- package/dist/mailing/reminder-mail.js.map +1 -0
- package/dist/manual.d.ts +12 -0
- package/dist/manual.d.ts.map +1 -0
- package/dist/manual.js +22 -0
- package/dist/manual.js.map +1 -0
- package/dist/messages/en.d.ts +176 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +151 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +359 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +14 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +3 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +151 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/access.d.ts +16 -0
- package/dist/next/access.d.ts.map +1 -0
- package/dist/next/access.js +24 -0
- package/dist/next/access.js.map +1 -0
- package/dist/next/actions.d.ts +24 -0
- package/dist/next/actions.d.ts.map +1 -0
- package/dist/next/actions.js +158 -0
- package/dist/next/actions.js.map +1 -0
- package/dist/next/context.d.ts +4 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +17 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/current-entitlement.d.ts +18 -0
- package/dist/next/current-entitlement.d.ts.map +1 -0
- package/dist/next/current-entitlement.js +26 -0
- package/dist/next/current-entitlement.js.map +1 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +12 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/pages.d.ts +24 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +166 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/pricing.d.ts +12 -0
- package/dist/next/pricing.d.ts.map +1 -0
- package/dist/next/pricing.js +20 -0
- package/dist/next/pricing.js.map +1 -0
- package/dist/next/route.d.ts +11 -0
- package/dist/next/route.d.ts.map +1 -0
- package/dist/next/route.js +56 -0
- package/dist/next/route.js.map +1 -0
- package/dist/options.d.ts +85 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +121 -0
- package/dist/options.js.map +1 -0
- package/dist/payment.d.ts +61 -0
- package/dist/payment.d.ts.map +1 -0
- package/dist/payment.js +12 -0
- package/dist/payment.js.map +1 -0
- package/dist/plans.d.ts +20 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +53 -0
- package/dist/plans.js.map +1 -0
- package/dist/price.d.ts +12 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +37 -0
- package/dist/price.js.map +1 -0
- package/dist/refund.d.ts +70 -0
- package/dist/refund.d.ts.map +1 -0
- package/dist/refund.js +109 -0
- package/dist/refund.js.map +1 -0
- package/dist/reminder.d.ts +30 -0
- package/dist/reminder.d.ts.map +1 -0
- package/dist/reminder.js +34 -0
- package/dist/reminder.js.map +1 -0
- package/dist/schema.d.ts +1023 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +77 -0
- package/dist/schema.js.map +1 -0
- package/dist/scripts/entitlement-scripts.d.ts +17 -0
- package/dist/scripts/entitlement-scripts.d.ts.map +1 -0
- package/dist/scripts/entitlement-scripts.js +167 -0
- package/dist/scripts/entitlement-scripts.js.map +1 -0
- package/dist/scripts/index.d.ts +3 -0
- package/dist/scripts/index.d.ts.map +1 -0
- package/dist/scripts/index.js +5 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/plan-scripts.d.ts +19 -0
- package/dist/scripts/plan-scripts.d.ts.map +1 -0
- package/dist/scripts/plan-scripts.js +106 -0
- package/dist/scripts/plan-scripts.js.map +1 -0
- package/dist/server/entitlements.d.ts +69 -0
- package/dist/server/entitlements.d.ts.map +1 -0
- package/dist/server/entitlements.js +202 -0
- package/dist/server/entitlements.js.map +1 -0
- package/dist/server/grants.d.ts +74 -0
- package/dist/server/grants.d.ts.map +1 -0
- package/dist/server/grants.js +174 -0
- package/dist/server/grants.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +17 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/options.d.ts +20 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +38 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/payments.d.ts +140 -0
- package/dist/server/payments.d.ts.map +1 -0
- package/dist/server/payments.js +339 -0
- package/dist/server/payments.js.map +1 -0
- package/dist/server/plans.d.ts +50 -0
- package/dist/server/plans.d.ts.map +1 -0
- package/dist/server/plans.js +129 -0
- package/dist/server/plans.js.map +1 -0
- package/dist/server/privacy.d.ts +77 -0
- package/dist/server/privacy.d.ts.map +1 -0
- package/dist/server/privacy.js +110 -0
- package/dist/server/privacy.js.map +1 -0
- package/dist/server/reminders.d.ts +20 -0
- package/dist/server/reminders.d.ts.map +1 -0
- package/dist/server/reminders.js +85 -0
- package/dist/server/reminders.js.map +1 -0
- package/dist/server/requests.d.ts +80 -0
- package/dist/server/requests.d.ts.map +1 -0
- package/dist/server/requests.js +155 -0
- package/dist/server/requests.js.map +1 -0
- package/dist/server/setup.d.ts +9 -0
- package/dist/server/setup.d.ts.map +1 -0
- package/dist/server/setup.js +34 -0
- package/dist/server/setup.js.map +1 -0
- package/dist/server/take-back.d.ts +80 -0
- package/dist/server/take-back.d.ts.map +1 -0
- package/dist/server/take-back.js +138 -0
- package/dist/server/take-back.js.map +1 -0
- package/dist/server/user-id.d.ts +4 -0
- package/dist/server/user-id.d.ts.map +1 -0
- package/dist/server/user-id.js +11 -0
- package/dist/server/user-id.js.map +1 -0
- package/dist/stripe-currency.d.ts +27 -0
- package/dist/stripe-currency.d.ts.map +1 -0
- package/dist/stripe-currency.js +57 -0
- package/dist/stripe-currency.js.map +1 -0
- package/dist/stripe-webhook.d.ts +101 -0
- package/dist/stripe-webhook.d.ts.map +1 -0
- package/dist/stripe-webhook.js +209 -0
- package/dist/stripe-webhook.js.map +1 -0
- package/dist/stripe.d.ts +25 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +116 -0
- package/dist/stripe.js.map +1 -0
- package/dist/ui/access-badge.d.ts +16 -0
- package/dist/ui/access-badge.d.ts.map +1 -0
- package/dist/ui/access-badge.js +43 -0
- package/dist/ui/access-badge.js.map +1 -0
- package/dist/ui/access-notice.d.ts +20 -0
- package/dist/ui/access-notice.d.ts.map +1 -0
- package/dist/ui/access-notice.js +41 -0
- package/dist/ui/access-notice.js.map +1 -0
- package/dist/ui/format.d.ts +11 -0
- package/dist/ui/format.d.ts.map +1 -0
- package/dist/ui/format.js +21 -0
- package/dist/ui/format.js.map +1 -0
- package/dist/ui/grant-form.d.ts +18 -0
- package/dist/ui/grant-form.d.ts.map +1 -0
- package/dist/ui/grant-form.js +23 -0
- package/dist/ui/grant-form.js.map +1 -0
- package/dist/ui/grant-history.d.ts +39 -0
- package/dist/ui/grant-history.d.ts.map +1 -0
- package/dist/ui/grant-history.js +36 -0
- package/dist/ui/grant-history.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/payment-form.d.ts +23 -0
- package/dist/ui/payment-form.d.ts.map +1 -0
- package/dist/ui/payment-form.js +34 -0
- package/dist/ui/payment-form.js.map +1 -0
- package/dist/ui/payment-requests.d.ts +30 -0
- package/dist/ui/payment-requests.d.ts.map +1 -0
- package/dist/ui/payment-requests.js +31 -0
- package/dist/ui/payment-requests.js.map +1 -0
- package/dist/ui/pricing-tiles.d.ts +22 -0
- package/dist/ui/pricing-tiles.d.ts.map +1 -0
- package/dist/ui/pricing-tiles.js +40 -0
- package/dist/ui/pricing-tiles.js.map +1 -0
- package/migrations/0001_create_entitlements.sql +16 -0
- package/migrations/0002_create_payments.sql +30 -0
- package/migrations/0003_record_payment_grants.sql +25 -0
- package/migrations/0004_create_requests_and_grants.sql +58 -0
- package/migrations/0005_record_refunded_amounts.sql +18 -0
- package/migrations/0006_record_request_handover_and_prices.sql +38 -0
- package/migrations/0007_record_failed_refunds.sql +27 -0
- package/migrations/0008_record_request_handover_claims.sql +9 -0
- package/migrations/0009_record_pending_charge_states.sql +16 -0
- package/module.json +23 -0
- package/package.json +85 -4
- package/src/calendar.ts +90 -0
- package/src/contract.ts +181 -0
- package/src/currency-digits.ts +37 -0
- package/src/entitlement.ts +84 -0
- package/src/fields.ts +37 -0
- package/src/index.ts +163 -0
- package/src/invoice.ts +58 -0
- package/src/mailing/index.ts +11 -0
- package/src/mailing/reminder-mail.ts +108 -0
- package/src/manual.ts +31 -0
- package/src/messages/en.ts +150 -0
- package/src/messages/index.ts +18 -0
- package/src/messages/pl.ts +152 -0
- package/src/next/access.tsx +55 -0
- package/src/next/actions.ts +176 -0
- package/src/next/context.ts +18 -0
- package/src/next/current-entitlement.ts +36 -0
- package/src/next/index.ts +11 -0
- package/src/next/next-modules.d.ts +21 -0
- package/src/next/pages.tsx +267 -0
- package/src/next/pricing.tsx +39 -0
- package/src/next/route.ts +57 -0
- package/src/options.ts +128 -0
- package/src/payment.ts +77 -0
- package/src/plans.ts +63 -0
- package/src/price.ts +50 -0
- package/src/refund.ts +135 -0
- package/src/reminder.ts +52 -0
- package/src/schema.ts +86 -0
- package/src/scripts/entitlement-scripts.ts +188 -0
- package/src/scripts/index.ts +11 -0
- package/src/scripts/plan-scripts.ts +143 -0
- package/src/server/entitlements.ts +227 -0
- package/src/server/grants.ts +227 -0
- package/src/server/health.ts +18 -0
- package/src/server/index.ts +83 -0
- package/src/server/options.ts +52 -0
- package/src/server/payments.ts +434 -0
- package/src/server/plans.ts +143 -0
- package/src/server/privacy.ts +189 -0
- package/src/server/reminders.ts +113 -0
- package/src/server/requests.ts +201 -0
- package/src/server/setup.ts +37 -0
- package/src/server/take-back.ts +190 -0
- package/src/server/user-id.ts +12 -0
- package/src/stripe-currency.ts +65 -0
- package/src/stripe-webhook.ts +278 -0
- package/src/stripe.ts +130 -0
- package/src/ui/access-badge.tsx +64 -0
- package/src/ui/access-notice.tsx +73 -0
- package/src/ui/format.ts +25 -0
- package/src/ui/grant-form.tsx +69 -0
- package/src/ui/grant-history.tsx +127 -0
- package/src/ui/index.ts +25 -0
- package/src/ui/payment-form.tsx +111 -0
- package/src/ui/payment-requests.tsx +126 -0
- package/src/ui/pricing-tiles.tsx +105 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Pages ready to mount with one line each:
|
|
3
|
+
// `export { PaymentPage as default } from "@softure-ai/billing/next"` at `routes.payment`, and
|
|
4
|
+
// `export { BillingAdminPage as default } from "@softure-ai/billing/next"` at `routes.admin`.
|
|
5
|
+
// Server components: they read the config and the session, and render the forms from `../ui`.
|
|
6
|
+
import { requireRole, requireUser } from "@softure-ai/auth/next";
|
|
7
|
+
import { formatMessage } from "@softure-ai/core";
|
|
8
|
+
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
9
|
+
import { ButtonLink, Card, EmptyState } from "@softure-ai/ui";
|
|
10
|
+
import { ACCOUNT_PARAM, CHECKOUT_PARAM, CHECKOUT_RESULTS, PLAN_FIELD } from "../fields.js";
|
|
11
|
+
import { findPlan, getLocalizedText } from "../plans.js";
|
|
12
|
+
import { formatPrice } from "../price.js";
|
|
13
|
+
import { getEntitlement } from "../server/entitlements.js";
|
|
14
|
+
import { getAccountHistory } from "../server/grants.js";
|
|
15
|
+
import { getBillingMessages, getBillingOptions, getBillingRoutes } from "../server/options.js";
|
|
16
|
+
import { findAccountById, getBillingPlans, getPaymentProvider } from "../server/plans.js";
|
|
17
|
+
import { listOpenRequests } from "../server/requests.js";
|
|
18
|
+
import { AccessBadge } from "../ui/access-badge.js";
|
|
19
|
+
import { formatDay, formatLastDay, formatPeriod } from "../ui/format.js";
|
|
20
|
+
import { GrantForm } from "../ui/grant-form.js";
|
|
21
|
+
import { AccountLookup, GrantHistory } from "../ui/grant-history.js";
|
|
22
|
+
import { PaymentForm } from "../ui/payment-form.js";
|
|
23
|
+
import { PaymentRequestList } from "../ui/payment-requests.js";
|
|
24
|
+
import { PricingTiles } from "../ui/pricing-tiles.js";
|
|
25
|
+
import { CurrentAccessBadge } from "./access.js";
|
|
26
|
+
import { dismissRequestAction, findAccountAction, grantPlanAction, grantRequestAction, revokeGrantAction, startPaymentAction } from "./actions.js";
|
|
27
|
+
import { getBillingContext } from "./context.js";
|
|
28
|
+
import { getCurrentEntitlement } from "./current-entitlement.js";
|
|
29
|
+
import { getPlanPaymentHref } from "./pricing.js";
|
|
30
|
+
const LAYOUT_CLASS = "sft:mx-auto sft:box-border sft:flex sft:w-full sft:flex-col sft:gap-4 sft:sm:max-w-md sft:px-4 sft:py-4";
|
|
31
|
+
const STACK_CLASS = "sft:flex sft:flex-col sft:gap-4";
|
|
32
|
+
const LEAD_CLASS = "sft:m-0 sft:font-sans sft:text-sm sft:text-muted";
|
|
33
|
+
const NOTICE_CLASS = "sft:m-0 sft:rounded-control sft:border sft:border-border-strong sft:bg-surface-raised sft:px-4 sft:py-2.5 sft:font-sans sft:text-sm sft:text-foreground";
|
|
34
|
+
async function readParam(searchParams, name) {
|
|
35
|
+
const value = (await searchParams)?.[name];
|
|
36
|
+
return Array.isArray(value) ? value[0] : value;
|
|
37
|
+
}
|
|
38
|
+
function isCheckoutResult(value) {
|
|
39
|
+
return CHECKOUT_RESULTS.some((result) => result === value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The plans and, with `?plan=<id>`, the order: the plan's price and the provider's form (the
|
|
43
|
+
* invoice request of the manual adapter, a checkout button of a hosted provider). Without a session
|
|
44
|
+
* it sends the visitor to log in and back. A read-only account reaches it too: it is where to pay.
|
|
45
|
+
* A hosted checkout comes back with `?checkout=success` or `?checkout=cancelled`, shown as a notice
|
|
46
|
+
* (the access itself changes when the provider's webhook confirms the payment).
|
|
47
|
+
*/
|
|
48
|
+
export async function PaymentPage({ searchParams }) {
|
|
49
|
+
const config = getSoftureConfig();
|
|
50
|
+
const route = getBillingRoutes(config).payment;
|
|
51
|
+
const planId = await readParam(searchParams, PLAN_FIELD);
|
|
52
|
+
const checkout = await readParam(searchParams, CHECKOUT_PARAM);
|
|
53
|
+
const user = await requireUser({ next: planId === undefined ? route : getPlanPaymentHref(route, planId), searchParams: await searchParams });
|
|
54
|
+
const provider = getPaymentProvider(config);
|
|
55
|
+
const messages = getBillingMessages(config);
|
|
56
|
+
const copy = messages.payment;
|
|
57
|
+
const plans = getBillingPlans(config);
|
|
58
|
+
const plan = planId === undefined ? undefined : findPlan(plans, planId);
|
|
59
|
+
const planName = plan === undefined ? "" : getLocalizedText(plan.name, config.locale);
|
|
60
|
+
const checkoutNotice = isCheckoutResult(checkout) ? (checkout === "success" ? copy.checkoutSuccess : copy.checkoutCancelled) : null;
|
|
61
|
+
const entitlement = await getCurrentEntitlement();
|
|
62
|
+
// Lifetime access leaves nothing to pay for: no order form (startPayment refuses it too).
|
|
63
|
+
const hasLifetime = entitlement?.status === "paid" && entitlement.endsAt === null;
|
|
64
|
+
return (_jsxs("main", { className: LAYOUT_CLASS, children: [_jsx(Card, { title: copy.title, subtitle: copy.lead, children: _jsxs("div", { className: STACK_CLASS, children: [checkoutNotice === null ? null : (_jsx("p", { role: "status", className: NOTICE_CLASS, "data-checkout": checkout, children: checkoutNotice })), _jsx("div", { children: _jsx(CurrentAccessBadge, {}) }), hasLifetime ? (_jsx("p", { role: "status", className: NOTICE_CLASS, "data-lifetime": "true", children: copy.lifetime })) : null, _jsx(PricingTiles, { plans: plans, messages: messages, locale: config.locale, getPlanHref: (candidate) => getPlanPaymentHref(route, candidate.id), selectedPlanId: plan?.id, label: copy.plansLabel })] }) }), plan === undefined || hasLifetime ? null : (_jsx(Card, { title: copy.orderTitle, subtitle: formatMessage(copy.orderLead, { plan: planName, price: formatPrice(plan.price, config.locale), period: formatPeriod(plan.period, config.locale, messages) }), children: _jsxs("div", { className: STACK_CLASS, children: [provider.collectsInvoiceDetails ? _jsx("p", { className: LEAD_CLASS, children: formatMessage(copy.invoiceLead, { email: user.email }) }) : null, _jsx(PaymentForm, { action: startPaymentAction, planId: plan.id, planName: planName, email: user.email, collectsInvoiceDetails: provider.collectsInvoiceDetails, messages: messages, locale: config.locale }), _jsx("div", { children: _jsx(ButtonLink, { href: route, variant: "ghost", size: "sm", children: copy.changePlan }) })] }) }))] }));
|
|
65
|
+
}
|
|
66
|
+
/** The plan's name in the app's locale, or its id when the config no longer has it. */
|
|
67
|
+
function getPlanName(plans, planId, locale) {
|
|
68
|
+
const plan = findPlan(plans, planId);
|
|
69
|
+
return plan === undefined ? planId : getLocalizedText(plan.name, locale);
|
|
70
|
+
}
|
|
71
|
+
function getHistoryHref(config, userId) {
|
|
72
|
+
return `${getBillingRoutes(config).admin}?${new URLSearchParams({ [ACCOUNT_PARAM]: userId }).toString()}`;
|
|
73
|
+
}
|
|
74
|
+
function toRequestRow(request, { config, messages, plans }) {
|
|
75
|
+
const copy = messages.admin.requests;
|
|
76
|
+
const values = { email: request.email, plan: getPlanName(plans, request.planId, config.locale) };
|
|
77
|
+
const { invoice } = request;
|
|
78
|
+
const details = [formatMessage(copy.requestedOn, { date: formatDay(request.requestedAt, config.locale, config.timezone) })];
|
|
79
|
+
if (request.price !== null)
|
|
80
|
+
details.push(formatMessage(copy.price, { price: formatPrice(request.price, config.locale) }));
|
|
81
|
+
if (invoice === null)
|
|
82
|
+
details.push(copy.noInvoice);
|
|
83
|
+
else {
|
|
84
|
+
details.push(formatMessage(copy.invoice, { name: invoice.name, address: invoice.address }));
|
|
85
|
+
if (invoice.taxId !== null)
|
|
86
|
+
details.push(formatMessage(copy.taxId, { taxId: invoice.taxId }));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
id: request.id,
|
|
90
|
+
title: formatMessage(copy.line, values),
|
|
91
|
+
details,
|
|
92
|
+
historyHref: getHistoryHref(config, request.userId),
|
|
93
|
+
grantLabel: formatMessage(copy.grantLabel, values),
|
|
94
|
+
dismissLabel: formatMessage(copy.dismissLabel, values),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function describeGrant(grant, { config, messages }) {
|
|
98
|
+
const copy = messages.admin.history;
|
|
99
|
+
if (grant === null)
|
|
100
|
+
return copy.noGrant;
|
|
101
|
+
if (grant.kind === "lifetime")
|
|
102
|
+
return copy.lifetime;
|
|
103
|
+
return formatMessage(copy.period, { from: formatDay(grant.from, config.locale, config.timezone), to: formatLastDay(grant.until, config.locale, config.timezone) });
|
|
104
|
+
}
|
|
105
|
+
function toHistoryRow(entry, context) {
|
|
106
|
+
const { config, messages, plans } = context;
|
|
107
|
+
const copy = messages.admin.history;
|
|
108
|
+
const plan = getPlanName(plans, entry.planId, config.locale);
|
|
109
|
+
const formatDate = (date) => formatDay(date, config.locale, config.timezone);
|
|
110
|
+
if (entry.source === "manual") {
|
|
111
|
+
const isActive = entry.status === "active";
|
|
112
|
+
return {
|
|
113
|
+
id: entry.id,
|
|
114
|
+
title: formatMessage(entry.isFromRequest ? copy.fromRequest : copy.manual, { plan }),
|
|
115
|
+
statusText: isActive || entry.revokedAt === null ? copy.active : formatMessage(copy.revokedOn, { date: formatDate(entry.revokedAt) }),
|
|
116
|
+
isCurrent: isActive,
|
|
117
|
+
details: [
|
|
118
|
+
entry.price === null
|
|
119
|
+
? formatMessage(copy.grantedOn, { date: formatDate(entry.at) })
|
|
120
|
+
: formatMessage(copy.grantedFor, { amount: formatPrice(entry.price, config.locale), date: formatDate(entry.at) }),
|
|
121
|
+
describeGrant(entry.grant, context),
|
|
122
|
+
],
|
|
123
|
+
revokeLabel: isActive ? formatMessage(copy.revokeLabel, { plan, date: formatDate(entry.at) }) : null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const isPaid = entry.status === "paid";
|
|
127
|
+
const formatAmount = (amount) => formatPrice({ amount, currency: entry.currency }, config.locale);
|
|
128
|
+
let statusText = copy.paid;
|
|
129
|
+
if (!isPaid && entry.refundedAt !== null)
|
|
130
|
+
statusText = formatMessage(copy.refundedOn, { date: formatDate(entry.refundedAt) });
|
|
131
|
+
else if (entry.refundedAmount > 0)
|
|
132
|
+
statusText = formatMessage(copy.partlyRefunded, { amount: formatAmount(entry.refundedAmount) });
|
|
133
|
+
return {
|
|
134
|
+
id: entry.id,
|
|
135
|
+
title: formatMessage(copy.provider, { plan, provider: entry.provider }),
|
|
136
|
+
statusText,
|
|
137
|
+
isCurrent: isPaid,
|
|
138
|
+
details: [
|
|
139
|
+
formatMessage(copy.paidOn, { amount: formatAmount(entry.amount), date: formatDate(entry.at) }),
|
|
140
|
+
describeGrant(entry.grant, context),
|
|
141
|
+
],
|
|
142
|
+
revokeLabel: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The admin page of manual payments: the open invoice requests (grant or dismiss each), the grant
|
|
147
|
+
* form, and an account's history (`?account=<id>`, reached by the email lookup or a request's
|
|
148
|
+
* link) with its access and a revoke button on each active manual grant. Anyone without the role
|
|
149
|
+
* of `billing({ adminRole })`, signed in or not, gets Next's "not found".
|
|
150
|
+
*/
|
|
151
|
+
export async function BillingAdminPage({ searchParams }) {
|
|
152
|
+
const config = getSoftureConfig();
|
|
153
|
+
await requireRole(getBillingOptions(config).adminRole);
|
|
154
|
+
const messages = getBillingMessages(config);
|
|
155
|
+
const plans = getBillingPlans(config);
|
|
156
|
+
const context = { config, messages, plans };
|
|
157
|
+
const ctx = await getBillingContext(config);
|
|
158
|
+
const requests = (await listOpenRequests(ctx)).map((request) => toRequestRow(request, context));
|
|
159
|
+
const accountId = await readParam(searchParams, ACCOUNT_PARAM);
|
|
160
|
+
const account = accountId === undefined ? null : await findAccountById(ctx, accountId);
|
|
161
|
+
const entitlement = account === null ? null : await getEntitlement(ctx, account.id);
|
|
162
|
+
const history = account === null ? [] : (await getAccountHistory(ctx, account.id)).map((entry) => toHistoryRow(entry, context));
|
|
163
|
+
const planOptions = plans.map((plan) => ({ value: plan.id, label: getLocalizedText(plan.name, config.locale) }));
|
|
164
|
+
return (_jsxs("main", { className: LAYOUT_CLASS, children: [_jsx(Card, { title: messages.admin.requests.title, subtitle: messages.admin.requests.lead, children: _jsx(PaymentRequestList, { requests: requests, grantAction: grantRequestAction, dismissAction: dismissRequestAction, messages: messages }) }), _jsx(Card, { title: messages.admin.title, subtitle: messages.admin.lead, children: planOptions.length === 0 ? (_jsx(EmptyState, { title: messages.admin.noPlans })) : (_jsx(GrantForm, { action: grantPlanAction, plans: planOptions, messages: messages, locale: config.locale })) }), _jsx(Card, { title: messages.admin.history.title, subtitle: messages.admin.history.lead, children: _jsxs("div", { className: STACK_CLASS, children: [_jsx(AccountLookup, { action: findAccountAction, messages: messages, locale: config.locale }), account === null ? null : (_jsxs("section", { className: STACK_CLASS, "aria-label": formatMessage(messages.admin.history.accountTitle, { email: account.email }), children: [_jsx("p", { className: LEAD_CLASS, children: formatMessage(messages.admin.history.accountTitle, { email: account.email }) }), entitlement === null ? null : (_jsx("div", { children: _jsx(AccessBadge, { entitlement: entitlement, messages: messages, locale: config.locale, timezone: config.timezone }) })), _jsx(GrantHistory, { rows: history, revokeAction: revokeGrantAction, messages: messages })] }))] }) })] }));
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ClassNames, LinkComponentType } from "@softure-ai/ui";
|
|
2
|
+
import { type PricingTilesSlot } from "../ui/pricing-tiles.js";
|
|
3
|
+
export interface PricingProps {
|
|
4
|
+
/** The app's link component, e.g. Next's `Link`; a plain `<a>` when omitted. */
|
|
5
|
+
readonly LinkComponent?: LinkComponentType;
|
|
6
|
+
readonly classNames?: ClassNames<PricingTilesSlot>;
|
|
7
|
+
readonly unstyled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** The payment page with a plan chosen. */
|
|
10
|
+
export declare function getPlanPaymentHref(paymentRoute: string, planId: string): string;
|
|
11
|
+
export declare function Pricing({ LinkComponent, classNames, unstyled }: PricingProps): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/next/pricing.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAIpE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,OAAO,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAgB5E"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The pricing tiles wired to the registered config, for any server component (a landing or a
|
|
3
|
+
// pricing page): `<Pricing LinkComponent={Link} />`. Each tile's button opens the payment page with
|
|
4
|
+
// its plan; the page asks a visitor without a session to log in first.
|
|
5
|
+
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
6
|
+
import { PLAN_FIELD } from "../fields.js";
|
|
7
|
+
import { getBillingMessages, getBillingRoutes } from "../server/options.js";
|
|
8
|
+
import { getBillingPlans } from "../server/plans.js";
|
|
9
|
+
import { PricingTiles } from "../ui/pricing-tiles.js";
|
|
10
|
+
/** The payment page with a plan chosen. */
|
|
11
|
+
export function getPlanPaymentHref(paymentRoute, planId) {
|
|
12
|
+
return `${paymentRoute}?${PLAN_FIELD}=${encodeURIComponent(planId)}`;
|
|
13
|
+
}
|
|
14
|
+
export function Pricing({ LinkComponent, classNames, unstyled }) {
|
|
15
|
+
const config = getSoftureConfig();
|
|
16
|
+
const messages = getBillingMessages(config);
|
|
17
|
+
const payment = getBillingRoutes(config).payment;
|
|
18
|
+
return (_jsx(PricingTiles, { plans: getBillingPlans(config), messages: messages, locale: config.locale, getPlanHref: (plan) => getPlanPaymentHref(payment, plan.id), LinkComponent: LinkComponent, label: messages.payment.plansLabel, classNames: classNames, unstyled: unstyled }));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/next/pricing.tsx"],"names":[],"mappings":";AAAA,6FAA6F;AAC7F,oGAAoG;AACpG,uEAAuE;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAyB,MAAM,wBAAwB,CAAC;AAS7E,2CAA2C;AAC3C,MAAM,UAAU,kBAAkB,CAAC,YAAoB,EAAE,MAAc;IACrE,OAAO,GAAG,YAAY,IAAI,UAAU,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAgB;IAC3E,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;IACjD,OAAO,CACL,KAAC,YAAY,IACX,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,EAC9B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EACrB,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,EAC3D,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,UAAU,EAClC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,QAAQ,GAClB,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const STRIPE_WEBHOOK_SECRET_ENV = "STRIPE_WEBHOOK_SECRET";
|
|
2
|
+
/** Stripe's checkout and charge events are a few kilobytes; anything far larger is not Stripe. */
|
|
3
|
+
export declare const STRIPE_WEBHOOK_MAX_BYTES: number;
|
|
4
|
+
/**
|
|
5
|
+
* Handles one delivery: 200 once it is recorded (also again, and for events billing ignores), 400
|
|
6
|
+
* for a delivery that is not signed by Stripe within the tolerance, 413 for an oversized body, 500
|
|
7
|
+
* when the secret is missing or the database fails, so Stripe retries. A payment whose account or
|
|
8
|
+
* plan is gone answers 200 and is logged: a retry cannot fix it, the owner refunds it in Stripe.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripeWebhookRoute(request: Request): Promise<Response>;
|
|
11
|
+
//# sourceMappingURL=route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/next/route.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AACjE,kGAAkG;AAClG,eAAO,MAAM,wBAAwB,QAAa,CAAC;AAQnD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0B5E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The Stripe webhook route. Mount it with a rename in app/api/billing/webhook/route.ts:
|
|
2
|
+
// `export { stripeWebhookRoute as POST } from "@softure-ai/billing/next"`, and point a Stripe
|
|
3
|
+
// webhook endpoint at it for `checkout.session.completed`, `checkout.session.async_payment_succeeded`
|
|
4
|
+
// and `charge.refunded`.
|
|
5
|
+
//
|
|
6
|
+
// Stripe calls it from its own servers: no session, no cookies, so it stays outside any auth
|
|
7
|
+
// guard. There is no rate limit on purpose: Stripe sends from a few addresses, and a bucket would
|
|
8
|
+
// drop real payments. The body is capped and the signature checked before the database is touched.
|
|
9
|
+
import { errorLogLabel } from "@softure-ai/core";
|
|
10
|
+
import { readSmallBody } from "@softure-ai/security";
|
|
11
|
+
import { receiveStripeWebhook } from "../server/payments.js";
|
|
12
|
+
import { STRIPE_SIGNATURE_HEADER } from "../stripe-webhook.js";
|
|
13
|
+
import { getBillingContext } from "./context.js";
|
|
14
|
+
export const STRIPE_WEBHOOK_SECRET_ENV = "STRIPE_WEBHOOK_SECRET";
|
|
15
|
+
/** Stripe's checkout and charge events are a few kilobytes; anything far larger is not Stripe. */
|
|
16
|
+
export const STRIPE_WEBHOOK_MAX_BYTES = 256 * 1024;
|
|
17
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
18
|
+
function answer(status) {
|
|
19
|
+
return new Response(null, { status, headers: NO_STORE });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Handles one delivery: 200 once it is recorded (also again, and for events billing ignores), 400
|
|
23
|
+
* for a delivery that is not signed by Stripe within the tolerance, 413 for an oversized body, 500
|
|
24
|
+
* when the secret is missing or the database fails, so Stripe retries. A payment whose account or
|
|
25
|
+
* plan is gone answers 200 and is logged: a retry cannot fix it, the owner refunds it in Stripe.
|
|
26
|
+
*/
|
|
27
|
+
export async function stripeWebhookRoute(request) {
|
|
28
|
+
const secret = (process.env[STRIPE_WEBHOOK_SECRET_ENV] ?? "").trim();
|
|
29
|
+
if (secret === "") {
|
|
30
|
+
console.error(`@softure-ai/billing: the Stripe webhook has no secret; set ${STRIPE_WEBHOOK_SECRET_ENV}`);
|
|
31
|
+
return answer(500);
|
|
32
|
+
}
|
|
33
|
+
const body = await readSmallBody(request, { maxBytes: STRIPE_WEBHOOK_MAX_BYTES });
|
|
34
|
+
if (!body.ok)
|
|
35
|
+
return answer(body.error === "security.body_too_large" ? 413 : 400);
|
|
36
|
+
let received;
|
|
37
|
+
try {
|
|
38
|
+
received = await receiveStripeWebhook(await getBillingContext(), {
|
|
39
|
+
payload: body.value,
|
|
40
|
+
signature: request.headers.get(STRIPE_SIGNATURE_HEADER),
|
|
41
|
+
secret,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`@softure-ai/billing: a Stripe webhook failed: ${errorLogLabel(error)}`);
|
|
46
|
+
return answer(500);
|
|
47
|
+
}
|
|
48
|
+
if (!received.ok)
|
|
49
|
+
return answer(400);
|
|
50
|
+
const { outcome } = received.value;
|
|
51
|
+
if (outcome.status === "refused") {
|
|
52
|
+
console.error(`@softure-ai/billing: the paid Stripe checkout ${outcome.checkoutId} granted nothing (${outcome.error}); refund it in the Stripe dashboard`);
|
|
53
|
+
}
|
|
54
|
+
return answer(200);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/next/route.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,8FAA8F;AAC9F,sGAAsG;AACtG,yBAAyB;AACzB,EAAE;AACF,6FAA6F;AAC7F,kGAAkG;AAClG,mGAAmG;AACnG,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB,CAAC;AACjE,kGAAkG;AAClG,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,GAAG,IAAI,CAAC;AAEnD,MAAM,QAAQ,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;AAEjD,SAAS,MAAM,CAAC,MAAc;IAC5B,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAgB;IACvD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrE,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,8DAA8D,yBAAyB,EAAE,CAAC,CAAC;QACzG,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,wBAAwB,EAAE,CAAC,CAAC;IAClF,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,yBAAyB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAElF,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,oBAAoB,CAAC,MAAM,iBAAiB,EAAE,EAAE;YAC/D,OAAO,EAAE,IAAI,CAAC,KAAK;YACnB,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;YACvD,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,iDAAiD,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACvF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC;IACnC,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,iDAAiD,OAAO,CAAC,UAAU,qBAAqB,OAAO,CAAC,KAAK,sCAAsC,CAAC,CAAC;IAC7J,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type PaymentProvider } from "./payment.js";
|
|
3
|
+
/** The longest trial or reminder window, in days. */
|
|
4
|
+
export declare const MAX_DAYS = 365;
|
|
5
|
+
/** The most plans the config declares. */
|
|
6
|
+
export declare const MAX_PLANS = 12;
|
|
7
|
+
/** The most feature lines one plan lists. */
|
|
8
|
+
export declare const MAX_FEATURES = 20;
|
|
9
|
+
/** The highest price, in the currency's minor unit. */
|
|
10
|
+
export declare const MAX_PRICE_AMOUNT = 100000000;
|
|
11
|
+
/** The most units one period counts. */
|
|
12
|
+
export declare const MAX_PERIOD_COUNT = 1000;
|
|
13
|
+
/** How long an open invoice request waits by default before it expires, in days. */
|
|
14
|
+
export declare const DEFAULT_REQUEST_EXPIRY_DAYS = 30;
|
|
15
|
+
/**
|
|
16
|
+
* What a partial refund of a provider payment does to access: `pro_rata` takes back the refunded
|
|
17
|
+
* share of the payment's unused days, `keep_access` takes back nothing until the whole payment is
|
|
18
|
+
* refunded.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PARTIAL_REFUND_POLICIES: readonly ["pro_rata", "keep_access"];
|
|
21
|
+
export type PartialRefundPolicy = (typeof PARTIAL_REFUND_POLICIES)[number];
|
|
22
|
+
export declare const billingOptionsSchema: z.ZodObject<{
|
|
23
|
+
trial: z.ZodPrefault<z.ZodObject<{
|
|
24
|
+
days: z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
reminderDays: z.ZodDefault<z.ZodNumber>;
|
|
26
|
+
startsAt: z.ZodOptional<z.ZodString>;
|
|
27
|
+
}, z.core.$strict>>;
|
|
28
|
+
paid: z.ZodPrefault<z.ZodObject<{
|
|
29
|
+
reminderDays: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
}, z.core.$strict>>;
|
|
31
|
+
requests: z.ZodPrefault<z.ZodObject<{
|
|
32
|
+
expireAfterDays: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
plans: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
name: z.ZodRecord<z.ZodEnum<{
|
|
37
|
+
en: "en";
|
|
38
|
+
pl: "pl";
|
|
39
|
+
}> & z.core.$partial, z.ZodString>;
|
|
40
|
+
description: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
41
|
+
en: "en";
|
|
42
|
+
pl: "pl";
|
|
43
|
+
}> & z.core.$partial, z.ZodString>>;
|
|
44
|
+
price: z.ZodObject<{
|
|
45
|
+
amount: z.ZodNumber;
|
|
46
|
+
currency: z.ZodString;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
period: z.ZodUnion<readonly [z.ZodPipe<z.ZodEnum<{
|
|
49
|
+
year: "year";
|
|
50
|
+
month: "month";
|
|
51
|
+
day: "day";
|
|
52
|
+
lifetime: "lifetime";
|
|
53
|
+
week: "week";
|
|
54
|
+
}>, z.ZodTransform<{
|
|
55
|
+
readonly unit: import("./contract.js").PeriodUnit;
|
|
56
|
+
readonly count: number;
|
|
57
|
+
} | {
|
|
58
|
+
readonly unit: "lifetime";
|
|
59
|
+
}, "year" | "month" | "day" | "lifetime" | "week">>, z.ZodObject<{
|
|
60
|
+
unit: z.ZodEnum<{
|
|
61
|
+
year: "year";
|
|
62
|
+
month: "month";
|
|
63
|
+
day: "day";
|
|
64
|
+
week: "week";
|
|
65
|
+
}>;
|
|
66
|
+
count: z.ZodDefault<z.ZodNumber>;
|
|
67
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
68
|
+
unit: z.ZodLiteral<"lifetime">;
|
|
69
|
+
}, z.core.$strict>]>;
|
|
70
|
+
features: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodEnum<{
|
|
71
|
+
en: "en";
|
|
72
|
+
pl: "pl";
|
|
73
|
+
}> & z.core.$partial, z.ZodString>>>;
|
|
74
|
+
isFeatured: z.ZodDefault<z.ZodBoolean>;
|
|
75
|
+
}, z.core.$strict>>>;
|
|
76
|
+
payment: z.ZodOptional<z.ZodCustom<PaymentProvider, PaymentProvider>>;
|
|
77
|
+
partialRefunds: z.ZodDefault<z.ZodEnum<{
|
|
78
|
+
pro_rata: "pro_rata";
|
|
79
|
+
keep_access: "keep_access";
|
|
80
|
+
}>>;
|
|
81
|
+
adminRole: z.ZodDefault<z.ZodString>;
|
|
82
|
+
}, z.core.$strict>;
|
|
83
|
+
export type BillingOptionsInput = z.input<typeof billingOptionsSchema>;
|
|
84
|
+
export type BillingOptions = z.output<typeof billingOptionsSchema>;
|
|
85
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGvE,qDAAqD;AACrD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,0CAA0C;AAC1C,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,6CAA6C;AAC7C,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,YAAc,CAAC;AAC5C,wCAAwC;AACxC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,oFAAoF;AACpF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,sCAAuC,CAAC;AAC5E,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAkC3E,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgE/B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The options an app passes to `billing({ ... })` in softure.config.ts, parsed at startup.
|
|
2
|
+
import { LOCALES } from "@softure-ai/core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { parseDay } from "./calendar.js";
|
|
5
|
+
import { PERIOD_UNITS } from "./contract.js";
|
|
6
|
+
import { isPaymentProvider } from "./payment.js";
|
|
7
|
+
import { isSupportedCurrency } from "./price.js";
|
|
8
|
+
/** The longest trial or reminder window, in days. */
|
|
9
|
+
export const MAX_DAYS = 365;
|
|
10
|
+
/** The most plans the config declares. */
|
|
11
|
+
export const MAX_PLANS = 12;
|
|
12
|
+
/** The most feature lines one plan lists. */
|
|
13
|
+
export const MAX_FEATURES = 20;
|
|
14
|
+
/** The highest price, in the currency's minor unit. */
|
|
15
|
+
export const MAX_PRICE_AMOUNT = 100_000_000;
|
|
16
|
+
/** The most units one period counts. */
|
|
17
|
+
export const MAX_PERIOD_COUNT = 1000;
|
|
18
|
+
/** How long an open invoice request waits by default before it expires, in days. */
|
|
19
|
+
export const DEFAULT_REQUEST_EXPIRY_DAYS = 30;
|
|
20
|
+
/**
|
|
21
|
+
* What a partial refund of a provider payment does to access: `pro_rata` takes back the refunded
|
|
22
|
+
* share of the payment's unused days, `keep_access` takes back nothing until the whole payment is
|
|
23
|
+
* refunded.
|
|
24
|
+
*/
|
|
25
|
+
export const PARTIAL_REFUND_POLICIES = ["pro_rata", "keep_access"];
|
|
26
|
+
const daysSchema = z.number().int().min(0).max(MAX_DAYS);
|
|
27
|
+
/** Kebab-case, at most 64 characters. */
|
|
28
|
+
const PLAN_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
29
|
+
/** Copy per locale; a locale without its own text falls back to `en`. */
|
|
30
|
+
const localizedTextSchema = z
|
|
31
|
+
.partialRecord(z.enum(LOCALES), z.string().trim().min(1).max(500))
|
|
32
|
+
.refine((text) => text.en !== undefined, "needs at least an en text");
|
|
33
|
+
const periodSchema = z.union([
|
|
34
|
+
z.enum([...PERIOD_UNITS, "lifetime"]).transform((unit) => (unit === "lifetime" ? { unit } : { unit, count: 1 })),
|
|
35
|
+
z.strictObject({ unit: z.enum(PERIOD_UNITS), count: z.number().int().min(1).max(MAX_PERIOD_COUNT).default(1) }),
|
|
36
|
+
z.strictObject({ unit: z.literal("lifetime") }),
|
|
37
|
+
]);
|
|
38
|
+
const planSchema = z.strictObject({
|
|
39
|
+
/** Kebab-case and unique, e.g. `monthly`; the payment page and the providers name the plan by it. */
|
|
40
|
+
id: z.string().max(64, "must be at most 64 characters").regex(PLAN_ID_PATTERN, "must be kebab-case, e.g. pro-yearly"),
|
|
41
|
+
name: localizedTextSchema,
|
|
42
|
+
description: localizedTextSchema.optional(),
|
|
43
|
+
price: z.strictObject({
|
|
44
|
+
/** In the currency's minor unit: 2900 is 29.00 PLN. */
|
|
45
|
+
amount: z.number().int().min(0).max(MAX_PRICE_AMOUNT),
|
|
46
|
+
currency: z.string().refine((code) => /^[A-Z]{3}$/.test(code) && isSupportedCurrency(code), "must be an upper-case ISO 4217 currency code billing knows, e.g. PLN"),
|
|
47
|
+
}),
|
|
48
|
+
/** `"month"`, `"year"`, `"lifetime"`, or `{ unit, count }` such as `{ unit: "month", count: 3 }`. */
|
|
49
|
+
period: periodSchema,
|
|
50
|
+
features: z.array(localizedTextSchema).max(MAX_FEATURES).default([]),
|
|
51
|
+
isFeatured: z.boolean().default(false),
|
|
52
|
+
});
|
|
53
|
+
export const billingOptionsSchema = z.strictObject({
|
|
54
|
+
trial: z
|
|
55
|
+
.strictObject({
|
|
56
|
+
/** Length of the trial every account starts with, the registration day included; 0 for none. */
|
|
57
|
+
days: daysSchema.default(14),
|
|
58
|
+
/** From how many days left the trial notice shows; 0 never. */
|
|
59
|
+
reminderDays: daysSchema.default(3),
|
|
60
|
+
/**
|
|
61
|
+
* The first day a trial can start, `YYYY-MM-DD` in the app's time zone: an account created
|
|
62
|
+
* before it (without an entitlement row) gets its trial from this day. For turning billing on
|
|
63
|
+
* for accounts that already exist.
|
|
64
|
+
*/
|
|
65
|
+
startsAt: z
|
|
66
|
+
.string()
|
|
67
|
+
.refine((day) => parseDay(day) !== null, "must be a calendar day as YYYY-MM-DD, e.g. 2026-11-01")
|
|
68
|
+
.optional(),
|
|
69
|
+
})
|
|
70
|
+
.prefault({}),
|
|
71
|
+
paid: z
|
|
72
|
+
.strictObject({
|
|
73
|
+
/** From how many days left the renewal notice shows; 0 never. Lifetime access never ends. */
|
|
74
|
+
reminderDays: daysSchema.default(7),
|
|
75
|
+
})
|
|
76
|
+
.prefault({}),
|
|
77
|
+
requests: z
|
|
78
|
+
.strictObject({
|
|
79
|
+
/**
|
|
80
|
+
* Days an open invoice request waits for the admin, counted from the buyer's last ask:
|
|
81
|
+
* `expireStaleRequests` (run daily) then closes it as `expired` and clears its invoice
|
|
82
|
+
* details. From 1 to 365.
|
|
83
|
+
*/
|
|
84
|
+
expireAfterDays: z.number().int().min(1).max(MAX_DAYS).default(DEFAULT_REQUEST_EXPIRY_DAYS),
|
|
85
|
+
})
|
|
86
|
+
.prefault({}),
|
|
87
|
+
/** The plans the pricing tiles and the payment page offer, in the order they show them. */
|
|
88
|
+
plans: z
|
|
89
|
+
.array(planSchema)
|
|
90
|
+
.max(MAX_PLANS)
|
|
91
|
+
.default([])
|
|
92
|
+
.superRefine((plans, context) => {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
plans.forEach((plan, index) => {
|
|
95
|
+
if (seen.has(plan.id))
|
|
96
|
+
context.addIssue({ code: "custom", message: `repeats the plan id "${plan.id}"`, path: [index, "id"] });
|
|
97
|
+
seen.add(plan.id);
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
/** The payment adapter, e.g. `manual({ onRequest })`; the payment page needs one. */
|
|
101
|
+
payment: z.custom(isPaymentProvider, "must be a payment provider such as manual()").optional(),
|
|
102
|
+
/**
|
|
103
|
+
* A partial refund of a provider payment: `pro_rata` takes back the share of the payment's unused
|
|
104
|
+
* days that the refunded money is of the money not refunded before (whole days, rounded down);
|
|
105
|
+
* `keep_access` takes back nothing. Either way the refund that completes the amount acts as a
|
|
106
|
+
* full refund, and a lifetime ends only then.
|
|
107
|
+
*/
|
|
108
|
+
partialRefunds: z.enum(PARTIAL_REFUND_POLICIES).default("pro_rata"),
|
|
109
|
+
/** The auth role that may grant plans in the admin page; declared in `auth({ roles })` unless `admin`. */
|
|
110
|
+
adminRole: z.string().min(1).default("admin"),
|
|
111
|
+
}).superRefine((options, context) => {
|
|
112
|
+
const { payment } = options;
|
|
113
|
+
if (payment?.checkPrice === undefined)
|
|
114
|
+
return;
|
|
115
|
+
options.plans.forEach((plan, index) => {
|
|
116
|
+
const problem = payment.checkPrice?.(plan.price) ?? null;
|
|
117
|
+
if (problem !== null)
|
|
118
|
+
context.addIssue({ code: "custom", message: problem, path: ["plans", index, "price"] });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Err, ModuleContext, Ok } from "@softure-ai/core";
|
|
2
|
+
import type { Queryable } from "@softure-ai/db";
|
|
3
|
+
import type { Plan, PlanPrice } from "./contract.js";
|
|
4
|
+
/** The signed-in account that pays. */
|
|
5
|
+
export interface PaymentAccount {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly email: string;
|
|
8
|
+
}
|
|
9
|
+
/** What an invoice needs, as the buyer typed it (trimmed). */
|
|
10
|
+
export interface InvoiceDetails {
|
|
11
|
+
/** A person's or a company's name. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** A tax number such as a Polish NIP or an EU VAT id; null when not given. */
|
|
14
|
+
readonly taxId: string | null;
|
|
15
|
+
readonly address: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PaymentRequest {
|
|
18
|
+
readonly plan: Plan;
|
|
19
|
+
readonly account: PaymentAccount;
|
|
20
|
+
/** The details the payment page asked for; null when the provider collects its own. */
|
|
21
|
+
readonly invoice: InvoiceDetails | null;
|
|
22
|
+
/** The payment page as an absolute URL, where a hosted checkout sends the buyer back. */
|
|
23
|
+
readonly returnUrl: string;
|
|
24
|
+
}
|
|
25
|
+
/** Where the buyer goes next. */
|
|
26
|
+
export type PaymentStart =
|
|
27
|
+
/** A hosted checkout (e.g. a provider's payment page); the browser is sent there. */
|
|
28
|
+
{
|
|
29
|
+
readonly type: "redirect";
|
|
30
|
+
readonly url: string;
|
|
31
|
+
}
|
|
32
|
+
/** The request was handed over (e.g. an invoice requested); access follows once it is paid. */
|
|
33
|
+
| {
|
|
34
|
+
readonly type: "requested";
|
|
35
|
+
};
|
|
36
|
+
export type PaymentContext = ModuleContext<Queryable>;
|
|
37
|
+
/** A payment adapter for `billing({ payment })`, e.g. `manual({ onRequest })`. */
|
|
38
|
+
export interface PaymentProvider {
|
|
39
|
+
/** A short lowercase name for logs, e.g. `manual`. */
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Whether the payment page asks for invoice details before `startPayment`. */
|
|
42
|
+
readonly collectsInvoiceDetails: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether `startPayment` hands a request to the owner and answers `requested` (the manual
|
|
45
|
+
* adapter) rather than sending the buyer to a checkout. Such a request is stored before the
|
|
46
|
+
* call, and the call is made once per open request: asking again only refreshes the stored one.
|
|
47
|
+
*/
|
|
48
|
+
readonly handsOverRequests: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Starts paying for `request.plan`. An expected failure (the provider refused, a notification
|
|
51
|
+
* could not be sent) is `billing.payment_failed`; anything thrown is a bug or an outage.
|
|
52
|
+
*/
|
|
53
|
+
startPayment(ctx: PaymentContext, request: PaymentRequest): Promise<Ok<PaymentStart> | Err<"billing.payment_failed">>;
|
|
54
|
+
/**
|
|
55
|
+
* Why the provider cannot charge `price` (e.g. a fraction of its currency's unit), or null when it
|
|
56
|
+
* can. Run for every plan when the config loads, so the deployer meets it, not the buyer.
|
|
57
|
+
*/
|
|
58
|
+
checkPrice?(price: PlanPrice): string | null;
|
|
59
|
+
}
|
|
60
|
+
export declare function isPaymentProvider(value: unknown): value is PaymentProvider;
|
|
61
|
+
//# sourceMappingURL=payment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.d.ts","sourceRoot":"","sources":["../src/payment.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAErD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,iCAAiC;AACjC,MAAM,MAAM,YAAY;AACtB,qFAAqF;AACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACrD,+FAA+F;GAC7F;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnC,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAEtD,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC;IACtH;;;OAGG;IACH,UAAU,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAW1E"}
|
package/dist/payment.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isPaymentProvider(value) {
|
|
2
|
+
if (typeof value !== "object" || value === null)
|
|
3
|
+
return false;
|
|
4
|
+
const candidate = value;
|
|
5
|
+
return (typeof candidate.name === "string" &&
|
|
6
|
+
candidate.name !== "" &&
|
|
7
|
+
typeof candidate.collectsInvoiceDetails === "boolean" &&
|
|
8
|
+
typeof candidate.handsOverRequests === "boolean" &&
|
|
9
|
+
typeof candidate.startPayment === "function" &&
|
|
10
|
+
(candidate.checkPrice === undefined || typeof candidate.checkPrice === "function"));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=payment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment.js","sourceRoot":"","sources":["../src/payment.ts"],"names":[],"mappings":"AAiEA,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAwD,CAAC;IAC3E,OAAO,CACL,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ;QAClC,SAAS,CAAC,IAAI,KAAK,EAAE;QACrB,OAAO,SAAS,CAAC,sBAAsB,KAAK,SAAS;QACrD,OAAO,SAAS,CAAC,iBAAiB,KAAK,SAAS;QAChD,OAAO,SAAS,CAAC,YAAY,KAAK,UAAU;QAC5C,CAAC,SAAS,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU,CAAC,CACnF,CAAC;AACJ,CAAC"}
|