@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,129 @@
|
|
|
1
|
+
// Plans on the server: the configured list, granting a plan to an account (the one path the
|
|
2
|
+
// manual admin page and a provider's webhook take) and starting a payment through the provider.
|
|
3
|
+
import { users } from "@softure-ai/auth";
|
|
4
|
+
import { err, ok } from "@softure-ai/core";
|
|
5
|
+
import { consumeRateLimit, subjectKey } from "@softure-ai/security/server";
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
import { parseInvoiceDetails } from "../invoice.js";
|
|
8
|
+
import { findPlan, getPlanGrant } from "../plans.js";
|
|
9
|
+
import { getPaymentGrant } from "../refund.js";
|
|
10
|
+
import { changeEntitlement, findEntitlementRecord } from "./entitlements.js";
|
|
11
|
+
import { getBillingOptions, getBillingRoutes } from "./options.js";
|
|
12
|
+
import { claimHandOver, confirmHandOver, recordPaymentRequest, releaseHandOver } from "./requests.js";
|
|
13
|
+
import { isUserId } from "./user-id.js";
|
|
14
|
+
import { assertPaymentSetup, PAYMENT_BUCKET } from "./setup.js";
|
|
15
|
+
/** The plans of `billing({ plans })`, in their order. */
|
|
16
|
+
export function getBillingPlans(config) {
|
|
17
|
+
return getBillingOptions(config).plans;
|
|
18
|
+
}
|
|
19
|
+
/** The provider of `billing({ payment })`. Throws when the app set none: a payment page without one is a bug. */
|
|
20
|
+
export function getPaymentProvider(config) {
|
|
21
|
+
const provider = getBillingOptions(config).payment;
|
|
22
|
+
if (provider === undefined)
|
|
23
|
+
throw new Error("@softure-ai/billing: billing({ payment }) is not set; the payment page needs a provider such as manual()");
|
|
24
|
+
return provider;
|
|
25
|
+
}
|
|
26
|
+
/** The account with this email (compared trimmed and lower-cased, as auth stores it), or null. */
|
|
27
|
+
export async function findAccountByEmail(ctx, email) {
|
|
28
|
+
const [row] = await ctx.db
|
|
29
|
+
.select({ id: users.id, email: users.email })
|
|
30
|
+
.from(users)
|
|
31
|
+
.where(eq(users.email, email.trim().toLowerCase()))
|
|
32
|
+
.limit(1);
|
|
33
|
+
return row ?? null;
|
|
34
|
+
}
|
|
35
|
+
/** The account with this id, or null (also for an id that is not a uuid). */
|
|
36
|
+
export async function findAccountById(ctx, id) {
|
|
37
|
+
if (!isUserId(id))
|
|
38
|
+
return null;
|
|
39
|
+
const [row] = await ctx.db.select({ id: users.id, email: users.email }).from(users).where(eq(users.id, id));
|
|
40
|
+
return row ?? null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `grantPlan` that also says what the grant added, for the payment row that pays for it. The event
|
|
44
|
+
* is computed under the entitlement's lock and may be computed twice (a concurrent first change), so
|
|
45
|
+
* the grant is taken from the last computation, the one applied.
|
|
46
|
+
*/
|
|
47
|
+
export async function applyPlan(ctx, userId, planId) {
|
|
48
|
+
const plan = findPlan(getBillingPlans(ctx.config), planId);
|
|
49
|
+
if (plan === undefined)
|
|
50
|
+
return err("billing.plan_unknown");
|
|
51
|
+
let grant = null;
|
|
52
|
+
const changed = await changeEntitlement(ctx, userId, (record, now) => {
|
|
53
|
+
const event = getPlanGrant(record, plan, now, ctx.config.timezone);
|
|
54
|
+
grant = getPaymentGrant(record, event, now);
|
|
55
|
+
return event;
|
|
56
|
+
});
|
|
57
|
+
return changed.ok ? ok({ entitlement: changed.value, grant }) : changed;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Grants one payment of the plan: a paid period that starts when the account's current access ends,
|
|
61
|
+
* or lifetime access. Computed under the entitlement's lock, so two grants at once give two periods.
|
|
62
|
+
* It records nothing: a grant made here is not in the account's history and cannot be revoked; an
|
|
63
|
+
* admin's grant goes through `grantPlanManually`.
|
|
64
|
+
*/
|
|
65
|
+
export async function grantPlan(ctx, userId, planId) {
|
|
66
|
+
const applied = await applyPlan(ctx, userId, planId);
|
|
67
|
+
return applied.ok ? ok(applied.value.entitlement) : applied;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Starts paying for a plan: counts `billing-payment` per account first, checks the plan, that the
|
|
71
|
+
* account has no lifetime access yet and, for a provider that needs them, the invoice details, then
|
|
72
|
+
* starts the provider. A provider that hands requests over (`handsOverRequests`, the manual adapter)
|
|
73
|
+
* gets the request stored first and is called once per open request: asking again refreshes the
|
|
74
|
+
* stored request and answers `requested` without a second hand-over, and a hand-over that failed
|
|
75
|
+
* (an `Err` or a throw) is released, so the next ask tries again; a claim left without an answer (the
|
|
76
|
+
* process stopped) is taken over by an ask a minute later. Database errors and provider
|
|
77
|
+
* throws propagate; a provider whose answer contradicts `handsOverRequests` throws.
|
|
78
|
+
*/
|
|
79
|
+
export async function startPayment(ctx, input) {
|
|
80
|
+
assertPaymentSetup(ctx.config);
|
|
81
|
+
const provider = getPaymentProvider(ctx.config);
|
|
82
|
+
const limited = await consumeRateLimit(ctx, { bucket: PAYMENT_BUCKET, key: subjectKey(`account:${input.account.id}`) });
|
|
83
|
+
if (!limited.ok)
|
|
84
|
+
return err("security.rate_limited");
|
|
85
|
+
const plan = findPlan(getBillingPlans(ctx.config), input.planId);
|
|
86
|
+
if (plan === undefined)
|
|
87
|
+
return err("billing.plan_unknown");
|
|
88
|
+
const record = await findEntitlementRecord(ctx, input.account.id);
|
|
89
|
+
if (record?.isLifetime === true)
|
|
90
|
+
return err("billing.lifetime_active");
|
|
91
|
+
let invoice = null;
|
|
92
|
+
if (provider.collectsInvoiceDetails) {
|
|
93
|
+
const parsed = parseInvoiceDetails(input.invoice);
|
|
94
|
+
if (!parsed.ok)
|
|
95
|
+
return parsed;
|
|
96
|
+
invoice = parsed.value;
|
|
97
|
+
}
|
|
98
|
+
const returnUrl = new URL(getBillingRoutes(ctx.config).payment, ctx.config.appOrigin).toString();
|
|
99
|
+
const request = { plan, account: input.account, invoice, returnUrl };
|
|
100
|
+
if (!provider.handsOverRequests) {
|
|
101
|
+
const started = await provider.startPayment(ctx, request);
|
|
102
|
+
if (started.ok && started.value.type === "requested")
|
|
103
|
+
throw new Error(`@softure-ai/billing: provider "${provider.name}" answered requested but does not set handsOverRequests`);
|
|
104
|
+
return started;
|
|
105
|
+
}
|
|
106
|
+
const requestId = await recordPaymentRequest(ctx, { userId: input.account.id, planId: plan.id, invoice, price: plan.price });
|
|
107
|
+
const claimedAt = await claimHandOver(ctx, requestId);
|
|
108
|
+
// Handed over before (asking again refreshed its details), or being handed over by a concurrent ask
|
|
109
|
+
// whose claim is younger than a minute.
|
|
110
|
+
if (claimedAt === null)
|
|
111
|
+
return ok({ type: "requested" });
|
|
112
|
+
let started;
|
|
113
|
+
try {
|
|
114
|
+
started = await provider.startPayment(ctx, request);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
await releaseHandOver(ctx, requestId, claimedAt);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (!started.ok) {
|
|
121
|
+
await releaseHandOver(ctx, requestId, claimedAt);
|
|
122
|
+
return started;
|
|
123
|
+
}
|
|
124
|
+
if (started.value.type !== "requested")
|
|
125
|
+
throw new Error(`@softure-ai/billing: provider "${provider.name}" sets handsOverRequests but answered ${started.value.type}`);
|
|
126
|
+
await confirmHandOver(ctx, requestId);
|
|
127
|
+
return started;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=plans.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plans.js","sourceRoot":"","sources":["../../src/server/plans.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ModuleContext, type Ok, type PrivacyContributor } from "@softure-ai/core";
|
|
2
|
+
/** One provider payment, as it appears in an export. */
|
|
3
|
+
export interface BillingPaymentData {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly checkoutId: string;
|
|
6
|
+
readonly paymentId: string | null;
|
|
7
|
+
readonly planId: string;
|
|
8
|
+
readonly amount: number;
|
|
9
|
+
readonly currency: string;
|
|
10
|
+
readonly status: "paid" | "refunded";
|
|
11
|
+
readonly paidAt: Date;
|
|
12
|
+
readonly refundedAt: Date | null;
|
|
13
|
+
/** The total refunded so far, in the currency's minor unit. */
|
|
14
|
+
readonly refundedAmount: number;
|
|
15
|
+
/** What the payment granted: `period` (from, until) or `lifetime`; null when recorded before grants were. */
|
|
16
|
+
readonly grantKind: "period" | "lifetime" | null;
|
|
17
|
+
readonly grantedFrom: Date | null;
|
|
18
|
+
readonly grantedUntil: Date | null;
|
|
19
|
+
}
|
|
20
|
+
/** One provider refund that failed, as it appears in an export. */
|
|
21
|
+
export interface BillingRefundFailureData {
|
|
22
|
+
/** The provider's payment id of the refunded payment (`BillingPaymentData.paymentId`). */
|
|
23
|
+
readonly paymentId: string | null;
|
|
24
|
+
readonly refundId: string;
|
|
25
|
+
/** What the refund was for, in the currency's minor unit. */
|
|
26
|
+
readonly amount: number;
|
|
27
|
+
readonly refundCreatedAt: Date;
|
|
28
|
+
readonly failedAt: Date;
|
|
29
|
+
}
|
|
30
|
+
/** One invoice request, as it appears in an export; the details are gone once it was closed. */
|
|
31
|
+
export interface BillingPaymentRequestData {
|
|
32
|
+
readonly planId: string;
|
|
33
|
+
readonly invoiceName: string | null;
|
|
34
|
+
readonly invoiceTaxId: string | null;
|
|
35
|
+
readonly invoiceAddress: string | null;
|
|
36
|
+
/** The plan's price the request quoted, in the currency's minor unit; null before prices were recorded. */
|
|
37
|
+
readonly amount: number | null;
|
|
38
|
+
readonly currency: string | null;
|
|
39
|
+
readonly status: "open" | "granted" | "dismissed" | "expired";
|
|
40
|
+
readonly requestedAt: Date;
|
|
41
|
+
readonly closedAt: Date | null;
|
|
42
|
+
}
|
|
43
|
+
/** One plan granted by hand, as it appears in an export. */
|
|
44
|
+
export interface BillingManualGrantData {
|
|
45
|
+
readonly planId: string;
|
|
46
|
+
readonly grantedAt: Date;
|
|
47
|
+
readonly grantKind: "period" | "lifetime";
|
|
48
|
+
readonly grantedFrom: Date | null;
|
|
49
|
+
readonly grantedUntil: Date | null;
|
|
50
|
+
readonly status: "active" | "revoked";
|
|
51
|
+
readonly revokedAt: Date | null;
|
|
52
|
+
/** What it was granted for, in the currency's minor unit; null before prices were recorded. */
|
|
53
|
+
readonly amount: number | null;
|
|
54
|
+
readonly currency: string | null;
|
|
55
|
+
}
|
|
56
|
+
/** What billing holds about one user, as it appears in their export. */
|
|
57
|
+
export interface BillingUserData {
|
|
58
|
+
readonly entitlement: {
|
|
59
|
+
readonly trialEndsAt: Date;
|
|
60
|
+
readonly paidUntil: Date | null;
|
|
61
|
+
readonly isLifetime: boolean;
|
|
62
|
+
readonly createdAt: Date;
|
|
63
|
+
readonly updatedAt: Date;
|
|
64
|
+
} | null;
|
|
65
|
+
/** Oldest first. */
|
|
66
|
+
readonly payments: readonly BillingPaymentData[];
|
|
67
|
+
/** Oldest first. */
|
|
68
|
+
readonly refundFailures: readonly BillingRefundFailureData[];
|
|
69
|
+
/** Oldest first. */
|
|
70
|
+
readonly paymentRequests: readonly BillingPaymentRequestData[];
|
|
71
|
+
/** Oldest first. */
|
|
72
|
+
readonly manualGrants: readonly BillingManualGrantData[];
|
|
73
|
+
}
|
|
74
|
+
export declare function exportBillingUserData(context: ModuleContext, userId: string): Promise<Ok<BillingUserData>>;
|
|
75
|
+
export declare function deleteBillingUserData(context: ModuleContext, userId: string): Promise<Ok<undefined>>;
|
|
76
|
+
export declare const billingPrivacyContributor: PrivacyContributor;
|
|
77
|
+
//# sourceMappingURL=privacy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privacy.d.ts","sourceRoot":"","sources":["../../src/server/privacy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The billing part of a GDPR export and deletion (`@softure-ai/privacy`): the account's entitlement
|
|
2
|
+
// row, its provider payments and their failed refunds, its invoice requests and the plans granted
|
|
3
|
+
// to it by hand. An account
|
|
4
|
+
// without an entitlement row has no stored entitlement (its trial is derived from the account). The
|
|
5
|
+
// provider keeps its own records of the payments. Which admin granted or revoked a plan is the
|
|
6
|
+
// admin's data, not the account's, and is left out of the export.
|
|
7
|
+
import { users } from "@softure-ai/auth";
|
|
8
|
+
import { ok } from "@softure-ai/core";
|
|
9
|
+
import { asc, eq } from "drizzle-orm";
|
|
10
|
+
import { entitlements, manualGrants, paymentRequests, payments, refundFailures } from "../schema.js";
|
|
11
|
+
import { isUserId } from "./user-id.js";
|
|
12
|
+
const EMPTY_USER_DATA = { entitlement: null, payments: [], refundFailures: [], paymentRequests: [], manualGrants: [] };
|
|
13
|
+
export async function exportBillingUserData(context, userId) {
|
|
14
|
+
if (!isUserId(userId))
|
|
15
|
+
return ok(EMPTY_USER_DATA);
|
|
16
|
+
// Core types `db` as unknown; privacy passes the @softure-ai/db handle or its transaction.
|
|
17
|
+
const db = context.db;
|
|
18
|
+
const [row] = await db
|
|
19
|
+
.select({
|
|
20
|
+
trialEndsAt: entitlements.trialEndsAt,
|
|
21
|
+
paidUntil: entitlements.paidUntil,
|
|
22
|
+
isLifetime: entitlements.isLifetime,
|
|
23
|
+
createdAt: entitlements.createdAt,
|
|
24
|
+
updatedAt: entitlements.updatedAt,
|
|
25
|
+
})
|
|
26
|
+
.from(entitlements)
|
|
27
|
+
.where(eq(entitlements.userId, userId));
|
|
28
|
+
const paymentRows = await db
|
|
29
|
+
.select({
|
|
30
|
+
provider: payments.provider,
|
|
31
|
+
checkoutId: payments.checkoutId,
|
|
32
|
+
paymentId: payments.paymentId,
|
|
33
|
+
planId: payments.planId,
|
|
34
|
+
amount: payments.amount,
|
|
35
|
+
currency: payments.currency,
|
|
36
|
+
status: payments.status,
|
|
37
|
+
paidAt: payments.paidAt,
|
|
38
|
+
refundedAt: payments.refundedAt,
|
|
39
|
+
refundedAmount: payments.refundedAmount,
|
|
40
|
+
grantKind: payments.grantKind,
|
|
41
|
+
grantedFrom: payments.grantedFrom,
|
|
42
|
+
grantedUntil: payments.grantedUntil,
|
|
43
|
+
})
|
|
44
|
+
.from(payments)
|
|
45
|
+
.where(eq(payments.userId, userId))
|
|
46
|
+
.orderBy(asc(payments.paidAt), asc(payments.id));
|
|
47
|
+
const failureRows = await db
|
|
48
|
+
.select({
|
|
49
|
+
paymentId: payments.paymentId,
|
|
50
|
+
refundId: refundFailures.refundId,
|
|
51
|
+
amount: refundFailures.amount,
|
|
52
|
+
refundCreatedAt: refundFailures.refundCreatedAt,
|
|
53
|
+
failedAt: refundFailures.failedAt,
|
|
54
|
+
})
|
|
55
|
+
.from(refundFailures)
|
|
56
|
+
.innerJoin(payments, eq(payments.id, refundFailures.paymentId))
|
|
57
|
+
.where(eq(payments.userId, userId))
|
|
58
|
+
.orderBy(asc(refundFailures.failedAt), asc(refundFailures.refundId));
|
|
59
|
+
const requestRows = await db
|
|
60
|
+
.select({
|
|
61
|
+
planId: paymentRequests.planId,
|
|
62
|
+
invoiceName: paymentRequests.invoiceName,
|
|
63
|
+
invoiceTaxId: paymentRequests.invoiceTaxId,
|
|
64
|
+
invoiceAddress: paymentRequests.invoiceAddress,
|
|
65
|
+
amount: paymentRequests.amount,
|
|
66
|
+
currency: paymentRequests.currency,
|
|
67
|
+
status: paymentRequests.status,
|
|
68
|
+
requestedAt: paymentRequests.requestedAt,
|
|
69
|
+
closedAt: paymentRequests.closedAt,
|
|
70
|
+
})
|
|
71
|
+
.from(paymentRequests)
|
|
72
|
+
.where(eq(paymentRequests.userId, userId))
|
|
73
|
+
.orderBy(asc(paymentRequests.requestedAt), asc(paymentRequests.id));
|
|
74
|
+
const grantRows = await db
|
|
75
|
+
.select({
|
|
76
|
+
planId: manualGrants.planId,
|
|
77
|
+
grantedAt: manualGrants.grantedAt,
|
|
78
|
+
grantKind: manualGrants.grantKind,
|
|
79
|
+
grantedFrom: manualGrants.grantedFrom,
|
|
80
|
+
grantedUntil: manualGrants.grantedUntil,
|
|
81
|
+
status: manualGrants.status,
|
|
82
|
+
revokedAt: manualGrants.revokedAt,
|
|
83
|
+
amount: manualGrants.amount,
|
|
84
|
+
currency: manualGrants.currency,
|
|
85
|
+
})
|
|
86
|
+
.from(manualGrants)
|
|
87
|
+
.where(eq(manualGrants.userId, userId))
|
|
88
|
+
.orderBy(asc(manualGrants.grantedAt), asc(manualGrants.id));
|
|
89
|
+
return ok({ entitlement: row ?? null, payments: paymentRows, refundFailures: failureRows, paymentRequests: requestRows, manualGrants: grantRows });
|
|
90
|
+
}
|
|
91
|
+
export async function deleteBillingUserData(context, userId) {
|
|
92
|
+
if (!isUserId(userId))
|
|
93
|
+
return ok();
|
|
94
|
+
const db = context.db;
|
|
95
|
+
// Lock the account first, in the order `changeEntitlement` takes its locks (account, then
|
|
96
|
+
// entitlement): a change running at the same time then waits instead of deadlocking the erase.
|
|
97
|
+
await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).for("update");
|
|
98
|
+
await db.delete(entitlements).where(eq(entitlements.userId, userId));
|
|
99
|
+
// Their failed refunds go with them (ON DELETE CASCADE).
|
|
100
|
+
await db.delete(payments).where(eq(payments.userId, userId));
|
|
101
|
+
// Grants first: they reference the requests they answered.
|
|
102
|
+
await db.delete(manualGrants).where(eq(manualGrants.userId, userId));
|
|
103
|
+
await db.delete(paymentRequests).where(eq(paymentRequests.userId, userId));
|
|
104
|
+
return ok();
|
|
105
|
+
}
|
|
106
|
+
export const billingPrivacyContributor = {
|
|
107
|
+
exportUserData: exportBillingUserData,
|
|
108
|
+
deleteUserData: deleteBillingUserData,
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=privacy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privacy.js","sourceRoot":"","sources":["../../src/server/privacy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AccessReminderKind } from "../reminder.js";
|
|
2
|
+
import { type BillingContext } from "./entitlements.js";
|
|
3
|
+
export interface FindAccessRemindersOptions {
|
|
4
|
+
/** An ended mail is due from the day access ended through this many local days after it. Default 3. */
|
|
5
|
+
readonly catchUpDays?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AccessReminderDue {
|
|
8
|
+
readonly userId: string;
|
|
9
|
+
readonly email: string;
|
|
10
|
+
readonly kind: AccessReminderKind;
|
|
11
|
+
/** The first instant without the access the mail is about. */
|
|
12
|
+
readonly endsAt: Date;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every account due a reminder at the clock's now, ordered by end, then account. Reads only.
|
|
16
|
+
* Throws for a `catchUpDays` that is not an integer from 0 to 365 (a caller bug); database errors
|
|
17
|
+
* propagate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findAccessReminders(ctx: BillingContext, options?: FindAccessRemindersOptions): Promise<AccessReminderDue[]>;
|
|
20
|
+
//# sourceMappingURL=reminders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminders.d.ts","sourceRoot":"","sources":["../../src/server/reminders.ts"],"names":[],"mappings":"AAUA,OAAO,EAA+D,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEtH,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAG5F,MAAM,WAAW,0BAA0B;IACzC,uGAAuG;IACvG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;CACvB;AASD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAerI"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The accounts due a reminder mail now. Two bounded range queries find the candidates: stored rows
|
|
2
|
+
// whose trial or dated paid access ends inside the widest window, and accounts without a row whose
|
|
3
|
+
// derived trial does (its end day is the creation day plus `trial.days`, so the window is a range
|
|
4
|
+
// on `auth.users.created_at`, indexed by auth, plus every account under `trial.startsAt` when the
|
|
5
|
+
// floor's trial ends in it). The pure rule then decides each candidate exactly,
|
|
6
|
+
// so the ranges only need to be wide enough, never exact.
|
|
7
|
+
import { users } from "@softure-ai/auth";
|
|
8
|
+
import { and, asc, eq, gte, isNull, lt, or } from "drizzle-orm";
|
|
9
|
+
import { getDayNumber, getStartOfDay } from "../calendar.js";
|
|
10
|
+
import { DEFAULT_CATCH_UP_DAYS, getAccessReminder, MAX_CATCH_UP_DAYS } from "../reminder.js";
|
|
11
|
+
import { entitlements } from "../schema.js";
|
|
12
|
+
import { getDefaultRecord, getTrialFloorDay } from "./entitlements.js";
|
|
13
|
+
import { getBillingOptions, getEntitlementPolicy } from "./options.js";
|
|
14
|
+
/**
|
|
15
|
+
* Every account due a reminder at the clock's now, ordered by end, then account. Reads only.
|
|
16
|
+
* Throws for a `catchUpDays` that is not an integer from 0 to 365 (a caller bug); database errors
|
|
17
|
+
* propagate.
|
|
18
|
+
*/
|
|
19
|
+
export async function findAccessReminders(ctx, options = {}) {
|
|
20
|
+
const catchUpDays = options.catchUpDays ?? DEFAULT_CATCH_UP_DAYS;
|
|
21
|
+
if (!Number.isInteger(catchUpDays) || catchUpDays < 0 || catchUpDays > MAX_CATCH_UP_DAYS) {
|
|
22
|
+
throw new Error(`@softure-ai/billing: catchUpDays must be an integer from 0 to ${String(MAX_CATCH_UP_DAYS)}, got ${String(catchUpDays)}`);
|
|
23
|
+
}
|
|
24
|
+
const now = ctx.clock.now();
|
|
25
|
+
const policy = getEntitlementPolicy(ctx.config);
|
|
26
|
+
const candidates = [...(await findStoredCandidates(ctx, now, catchUpDays)), ...(await findDerivedCandidates(ctx, now, catchUpDays))];
|
|
27
|
+
const due = [];
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
const reminder = getAccessReminder({ record: candidate.record, accountCreatedAt: candidate.createdAt, now, policy, catchUpDays });
|
|
30
|
+
if (reminder !== null)
|
|
31
|
+
due.push({ userId: candidate.userId, email: candidate.email, ...reminder });
|
|
32
|
+
}
|
|
33
|
+
return due.sort((first, second) => first.endsAt.getTime() - second.endsAt.getTime() || first.userId.localeCompare(second.userId));
|
|
34
|
+
}
|
|
35
|
+
/** Stored rows without lifetime whose trial or paid end lies from the catch-up start to past the widest reminder window. */
|
|
36
|
+
async function findStoredCandidates(ctx, now, catchUpDays) {
|
|
37
|
+
const { timezone } = ctx.config;
|
|
38
|
+
const { trial, paid } = getBillingOptions(ctx.config);
|
|
39
|
+
const today = getDayNumber(now, timezone);
|
|
40
|
+
const from = getStartOfDay(today - catchUpDays, timezone);
|
|
41
|
+
const to = getStartOfDay(today + Math.max(trial.reminderDays, paid.reminderDays) + 1, timezone);
|
|
42
|
+
const rows = await ctx.db
|
|
43
|
+
.select({
|
|
44
|
+
userId: users.id,
|
|
45
|
+
email: users.email,
|
|
46
|
+
createdAt: users.createdAt,
|
|
47
|
+
trialEndsAt: entitlements.trialEndsAt,
|
|
48
|
+
paidUntil: entitlements.paidUntil,
|
|
49
|
+
isLifetime: entitlements.isLifetime,
|
|
50
|
+
})
|
|
51
|
+
.from(entitlements)
|
|
52
|
+
.innerJoin(users, eq(users.id, entitlements.userId))
|
|
53
|
+
.where(and(eq(entitlements.isLifetime, false), or(and(gte(entitlements.trialEndsAt, from), lt(entitlements.trialEndsAt, to)), and(gte(entitlements.paidUntil, from), lt(entitlements.paidUntil, to)))))
|
|
54
|
+
.orderBy(asc(users.id));
|
|
55
|
+
return rows.map(({ trialEndsAt, paidUntil, isLifetime, ...account }) => ({ ...account, record: { trialEndsAt, paidUntil, isLifetime } }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Accounts without a row whose derived trial ends in the same span: a trial ends at the start of
|
|
59
|
+
* the creation day plus `trial.days`, so the span moves back by `trial.days` onto `created_at`.
|
|
60
|
+
* Under `trial.startsAt` every account created before the floor day shares one trial end; when that
|
|
61
|
+
* end lies in the span, they are all candidates.
|
|
62
|
+
*/
|
|
63
|
+
async function findDerivedCandidates(ctx, now, catchUpDays) {
|
|
64
|
+
const { timezone } = ctx.config;
|
|
65
|
+
const { trial } = getBillingOptions(ctx.config);
|
|
66
|
+
const today = getDayNumber(now, timezone);
|
|
67
|
+
const from = getStartOfDay(today - catchUpDays - trial.days, timezone);
|
|
68
|
+
const to = getStartOfDay(today + trial.reminderDays + 1 - trial.days, timezone);
|
|
69
|
+
let createdInSpan = and(gte(users.createdAt, from), lt(users.createdAt, to));
|
|
70
|
+
const floorDay = getTrialFloorDay(ctx);
|
|
71
|
+
if (floorDay !== null) {
|
|
72
|
+
const floorEndDay = floorDay + trial.days;
|
|
73
|
+
if (floorEndDay >= today - catchUpDays && floorEndDay <= today + trial.reminderDays) {
|
|
74
|
+
createdInSpan = or(createdInSpan, lt(users.createdAt, getStartOfDay(floorDay, timezone)));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const rows = await ctx.db
|
|
78
|
+
.select({ userId: users.id, email: users.email, createdAt: users.createdAt })
|
|
79
|
+
.from(users)
|
|
80
|
+
.leftJoin(entitlements, eq(entitlements.userId, users.id))
|
|
81
|
+
.where(and(isNull(entitlements.userId), createdInSpan))
|
|
82
|
+
.orderBy(asc(users.id));
|
|
83
|
+
return rows.map((account) => ({ ...account, record: getDefaultRecord(ctx, account.createdAt) }));
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=reminders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminders.js","sourceRoot":"","sources":["../../src/server/reminders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type Err, type Ok } from "@softure-ai/core";
|
|
2
|
+
import type { Queryable } from "@softure-ai/db";
|
|
3
|
+
import type { PlanPrice } from "../contract.js";
|
|
4
|
+
import type { InvoiceDetails } from "../payment.js";
|
|
5
|
+
import type { BillingContext } from "./entitlements.js";
|
|
6
|
+
/** How many open requests the admin page lists by default. */
|
|
7
|
+
export declare const OPEN_REQUESTS_LIMIT = 50;
|
|
8
|
+
export interface RecordPaymentRequestInput {
|
|
9
|
+
readonly userId: string;
|
|
10
|
+
readonly planId: string;
|
|
11
|
+
/** Null when the provider collects its own details. */
|
|
12
|
+
readonly invoice: InvoiceDetails | null;
|
|
13
|
+
/** The plan's price now: what the request quotes. */
|
|
14
|
+
readonly price: PlanPrice;
|
|
15
|
+
}
|
|
16
|
+
/** An open request, as the admin page lists it. */
|
|
17
|
+
export interface OpenPaymentRequest {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly userId: string;
|
|
20
|
+
readonly email: string;
|
|
21
|
+
readonly planId: string;
|
|
22
|
+
readonly invoice: InvoiceDetails | null;
|
|
23
|
+
/** The plan's price when it was asked for; null for a request stored before prices were. */
|
|
24
|
+
readonly price: PlanPrice | null;
|
|
25
|
+
readonly requestedAt: Date;
|
|
26
|
+
}
|
|
27
|
+
/** The columns that close a request: its status, when, and no invoice details any more. */
|
|
28
|
+
export declare function getClosedRequestColumns(status: "granted" | "dismissed" | "expired", now: Date): {
|
|
29
|
+
status: "granted" | "dismissed" | "expired";
|
|
30
|
+
closedAt: Date;
|
|
31
|
+
invoiceName: null;
|
|
32
|
+
invoiceTaxId: null;
|
|
33
|
+
invoiceAddress: null;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Stores a request before it is handed over to the owner, or refreshes the account's open request
|
|
37
|
+
* for the same plan (its details, price and time; its hand-over and claim stay). Returns the
|
|
38
|
+
* request's id. Database errors propagate.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordPaymentRequest(ctx: Pick<BillingContext, "db" | "clock">, input: RecordPaymentRequestInput): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Claims the hand-over of an open request that was not handed over yet: the time it was claimed,
|
|
43
|
+
* or null when it was handed over already, is being handed over (a claim younger than
|
|
44
|
+
* `HANDOVER_CLAIM_TIMEOUT_MS`), or is no longer open. An older claim was left without an answer and
|
|
45
|
+
* is taken over. A conditional update, so two asks at once hand it over once. Database errors
|
|
46
|
+
* propagate.
|
|
47
|
+
*/
|
|
48
|
+
export declare function claimHandOver(ctx: Pick<BillingContext, "db" | "clock">, requestId: string): Promise<Date | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Records a hand-over that answered: the request is never handed over again, whichever ask holds
|
|
51
|
+
* the claim now. The first record wins. Database errors propagate.
|
|
52
|
+
*/
|
|
53
|
+
export declare function confirmHandOver(ctx: Pick<BillingContext, "db" | "clock">, requestId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Gives back a claim whose hand-over failed, so the next ask tries again. Only the claim made at
|
|
56
|
+
* `claimedAt` is cleared, never a later one. Database errors propagate.
|
|
57
|
+
*/
|
|
58
|
+
export declare function releaseHandOver(ctx: Pick<BillingContext, "db">, requestId: string, claimedAt: Date): Promise<void>;
|
|
59
|
+
/** The open requests, oldest first, with each account's email. */
|
|
60
|
+
export declare function listOpenRequests(ctx: Pick<BillingContext, "db">, limit?: number): Promise<readonly OpenPaymentRequest[]>;
|
|
61
|
+
/** The open request with this id (its account and plan), or undefined. */
|
|
62
|
+
export declare function findOpenRequest(db: Queryable, requestId: string): Promise<{
|
|
63
|
+
readonly userId: string;
|
|
64
|
+
readonly planId: string;
|
|
65
|
+
} | undefined>;
|
|
66
|
+
/** Closes an open request without a grant and clears its details; `billing.request_closed` when it is not open. */
|
|
67
|
+
export declare function dismissPaymentRequest(ctx: Pick<BillingContext, "db" | "clock">, requestId: string): Promise<Ok<undefined> | Err<"billing.request_closed">>;
|
|
68
|
+
export interface ExpiredRequestsSummary {
|
|
69
|
+
/** Open requests closed as `expired` by this run. */
|
|
70
|
+
readonly expired: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Closes the open requests nobody asked again for in `requests.expireAfterDays` days as `expired`
|
|
74
|
+
* and clears their invoice details: personal data kept no longer than the request waits. Run it
|
|
75
|
+
* daily (a scheduler); a repeated run closes nothing new. Database errors propagate.
|
|
76
|
+
*/
|
|
77
|
+
export declare function expireStaleRequests(ctx: BillingContext): Promise<ExpiredRequestsSummary>;
|
|
78
|
+
/** A stored price, or null when the row has none (a CHECK stores both or neither). */
|
|
79
|
+
export declare function readPrice(amount: number | null, currency: string | null): PlanPrice | null;
|
|
80
|
+
//# sourceMappingURL=requests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requests.d.ts","sourceRoot":"","sources":["../../src/server/requests.ts"],"names":[],"mappings":"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"}
|