@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,155 @@
|
|
|
1
|
+
// Invoice requests in billing: stored before a provider hands a request over (the manual adapter),
|
|
2
|
+
// listed in the admin page until the admin grants or dismisses them or they expire. One open
|
|
3
|
+
// request per account and plan: asking again refreshes its details and price. The hand-over is
|
|
4
|
+
// claimed on the row (`handover_claimed_at`) and recorded once it answered (`handed_over_at`), so
|
|
5
|
+
// an open request reaches the owner once; a failed hand-over is released for the next ask, and a
|
|
6
|
+
// claim left without an answer (the process stopped) is taken over after a minute. Closing a request clears its invoice details, which are
|
|
7
|
+
// personal data the app no longer needs (the migration's CHECK holds closed rows empty).
|
|
8
|
+
import { users } from "@softure-ai/auth";
|
|
9
|
+
import { err, ok } from "@softure-ai/core";
|
|
10
|
+
import { and, asc, eq, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
11
|
+
import { paymentRequests } from "../schema.js";
|
|
12
|
+
import { getBillingOptions } from "./options.js";
|
|
13
|
+
import { isUuid } from "./user-id.js";
|
|
14
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
/** How long a hand-over claim blocks other asks: a provider's mail call takes seconds. */
|
|
16
|
+
const HANDOVER_CLAIM_TIMEOUT_MS = 60 * 1000;
|
|
17
|
+
/** How many open requests the admin page lists by default. */
|
|
18
|
+
export const OPEN_REQUESTS_LIMIT = 50;
|
|
19
|
+
/** The columns that close a request: its status, when, and no invoice details any more. */
|
|
20
|
+
export function getClosedRequestColumns(status, now) {
|
|
21
|
+
return { status, closedAt: now, invoiceName: null, invoiceTaxId: null, invoiceAddress: null };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stores a request before it is handed over to the owner, or refreshes the account's open request
|
|
25
|
+
* for the same plan (its details, price and time; its hand-over and claim stay). Returns the
|
|
26
|
+
* request's id. Database errors propagate.
|
|
27
|
+
*/
|
|
28
|
+
export async function recordPaymentRequest(ctx, input) {
|
|
29
|
+
const now = ctx.clock.now();
|
|
30
|
+
const details = {
|
|
31
|
+
invoiceName: input.invoice?.name ?? null,
|
|
32
|
+
invoiceTaxId: input.invoice?.taxId ?? null,
|
|
33
|
+
invoiceAddress: input.invoice?.address ?? null,
|
|
34
|
+
amount: input.price.amount,
|
|
35
|
+
currency: input.price.currency,
|
|
36
|
+
};
|
|
37
|
+
const [row] = await ctx.db
|
|
38
|
+
.insert(paymentRequests)
|
|
39
|
+
.values({ userId: input.userId, planId: input.planId, ...details, status: "open", requestedAt: now })
|
|
40
|
+
.onConflictDoUpdate({
|
|
41
|
+
target: [paymentRequests.userId, paymentRequests.planId],
|
|
42
|
+
targetWhere: sql `status = 'open'`,
|
|
43
|
+
set: { ...details, requestedAt: now },
|
|
44
|
+
})
|
|
45
|
+
.returning();
|
|
46
|
+
// An insert or an update of the conflicting row always returns it.
|
|
47
|
+
if (row === undefined)
|
|
48
|
+
throw new Error("@softure-ai/billing: storing a payment request returned no row");
|
|
49
|
+
return row.id;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Claims the hand-over of an open request that was not handed over yet: the time it was claimed,
|
|
53
|
+
* or null when it was handed over already, is being handed over (a claim younger than
|
|
54
|
+
* `HANDOVER_CLAIM_TIMEOUT_MS`), or is no longer open. An older claim was left without an answer and
|
|
55
|
+
* is taken over. A conditional update, so two asks at once hand it over once. Database errors
|
|
56
|
+
* propagate.
|
|
57
|
+
*/
|
|
58
|
+
export async function claimHandOver(ctx, requestId) {
|
|
59
|
+
const now = ctx.clock.now();
|
|
60
|
+
const staleBefore = new Date(now.getTime() - HANDOVER_CLAIM_TIMEOUT_MS);
|
|
61
|
+
const [claimed] = await ctx.db
|
|
62
|
+
.update(paymentRequests)
|
|
63
|
+
.set({ handoverClaimedAt: now })
|
|
64
|
+
.where(and(eq(paymentRequests.id, requestId), eq(paymentRequests.status, "open"), isNull(paymentRequests.handedOverAt), or(isNull(paymentRequests.handoverClaimedAt), lte(paymentRequests.handoverClaimedAt, staleBefore))))
|
|
65
|
+
.returning();
|
|
66
|
+
return claimed === undefined ? null : now;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Records a hand-over that answered: the request is never handed over again, whichever ask holds
|
|
70
|
+
* the claim now. The first record wins. Database errors propagate.
|
|
71
|
+
*/
|
|
72
|
+
export async function confirmHandOver(ctx, requestId) {
|
|
73
|
+
await ctx.db
|
|
74
|
+
.update(paymentRequests)
|
|
75
|
+
.set({ handedOverAt: ctx.clock.now(), handoverClaimedAt: null })
|
|
76
|
+
.where(and(eq(paymentRequests.id, requestId), isNull(paymentRequests.handedOverAt)));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Gives back a claim whose hand-over failed, so the next ask tries again. Only the claim made at
|
|
80
|
+
* `claimedAt` is cleared, never a later one. Database errors propagate.
|
|
81
|
+
*/
|
|
82
|
+
export async function releaseHandOver(ctx, requestId, claimedAt) {
|
|
83
|
+
await ctx.db
|
|
84
|
+
.update(paymentRequests)
|
|
85
|
+
.set({ handoverClaimedAt: null })
|
|
86
|
+
.where(and(eq(paymentRequests.id, requestId), eq(paymentRequests.handoverClaimedAt, claimedAt)));
|
|
87
|
+
}
|
|
88
|
+
/** The open requests, oldest first, with each account's email. */
|
|
89
|
+
export async function listOpenRequests(ctx, limit = OPEN_REQUESTS_LIMIT) {
|
|
90
|
+
const rows = await ctx.db
|
|
91
|
+
.select({
|
|
92
|
+
id: paymentRequests.id,
|
|
93
|
+
userId: paymentRequests.userId,
|
|
94
|
+
email: users.email,
|
|
95
|
+
planId: paymentRequests.planId,
|
|
96
|
+
invoiceName: paymentRequests.invoiceName,
|
|
97
|
+
invoiceTaxId: paymentRequests.invoiceTaxId,
|
|
98
|
+
invoiceAddress: paymentRequests.invoiceAddress,
|
|
99
|
+
amount: paymentRequests.amount,
|
|
100
|
+
currency: paymentRequests.currency,
|
|
101
|
+
requestedAt: paymentRequests.requestedAt,
|
|
102
|
+
})
|
|
103
|
+
.from(paymentRequests)
|
|
104
|
+
.innerJoin(users, eq(users.id, paymentRequests.userId))
|
|
105
|
+
.where(eq(paymentRequests.status, "open"))
|
|
106
|
+
.orderBy(asc(paymentRequests.requestedAt), asc(paymentRequests.id))
|
|
107
|
+
.limit(limit);
|
|
108
|
+
return rows.map(({ invoiceName, invoiceTaxId, invoiceAddress, amount, currency, ...row }) => ({
|
|
109
|
+
...row,
|
|
110
|
+
invoice: invoiceName === null || invoiceAddress === null ? null : { name: invoiceName, taxId: invoiceTaxId, address: invoiceAddress },
|
|
111
|
+
price: readPrice(amount, currency),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/** The open request with this id (its account and plan), or undefined. */
|
|
115
|
+
export async function findOpenRequest(db, requestId) {
|
|
116
|
+
if (!isUuid(requestId))
|
|
117
|
+
return undefined;
|
|
118
|
+
const [row] = await db
|
|
119
|
+
.select({ userId: paymentRequests.userId, planId: paymentRequests.planId })
|
|
120
|
+
.from(paymentRequests)
|
|
121
|
+
.where(and(eq(paymentRequests.id, requestId), eq(paymentRequests.status, "open")));
|
|
122
|
+
return row;
|
|
123
|
+
}
|
|
124
|
+
/** Closes an open request without a grant and clears its details; `billing.request_closed` when it is not open. */
|
|
125
|
+
export async function dismissPaymentRequest(ctx, requestId) {
|
|
126
|
+
if (!isUuid(requestId))
|
|
127
|
+
return err("billing.request_closed");
|
|
128
|
+
const [closed] = await ctx.db
|
|
129
|
+
.update(paymentRequests)
|
|
130
|
+
.set(getClosedRequestColumns("dismissed", ctx.clock.now()))
|
|
131
|
+
.where(and(eq(paymentRequests.id, requestId), eq(paymentRequests.status, "open")))
|
|
132
|
+
.returning();
|
|
133
|
+
return closed === undefined ? err("billing.request_closed") : ok();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Closes the open requests nobody asked again for in `requests.expireAfterDays` days as `expired`
|
|
137
|
+
* and clears their invoice details: personal data kept no longer than the request waits. Run it
|
|
138
|
+
* daily (a scheduler); a repeated run closes nothing new. Database errors propagate.
|
|
139
|
+
*/
|
|
140
|
+
export async function expireStaleRequests(ctx) {
|
|
141
|
+
const now = ctx.clock.now();
|
|
142
|
+
const { expireAfterDays } = getBillingOptions(ctx.config).requests;
|
|
143
|
+
const cutoff = new Date(now.getTime() - expireAfterDays * DAY_MS);
|
|
144
|
+
const expired = await ctx.db
|
|
145
|
+
.update(paymentRequests)
|
|
146
|
+
.set(getClosedRequestColumns("expired", now))
|
|
147
|
+
.where(and(eq(paymentRequests.status, "open"), lt(paymentRequests.requestedAt, cutoff)))
|
|
148
|
+
.returning();
|
|
149
|
+
return { expired: expired.length };
|
|
150
|
+
}
|
|
151
|
+
/** A stored price, or null when the row has none (a CHECK stores both or neither). */
|
|
152
|
+
export function readPrice(amount, currency) {
|
|
153
|
+
return amount === null || currency === null ? null : { amount, currency };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=requests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requests.js","sourceRoot":"","sources":["../../src/server/requests.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
export declare const PAYMENT_BUCKET = "billing-payment";
|
|
3
|
+
export declare function assertPaymentSetup(config: SoftureConfig): void;
|
|
4
|
+
/**
|
|
5
|
+
* Throws when `billing({ adminRole })` is not a role of `auth({ roles })`: a misspelt role would
|
|
6
|
+
* otherwise surface only when an admin opens the admin page.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertAdminRoleDeclared(config: SoftureConfig): void;
|
|
9
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAKA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjE,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAKhD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAO9D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAUnE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// What billing needs from the app's other modules, checked once per config so a setup mistake fails
|
|
2
|
+
// with one clear error instead of on each request: the rate limit bucket in `security({ buckets })`
|
|
3
|
+
// (before the first payment), and an `adminRole` that `auth({ roles })` declares (at the first
|
|
4
|
+
// billing request and in the readiness probe).
|
|
5
|
+
import { getDeclaredRoles, isDeclaredRole } from "@softure-ai/auth/server";
|
|
6
|
+
import { getModule } from "@softure-ai/core";
|
|
7
|
+
import { getBillingOptions } from "./options.js";
|
|
8
|
+
export const PAYMENT_BUCKET = "billing-payment";
|
|
9
|
+
const checkedPaymentConfigs = new WeakSet();
|
|
10
|
+
const checkedRoleConfigs = new WeakSet();
|
|
11
|
+
export function assertPaymentSetup(config) {
|
|
12
|
+
if (checkedPaymentConfigs.has(config))
|
|
13
|
+
return;
|
|
14
|
+
const security = getModule(config, "security")?.options;
|
|
15
|
+
if (!Object.hasOwn(security?.buckets ?? {}, PAYMENT_BUCKET)) {
|
|
16
|
+
throw new Error(`@softure-ai/billing: security({ buckets }) lacks "${PAYMENT_BUCKET}"; spread BILLING_RATE_LIMIT_BUCKETS into it`);
|
|
17
|
+
}
|
|
18
|
+
checkedPaymentConfigs.add(config);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Throws when `billing({ adminRole })` is not a role of `auth({ roles })`: a misspelt role would
|
|
22
|
+
* otherwise surface only when an admin opens the admin page.
|
|
23
|
+
*/
|
|
24
|
+
export function assertAdminRoleDeclared(config) {
|
|
25
|
+
if (checkedRoleConfigs.has(config))
|
|
26
|
+
return;
|
|
27
|
+
const { adminRole } = getBillingOptions(config);
|
|
28
|
+
if (!isDeclaredRole(config, adminRole)) {
|
|
29
|
+
const declared = [...getDeclaredRoles(config)].join(", ");
|
|
30
|
+
throw new Error(`@softure-ai/billing: billing({ adminRole }) is "${adminRole}", which auth({ roles }) does not declare (declared: ${declared}); declare it there or fix the name`);
|
|
31
|
+
}
|
|
32
|
+
checkedRoleConfigs.add(config);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,oGAAoG;AACpG,+FAA+F;AAC/F,+CAA+C;AAC/C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAsB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,MAAM,qBAAqB,GAAG,IAAI,OAAO,EAAiB,CAAC;AAC3D,MAAM,kBAAkB,GAAG,IAAI,OAAO,EAAiB,CAAC;AAExD,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,IAAI,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO;IAC9C,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,OAAsE,CAAC;IACvH,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,cAAc,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,qDAAqD,cAAc,8CAA8C,CAAC,CAAC;IACrI,CAAC;IACD,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAqB;IAC3D,IAAI,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO;IAC3C,MAAM,EAAE,SAAS,EAAE,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACvC,MAAM,QAAQ,GAAG,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,mDAAmD,SAAS,wDAAwD,QAAQ,qCAAqC,CAClK,CAAC;IACJ,CAAC;IACD,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Queryable } from "@softure-ai/db";
|
|
2
|
+
import type { Entitlement, PaymentGrant } from "../contract.js";
|
|
3
|
+
import { type RefundShare } from "../refund.js";
|
|
4
|
+
import { type BillingContext } from "./entitlements.js";
|
|
5
|
+
/**
|
|
6
|
+
* Locks the account's entitlement row until the transaction ends (nothing when it has none yet:
|
|
7
|
+
* a check that must hold before the first change pins the row first, `pinEntitlementRow`).
|
|
8
|
+
* Taken before a refund's or a revoke's own row, so every take-back of the account queues here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function lockEntitlementRow(tx: Queryable, userId: string): Promise<void>;
|
|
11
|
+
/** Whether a paid payment of the account (other than `exceptId`) bought lifetime access. */
|
|
12
|
+
export declare function hasPaidLifetimePayment(tx: Queryable, userId: string, exceptId?: string): Promise<boolean>;
|
|
13
|
+
/** Whether an active manual grant of the account (other than `exceptId`) gave lifetime access. */
|
|
14
|
+
export declare function hasActiveManualLifetime(tx: Queryable, userId: string, exceptId?: string): Promise<boolean>;
|
|
15
|
+
export interface ShiftLaterPeriodsInput {
|
|
16
|
+
readonly userId: string;
|
|
17
|
+
/** Stored periods starting at or after this instant move (the end of the period taken back or given back). */
|
|
18
|
+
readonly after: Date;
|
|
19
|
+
readonly days: number;
|
|
20
|
+
/** `back` when days are taken back, `forward` when a failed refund gives them back. */
|
|
21
|
+
readonly direction: "back" | "forward";
|
|
22
|
+
readonly timezone: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Moves the stored periods that follow the one taken back or given back (paid provider payments and
|
|
26
|
+
* active manual grants starting at or after `after`) by the days, so a later refund or revoke of one
|
|
27
|
+
* of them takes back the right days. The caller holds the entitlement lock.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shiftLaterPeriods(tx: Queryable, input: ShiftLaterPeriodsInput): Promise<void>;
|
|
30
|
+
export interface TakeBackGrantInput {
|
|
31
|
+
readonly userId: string;
|
|
32
|
+
/** What the refunded payment or revoked grant added; null for a payment stored before grants were (revokes paid access). */
|
|
33
|
+
readonly grant: PaymentGrant | null;
|
|
34
|
+
readonly now: Date;
|
|
35
|
+
/** Whether something else still pays for lifetime access; asked under the entitlement lock. */
|
|
36
|
+
readonly hasOtherLifetime: () => Promise<boolean>;
|
|
37
|
+
/** The part of a payment a partial refund returns; omitted to take back the whole grant. */
|
|
38
|
+
readonly share?: RefundShare;
|
|
39
|
+
}
|
|
40
|
+
/** Where the account stands after a take-back, and the local days its period lost (0 when nothing moved). */
|
|
41
|
+
export interface TakeBack {
|
|
42
|
+
readonly entitlement: Entitlement;
|
|
43
|
+
readonly days: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Takes back what one grant added (its share, for a partial refund), inside the caller's transaction
|
|
47
|
+
* (`ctx.db` is it), and returns where the account stands after and how many days its period lost.
|
|
48
|
+
* The caller locked the account and the entitlement (`lockEntitlementRow`) and flipped its row
|
|
49
|
+
* first: a lifetime granted at the same time is either committed and seen below, or waits for the
|
|
50
|
+
* entitlement lock and is applied after.
|
|
51
|
+
*/
|
|
52
|
+
export declare function takeBackGrant(ctx: BillingContext, input: TakeBackGrantInput): Promise<TakeBack>;
|
|
53
|
+
export interface GiveBackDaysInput {
|
|
54
|
+
readonly userId: string;
|
|
55
|
+
/** The period the refunded payment stores now. */
|
|
56
|
+
readonly grant: Extract<PaymentGrant, {
|
|
57
|
+
kind: "period";
|
|
58
|
+
}>;
|
|
59
|
+
/** Whether the payment was still paid (refunded in part) before the failure. */
|
|
60
|
+
readonly isPaid: boolean;
|
|
61
|
+
readonly days: number;
|
|
62
|
+
readonly now: Date;
|
|
63
|
+
}
|
|
64
|
+
/** Where the account stands after days were given back, and the period the payment pays for now. */
|
|
65
|
+
export interface GiveBack {
|
|
66
|
+
readonly entitlement: Entitlement;
|
|
67
|
+
readonly period: Extract<PaymentGrant, {
|
|
68
|
+
kind: "period";
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Gives back `days` local days a failed refund had taken, inside the caller's transaction, under the
|
|
73
|
+
* same locks as `takeBackGrant`. While the payment is paid and its period still ahead, the days go
|
|
74
|
+
* back right after that period: dated access and every later stored period move forward by them
|
|
75
|
+
* (the take-back in reverse). Otherwise (refunded in full, or the period used up) they are a grant
|
|
76
|
+
* at the end, from the latest of the trial's end, dated access and now, and that is the period the
|
|
77
|
+
* payment pays for.
|
|
78
|
+
*/
|
|
79
|
+
export declare function giveBackDays(ctx: BillingContext, input: GiveBackDaysInput): Promise<GiveBack>;
|
|
80
|
+
//# sourceMappingURL=take-back.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"take-back.d.ts","sourceRoot":"","sources":["../../src/server/take-back.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAuC,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAErG,OAAO,EAAmG,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEjJ,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGlG;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErF;AAED,4FAA4F;AAC5F,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK/G;AAED,kGAAkG;AAClG,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKhH;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8GAA8G;IAC9G,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAeD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBnG;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4HAA4H;IAC5H,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,+FAA+F;IAC/F,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED,6GAA6G;AAC7G,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAYD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkBrG;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;IAC1D,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;CACpB;AAED,oGAAoG;AACpG,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAC5D;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBnG"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { and, eq, gte, ne } from "drizzle-orm";
|
|
2
|
+
import { resolveEntitlement } from "../entitlement.js";
|
|
3
|
+
import { getGrantStart, getRefundEvent, getTakenBackDays, isFullShare, moveBackByDays, moveForwardByDays } from "../refund.js";
|
|
4
|
+
import { entitlements, manualGrants, payments } from "../schema.js";
|
|
5
|
+
import { changeEntitlement, findEntitlementRecord } from "./entitlements.js";
|
|
6
|
+
import { getEntitlementPolicy } from "./options.js";
|
|
7
|
+
/**
|
|
8
|
+
* Locks the account's entitlement row until the transaction ends (nothing when it has none yet:
|
|
9
|
+
* a check that must hold before the first change pins the row first, `pinEntitlementRow`).
|
|
10
|
+
* Taken before a refund's or a revoke's own row, so every take-back of the account queues here.
|
|
11
|
+
*/
|
|
12
|
+
export async function lockEntitlementRow(tx, userId) {
|
|
13
|
+
await tx.select({ userId: entitlements.userId }).from(entitlements).where(eq(entitlements.userId, userId)).for("update");
|
|
14
|
+
}
|
|
15
|
+
/** Whether a paid payment of the account (other than `exceptId`) bought lifetime access. */
|
|
16
|
+
export async function hasPaidLifetimePayment(tx, userId, exceptId) {
|
|
17
|
+
const conditions = [eq(payments.userId, userId), eq(payments.grantKind, "lifetime"), eq(payments.status, "paid")];
|
|
18
|
+
if (exceptId !== undefined)
|
|
19
|
+
conditions.push(ne(payments.id, exceptId));
|
|
20
|
+
const [other] = await tx.select({ id: payments.id }).from(payments).where(and(...conditions)).limit(1);
|
|
21
|
+
return other !== undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Whether an active manual grant of the account (other than `exceptId`) gave lifetime access. */
|
|
24
|
+
export async function hasActiveManualLifetime(tx, userId, exceptId) {
|
|
25
|
+
const conditions = [eq(manualGrants.userId, userId), eq(manualGrants.grantKind, "lifetime"), eq(manualGrants.status, "active")];
|
|
26
|
+
if (exceptId !== undefined)
|
|
27
|
+
conditions.push(ne(manualGrants.id, exceptId));
|
|
28
|
+
const [other] = await tx.select({ id: manualGrants.id }).from(manualGrants).where(and(...conditions)).limit(1);
|
|
29
|
+
return other !== undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The stored period moved by `days` local days in the input's direction. */
|
|
32
|
+
function getShiftedPeriod(row, input) {
|
|
33
|
+
if (row.grantedFrom === null || row.grantedUntil === null)
|
|
34
|
+
return null;
|
|
35
|
+
const days = input.direction === "back" ? input.days : -input.days;
|
|
36
|
+
return { grantedFrom: moveBackByDays(row.grantedFrom, days, input.timezone), grantedUntil: moveBackByDays(row.grantedUntil, days, input.timezone) };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Moves the stored periods that follow the one taken back or given back (paid provider payments and
|
|
40
|
+
* active manual grants starting at or after `after`) by the days, so a later refund or revoke of one
|
|
41
|
+
* of them takes back the right days. The caller holds the entitlement lock.
|
|
42
|
+
*/
|
|
43
|
+
export async function shiftLaterPeriods(tx, input) {
|
|
44
|
+
const laterPayments = await tx
|
|
45
|
+
.select({ id: payments.id, grantedFrom: payments.grantedFrom, grantedUntil: payments.grantedUntil })
|
|
46
|
+
.from(payments)
|
|
47
|
+
.where(and(eq(payments.userId, input.userId), eq(payments.grantKind, "period"), eq(payments.status, "paid"), gte(payments.grantedFrom, input.after)));
|
|
48
|
+
for (const row of laterPayments) {
|
|
49
|
+
const shifted = getShiftedPeriod(row, input);
|
|
50
|
+
if (shifted !== null)
|
|
51
|
+
await tx.update(payments).set(shifted).where(eq(payments.id, row.id));
|
|
52
|
+
}
|
|
53
|
+
const laterGrants = await tx
|
|
54
|
+
.select({ id: manualGrants.id, grantedFrom: manualGrants.grantedFrom, grantedUntil: manualGrants.grantedUntil })
|
|
55
|
+
.from(manualGrants)
|
|
56
|
+
.where(and(eq(manualGrants.userId, input.userId), eq(manualGrants.grantKind, "period"), eq(manualGrants.status, "active"), gte(manualGrants.grantedFrom, input.after)));
|
|
57
|
+
for (const row of laterGrants) {
|
|
58
|
+
const shifted = getShiftedPeriod(row, input);
|
|
59
|
+
if (shifted !== null)
|
|
60
|
+
await tx.update(manualGrants).set(shifted).where(eq(manualGrants.id, row.id));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The change taking back `grant` makes, decided under the entitlement lock, or null when it takes nothing back. */
|
|
64
|
+
async function getTakeBackEvent(ctx, input, record) {
|
|
65
|
+
const { grant, share } = input;
|
|
66
|
+
// A partial refund of a payment without a recorded grant, or of a lifetime, takes nothing back.
|
|
67
|
+
if (!isFullShare(share) && grant?.kind !== "period")
|
|
68
|
+
return null;
|
|
69
|
+
if (grant === null)
|
|
70
|
+
return { type: "revoke" };
|
|
71
|
+
if (grant.kind === "lifetime" && (await input.hasOtherLifetime()))
|
|
72
|
+
return null;
|
|
73
|
+
return getRefundEvent(record, grant, { now: input.now, timezone: ctx.config.timezone, share });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Takes back what one grant added (its share, for a partial refund), inside the caller's transaction
|
|
77
|
+
* (`ctx.db` is it), and returns where the account stands after and how many days its period lost.
|
|
78
|
+
* The caller locked the account and the entitlement (`lockEntitlementRow`) and flipped its row
|
|
79
|
+
* first: a lifetime granted at the same time is either committed and seen below, or waits for the
|
|
80
|
+
* entitlement lock and is applied after.
|
|
81
|
+
*/
|
|
82
|
+
export async function takeBackGrant(ctx, input) {
|
|
83
|
+
const tx = ctx.db;
|
|
84
|
+
const record = await findEntitlementRecord(ctx, input.userId);
|
|
85
|
+
// The caller's row references the account, which its key share lock keeps.
|
|
86
|
+
if (record === null)
|
|
87
|
+
throw new Error("@softure-ai/billing: a grant to take back has no account");
|
|
88
|
+
const event = await getTakeBackEvent(ctx, input, record);
|
|
89
|
+
if (event === null)
|
|
90
|
+
return { entitlement: resolveEntitlement(record, input.now, getEntitlementPolicy(ctx.config)), days: 0 };
|
|
91
|
+
const { grant } = input;
|
|
92
|
+
const { timezone } = ctx.config;
|
|
93
|
+
let days = 0;
|
|
94
|
+
if (grant?.kind === "period") {
|
|
95
|
+
days = getTakenBackDays(grant, { now: input.now, timezone, share: input.share });
|
|
96
|
+
await shiftLaterPeriods(tx, { userId: input.userId, after: grant.until, days, direction: "back", timezone });
|
|
97
|
+
}
|
|
98
|
+
const changed = await changeEntitlement(ctx, input.userId, event);
|
|
99
|
+
// The account is locked and these events are never refused.
|
|
100
|
+
if (!changed.ok)
|
|
101
|
+
throw new Error(`@softure-ai/billing: taking back a grant failed with ${changed.error}`);
|
|
102
|
+
return { entitlement: changed.value, days };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gives back `days` local days a failed refund had taken, inside the caller's transaction, under the
|
|
106
|
+
* same locks as `takeBackGrant`. While the payment is paid and its period still ahead, the days go
|
|
107
|
+
* back right after that period: dated access and every later stored period move forward by them
|
|
108
|
+
* (the take-back in reverse). Otherwise (refunded in full, or the period used up) they are a grant
|
|
109
|
+
* at the end, from the latest of the trial's end, dated access and now, and that is the period the
|
|
110
|
+
* payment pays for.
|
|
111
|
+
*/
|
|
112
|
+
export async function giveBackDays(ctx, input) {
|
|
113
|
+
const record = await findEntitlementRecord(ctx, input.userId);
|
|
114
|
+
// The caller's payment row references the account, which its key share lock keeps.
|
|
115
|
+
if (record === null)
|
|
116
|
+
throw new Error("@softure-ai/billing: days to give back have no account");
|
|
117
|
+
const { timezone } = ctx.config;
|
|
118
|
+
const { grant, days, now } = input;
|
|
119
|
+
const { paidUntil } = record;
|
|
120
|
+
let period;
|
|
121
|
+
let until;
|
|
122
|
+
if (input.isPaid && grant.until > now && paidUntil !== null && paidUntil >= grant.until) {
|
|
123
|
+
await shiftLaterPeriods(ctx.db, { userId: input.userId, after: grant.until, days, direction: "forward", timezone });
|
|
124
|
+
period = { kind: "period", from: grant.from, until: moveForwardByDays(grant.until, days, timezone) };
|
|
125
|
+
until = moveForwardByDays(paidUntil, days, timezone);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const from = getGrantStart(record, now);
|
|
129
|
+
until = moveForwardByDays(from, days, timezone);
|
|
130
|
+
period = { kind: "period", from, until };
|
|
131
|
+
}
|
|
132
|
+
const changed = await changeEntitlement(ctx, input.userId, { type: "grant", until });
|
|
133
|
+
// The account is locked and the end lies at least a day past dated access and now.
|
|
134
|
+
if (!changed.ok)
|
|
135
|
+
throw new Error(`@softure-ai/billing: giving back refunded days failed with ${changed.error}`);
|
|
136
|
+
return { entitlement: changed.value, period };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=take-back.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"take-back.js","sourceRoot":"","sources":["../../src/server/take-back.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-id.d.ts","sourceRoot":"","sources":["../../src/server/user-id.ts"],"names":[],"mappings":"AAIA,gDAAgD;AAChD,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Account ids are auth's uuids, and the module's own rows use uuids too; anything else names no row
|
|
2
|
+
// and is answered without a query (Postgres would refuse to compare it with a uuid column).
|
|
3
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
/** Whether `value` can be a row id (a uuid). */
|
|
5
|
+
export function isUuid(value) {
|
|
6
|
+
return UUID.test(value);
|
|
7
|
+
}
|
|
8
|
+
export function isUserId(value) {
|
|
9
|
+
return isUuid(value);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=user-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-id.js","sourceRoot":"","sources":["../../src/server/user-id.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,4FAA4F;AAC5F,MAAM,IAAI,GAAG,iEAAiE,CAAC;AAE/E,gDAAgD;AAChD,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PlanPrice } from "./contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Charged without a minor unit. UGX is not here: Stripe still takes it as a two-decimal value whose
|
|
4
|
+
* decimals are always 00 (as ISK). The guide's text export omits the list; this is Stripe's
|
|
5
|
+
* long-standing list, which the guide's index matches.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STRIPE_ZERO_DECIMAL_CURRENCIES: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Charged with three decimals, and the last one must be 0 (Stripe's earlier guide; the current
|
|
10
|
+
* text export omits the section).
|
|
11
|
+
*/
|
|
12
|
+
export declare const STRIPE_THREE_DECIMAL_CURRENCIES: ReadonlySet<string>;
|
|
13
|
+
/** Digits of the unit Stripe takes for `currency` (ISO 4217, upper case): 0, 2 or 3. */
|
|
14
|
+
export declare function getStripeMinorUnitDigits(currency: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* The price as Stripe's `unit_amount`, e.g. ISK 1,500 (`amount: 1500`) as 150000; null when Stripe
|
|
17
|
+
* cannot charge it exactly (a fraction of Stripe's unit, or a three-decimal amount not ending in 0).
|
|
18
|
+
*/
|
|
19
|
+
export declare function toStripeAmount(price: PlanPrice): number | null;
|
|
20
|
+
/**
|
|
21
|
+
* An amount Stripe reports (a checkout's total, a charge's refunded total) in billing's unit,
|
|
22
|
+
* rounded down when it is not whole, so a refunded total is never over-counted.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fromStripeAmount(amount: number, currency: string): number;
|
|
25
|
+
/** Why Stripe cannot charge `price` exactly, for the config error; null when it can. */
|
|
26
|
+
export declare function describeStripePriceProblem(price: PlanPrice): string | null;
|
|
27
|
+
//# sourceMappingURL=stripe-currency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-currency.d.ts","sourceRoot":"","sources":["../src/stripe-currency.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG/C;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,WAAW,CAAC,MAAM,CAE7D,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,WAAW,CAAC,MAAM,CAAgD,CAAC;AAEjH,wFAAwF;AACxF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIjE;AAOD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAM9D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAM1E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getMinorUnitDigits } from "./price.js";
|
|
2
|
+
/**
|
|
3
|
+
* Charged without a minor unit. UGX is not here: Stripe still takes it as a two-decimal value whose
|
|
4
|
+
* decimals are always 00 (as ISK). The guide's text export omits the list; this is Stripe's
|
|
5
|
+
* long-standing list, which the guide's index matches.
|
|
6
|
+
*/
|
|
7
|
+
export const STRIPE_ZERO_DECIMAL_CURRENCIES = new Set([
|
|
8
|
+
"BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "VND", "VUV", "XAF", "XOF", "XPF",
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Charged with three decimals, and the last one must be 0 (Stripe's earlier guide; the current
|
|
12
|
+
* text export omits the section).
|
|
13
|
+
*/
|
|
14
|
+
export const STRIPE_THREE_DECIMAL_CURRENCIES = new Set(["BHD", "JOD", "KWD", "OMR", "TND"]);
|
|
15
|
+
/** Digits of the unit Stripe takes for `currency` (ISO 4217, upper case): 0, 2 or 3. */
|
|
16
|
+
export function getStripeMinorUnitDigits(currency) {
|
|
17
|
+
if (STRIPE_ZERO_DECIMAL_CURRENCIES.has(currency))
|
|
18
|
+
return 0;
|
|
19
|
+
if (STRIPE_THREE_DECIMAL_CURRENCIES.has(currency))
|
|
20
|
+
return 3;
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
/** How many digits Stripe's unit has beyond billing's (negative when it has fewer). */
|
|
24
|
+
function getDigitShift(currency) {
|
|
25
|
+
return getStripeMinorUnitDigits(currency) - getMinorUnitDigits(currency);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The price as Stripe's `unit_amount`, e.g. ISK 1,500 (`amount: 1500`) as 150000; null when Stripe
|
|
29
|
+
* cannot charge it exactly (a fraction of Stripe's unit, or a three-decimal amount not ending in 0).
|
|
30
|
+
*/
|
|
31
|
+
export function toStripeAmount(price) {
|
|
32
|
+
const shift = getDigitShift(price.currency);
|
|
33
|
+
const amount = shift >= 0 ? price.amount * 10 ** shift : price.amount / 10 ** -shift;
|
|
34
|
+
if (!Number.isInteger(amount))
|
|
35
|
+
return null;
|
|
36
|
+
if (STRIPE_THREE_DECIMAL_CURRENCIES.has(price.currency) && amount % 10 !== 0)
|
|
37
|
+
return null;
|
|
38
|
+
return amount;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An amount Stripe reports (a checkout's total, a charge's refunded total) in billing's unit,
|
|
42
|
+
* rounded down when it is not whole, so a refunded total is never over-counted.
|
|
43
|
+
*/
|
|
44
|
+
export function fromStripeAmount(amount, currency) {
|
|
45
|
+
const shift = getDigitShift(currency);
|
|
46
|
+
return shift >= 0 ? Math.floor(amount / 10 ** shift) : amount * 10 ** -shift;
|
|
47
|
+
}
|
|
48
|
+
/** Why Stripe cannot charge `price` exactly, for the config error; null when it can. */
|
|
49
|
+
export function describeStripePriceProblem(price) {
|
|
50
|
+
if (toStripeAmount(price) !== null)
|
|
51
|
+
return null;
|
|
52
|
+
if (STRIPE_THREE_DECIMAL_CURRENCIES.has(price.currency)) {
|
|
53
|
+
return `stripe() charges ${price.currency} in multiples of 10 of its minor unit; round the amount to end in 0`;
|
|
54
|
+
}
|
|
55
|
+
return `stripe() charges ${price.currency} with ${String(getStripeMinorUnitDigits(price.currency))} decimals; the amount must be a whole number of them`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=stripe-currency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-currency.js","sourceRoot":"","sources":["../src/stripe-currency.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAwB,IAAI,GAAG,CAAC;IACzE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;CACxG,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjH,wFAAwF;AACxF,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,IAAI,8BAA8B,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,CAAC,CAAC;IAC3D,IAAI,+BAA+B,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,CAAC,CAAC;IAC5D,OAAO,CAAC,CAAC;AACX,CAAC;AAED,uFAAuF;AACvF,SAAS,aAAa,CAAC,QAAgB;IACrC,OAAO,wBAAwB,CAAC,QAAQ,CAAC,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC;IACrF,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,+BAA+B,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,MAAM,GAAG,EAAE,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1F,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,QAAgB;IAC/D,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC;AAC/E,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,0BAA0B,CAAC,KAAgB;IACzD,IAAI,cAAc,CAAC,KAAK,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,+BAA+B,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxD,OAAO,oBAAoB,KAAK,CAAC,QAAQ,qEAAqE,CAAC;IACjH,CAAC;IACD,OAAO,oBAAoB,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,wBAAwB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,sDAAsD,CAAC;AAC3J,CAAC"}
|