@softure-ai/billing 0.0.0-stage → 0.1.5
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/LICENSE +21 -0
- package/README.md +699 -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 +81 -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
package/dist/plans.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Locale } from "@softure-ai/core";
|
|
2
|
+
import type { EntitlementEvent, EntitlementRecord, LocalizedText, Plan, PlanPeriod } from "./contract.js";
|
|
3
|
+
/** The text in `locale`, else the `en` text (the options require one). */
|
|
4
|
+
export declare function getLocalizedText(text: LocalizedText, locale: Locale): string;
|
|
5
|
+
/** The plan with this id, or undefined. */
|
|
6
|
+
export declare function findPlan(plans: readonly Plan[], planId: string): Plan | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The end of one period begun at `start`: the start of the local day one period after the start
|
|
9
|
+
* day. The start day counts as the first day, as for trials. Lifetime periods have no end.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPeriodEnd(start: Date, period: Exclude<PlanPeriod, {
|
|
12
|
+
unit: "lifetime";
|
|
13
|
+
}>, timezone: string): Date;
|
|
14
|
+
/**
|
|
15
|
+
* The change one payment of `plan` makes to `record` at `now`. A paid period starts when the access
|
|
16
|
+
* the account already has ends (a running trial or paid access), so paying early loses no day; a
|
|
17
|
+
* lifetime plan grants lifetime access.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getPlanGrant(record: EntitlementRecord, plan: Plan, now: Date, timezone: string): EntitlementEvent;
|
|
20
|
+
//# sourceMappingURL=plans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAM1G,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,2CAA2C;AAC3C,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAEjF;AAWD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYnH;AAOD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAIjH"}
|
package/dist/plans.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getDayNumber, getStartOfDay } from "./calendar.js";
|
|
2
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
const DAYS_PER_WEEK = 7;
|
|
4
|
+
const MONTHS_PER_YEAR = 12;
|
|
5
|
+
/** The text in `locale`, else the `en` text (the options require one). */
|
|
6
|
+
export function getLocalizedText(text, locale) {
|
|
7
|
+
return text[locale] ?? text.en ?? "";
|
|
8
|
+
}
|
|
9
|
+
/** The plan with this id, or undefined. */
|
|
10
|
+
export function findPlan(plans, planId) {
|
|
11
|
+
return plans.find((plan) => plan.id === planId);
|
|
12
|
+
}
|
|
13
|
+
/** A day number `months` calendar months later; a day the target month lacks becomes its last day. */
|
|
14
|
+
function addMonths(dayNumber, months) {
|
|
15
|
+
const date = new Date(dayNumber * DAY_MS);
|
|
16
|
+
const year = date.getUTCFullYear();
|
|
17
|
+
const month = date.getUTCMonth() + months;
|
|
18
|
+
const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
19
|
+
return Math.floor(Date.UTC(year, month, Math.min(date.getUTCDate(), lastDay)) / DAY_MS);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The end of one period begun at `start`: the start of the local day one period after the start
|
|
23
|
+
* day. The start day counts as the first day, as for trials. Lifetime periods have no end.
|
|
24
|
+
*/
|
|
25
|
+
export function getPeriodEnd(start, period, timezone) {
|
|
26
|
+
const day = getDayNumber(start, timezone);
|
|
27
|
+
switch (period.unit) {
|
|
28
|
+
case "day":
|
|
29
|
+
return getStartOfDay(day + period.count, timezone);
|
|
30
|
+
case "week":
|
|
31
|
+
return getStartOfDay(day + period.count * DAYS_PER_WEEK, timezone);
|
|
32
|
+
case "month":
|
|
33
|
+
return getStartOfDay(addMonths(day, period.count), timezone);
|
|
34
|
+
case "year":
|
|
35
|
+
return getStartOfDay(addMonths(day, period.count * MONTHS_PER_YEAR), timezone);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The latest of some instants. */
|
|
39
|
+
function getLatest(first, ...rest) {
|
|
40
|
+
return rest.reduce((latest, instant) => (instant > latest ? instant : latest), first);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The change one payment of `plan` makes to `record` at `now`. A paid period starts when the access
|
|
44
|
+
* the account already has ends (a running trial or paid access), so paying early loses no day; a
|
|
45
|
+
* lifetime plan grants lifetime access.
|
|
46
|
+
*/
|
|
47
|
+
export function getPlanGrant(record, plan, now, timezone) {
|
|
48
|
+
if (plan.period.unit === "lifetime")
|
|
49
|
+
return { type: "grant_lifetime" };
|
|
50
|
+
const start = getLatest(now, record.trialEndsAt, ...(record.paidUntil === null ? [] : [record.paidUntil]));
|
|
51
|
+
return { type: "grant", until: getPeriodEnd(start, plan.period, timezone) };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=plans.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plans.js","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"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"}
|
package/dist/price.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Locale } from "@softure-ai/core";
|
|
2
|
+
import type { PlanPrice } from "./contract.js";
|
|
3
|
+
/** Whether billing accepts `currency`: an upper-case ISO 4217 code in its pinned table. */
|
|
4
|
+
export declare function isSupportedCurrency(currency: string): boolean;
|
|
5
|
+
/** Digits of the currency's minor unit: 2 for PLN, EUR and HUF, 0 for JPY, 3 for KWD. */
|
|
6
|
+
export declare function getMinorUnitDigits(currency: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* The price in the locale's notation, e.g. "PLN 29.00" in en. A stored price in a code billing no
|
|
9
|
+
* longer accepts is shown with `Intl`'s own digits, so a history page never breaks on it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatPrice(price: PlanPrice, locale: Locale): string;
|
|
12
|
+
//# sourceMappingURL=price.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price.d.ts","sourceRoot":"","sources":["../src/price.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAuB/C,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,yFAAyF;AACzF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK3D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIpE"}
|
package/dist/price.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
2
|
+
const formatters = new Map();
|
|
3
|
+
/** The pinned digits, or undefined for a code billing does not accept. */
|
|
4
|
+
function findMinorUnitDigits(currency) {
|
|
5
|
+
return Object.hasOwn(CURRENCY_MINOR_UNIT_DIGITS, currency) ? CURRENCY_MINOR_UNIT_DIGITS[currency] : undefined;
|
|
6
|
+
}
|
|
7
|
+
function getFormatter(locale, currency, digits) {
|
|
8
|
+
const key = `${locale}:${currency}`;
|
|
9
|
+
let format = formatters.get(key);
|
|
10
|
+
if (format === undefined) {
|
|
11
|
+
format = new Intl.NumberFormat(locale, digits === undefined ? { style: "currency", currency } : { style: "currency", currency, minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
12
|
+
formatters.set(key, format);
|
|
13
|
+
}
|
|
14
|
+
return format;
|
|
15
|
+
}
|
|
16
|
+
/** Whether billing accepts `currency`: an upper-case ISO 4217 code in its pinned table. */
|
|
17
|
+
export function isSupportedCurrency(currency) {
|
|
18
|
+
return findMinorUnitDigits(currency) !== undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Digits of the currency's minor unit: 2 for PLN, EUR and HUF, 0 for JPY, 3 for KWD. */
|
|
21
|
+
export function getMinorUnitDigits(currency) {
|
|
22
|
+
const digits = findMinorUnitDigits(currency);
|
|
23
|
+
// A config with this code is refused when it loads, so reaching here is a bug.
|
|
24
|
+
if (digits === undefined)
|
|
25
|
+
throw new Error(`getMinorUnitDigits: billing does not know the currency "${currency}"`);
|
|
26
|
+
return digits;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The price in the locale's notation, e.g. "PLN 29.00" in en. A stored price in a code billing no
|
|
30
|
+
* longer accepts is shown with `Intl`'s own digits, so a history page never breaks on it.
|
|
31
|
+
*/
|
|
32
|
+
export function formatPrice(price, locale) {
|
|
33
|
+
const digits = findMinorUnitDigits(price.currency);
|
|
34
|
+
const format = getFormatter(locale, price.currency, digits);
|
|
35
|
+
return format.format(price.amount / 10 ** (digits ?? format.resolvedOptions().maximumFractionDigits ?? 2));
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=price.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price.js","sourceRoot":"","sources":["../src/price.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAElE,MAAM,UAAU,GAAG,IAAI,GAAG,EAA6B,CAAC;AAExD,0EAA0E;AAC1E,SAAS,mBAAmB,CAAC,QAAgB;IAC3C,OAAO,MAAM,CAAC,MAAM,CAAC,0BAA0B,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAChH,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,QAAgB,EAAE,MAA0B;IAChF,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,QAAQ,EAAE,CAAC;IACpC,IAAI,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,IAAI,IAAI,CAAC,YAAY,CAC5B,MAAM,EACN,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,CACvJ,CAAC;QACF,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,OAAO,mBAAmB,CAAC,QAAQ,CAAC,KAAK,SAAS,CAAC;AACrD,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC7C,+EAA+E;IAC/E,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,QAAQ,GAAG,CAAC,CAAC;IAClH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgB,EAAE,MAAc;IAC1D,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC,qBAAqB,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7G,CAAC"}
|
package/dist/refund.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EntitlementEvent, EntitlementRecord, PaymentGrant } from "./contract.js";
|
|
2
|
+
/** Where a grant added to `record` at `now` starts: the latest of the trial's end, dated paid access and `now`. */
|
|
3
|
+
export declare function getGrantStart(record: EntitlementRecord, now: Date): Date;
|
|
4
|
+
/**
|
|
5
|
+
* What a plan's event added to `record`: the period from where access ended (the trial, dated paid
|
|
6
|
+
* access or `now`, as `getPlanGrant` starts it) to the event's end, or lifetime access. Null for an
|
|
7
|
+
* event that is not a plan grant.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getPaymentGrant(record: EntitlementRecord, event: EntitlementEvent, now: Date): PaymentGrant | null;
|
|
10
|
+
/**
|
|
11
|
+
* `instant` moved back by `days` local days in `timezone`, at the same local time of day (the first
|
|
12
|
+
* instant of the day when that time does not exist there).
|
|
13
|
+
*/
|
|
14
|
+
export declare function moveBackByDays(instant: Date, days: number, timezone: string): Date;
|
|
15
|
+
/**
|
|
16
|
+
* The local days of a period not used yet at `now`, `[max(from, now), until)`, today included; 0
|
|
17
|
+
* once the period has ended. A refund moves dated access, and every period stacked after this one,
|
|
18
|
+
* back by this many days.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getUnusedDays(grant: Extract<PaymentGrant, {
|
|
21
|
+
kind: "period";
|
|
22
|
+
}>, now: Date, timezone: string): number;
|
|
23
|
+
/** The part of a payment a refund returns: `refunded` newly refunded out of the `outstanding` amount not refunded before. */
|
|
24
|
+
export interface RefundShare {
|
|
25
|
+
readonly refunded: number;
|
|
26
|
+
readonly outstanding: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RefundTiming {
|
|
29
|
+
readonly now: Date;
|
|
30
|
+
readonly timezone: string;
|
|
31
|
+
/** The part of the payment refunded; omitted for a full refund. */
|
|
32
|
+
readonly share?: RefundShare;
|
|
33
|
+
}
|
|
34
|
+
/** Whether `share` returns everything not refunded before (or is absent: a full refund). */
|
|
35
|
+
export declare function isFullShare(share: RefundShare | undefined): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The local days a refund takes back from a period: all its unused days (`getUnusedDays`) for a
|
|
38
|
+
* full share, else that share of them rounded down, so the account keeps any part of a day.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getTakenBackDays(grant: Extract<PaymentGrant, {
|
|
41
|
+
kind: "period";
|
|
42
|
+
}>, timing: RefundTiming): number;
|
|
43
|
+
/**
|
|
44
|
+
* The change a refund of a payment that granted `grant` makes to `record`: a period moves dated
|
|
45
|
+
* paid access back by the days it takes back (`getTakenBackDays`), and a full refund of a lifetime
|
|
46
|
+
* ends lifetime access. Null when nothing is taken back (the period is used up, the share rounds to
|
|
47
|
+
* no day, a partial refund of a lifetime, or dated access is already gone).
|
|
48
|
+
*/
|
|
49
|
+
export declare function getRefundEvent(record: EntitlementRecord, grant: PaymentGrant, timing: RefundTiming): EntitlementEvent | null;
|
|
50
|
+
/** `instant` moved forward by `days` local days in `timezone`, as `moveBackByDays` moves it back. */
|
|
51
|
+
export declare function moveForwardByDays(instant: Date, days: number, timezone: string): Date;
|
|
52
|
+
export interface RestoredDaysInput {
|
|
53
|
+
/** The local days refunds took from the payment's period so far. */
|
|
54
|
+
readonly takenBackDays: number;
|
|
55
|
+
/** The payment's refunded total before the failure. */
|
|
56
|
+
readonly refundedAmount: number;
|
|
57
|
+
/** The part of that total the failure gives back (at most `refundedAmount`). */
|
|
58
|
+
readonly restoredAmount: number;
|
|
59
|
+
/** The app's `partialRefunds` policy. */
|
|
60
|
+
readonly policy: "pro_rata" | "keep_access";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The local days a failed refund gives back. Under `pro_rata` the share of the days refunds took
|
|
64
|
+
* that the failed money is of the money refunded, rounded down, and every day once nothing stays
|
|
65
|
+
* refunded, so failures that undo every refund give back exactly what was taken. Under
|
|
66
|
+
* `keep_access` only the refund that completed the payment took days, so any failure gives them
|
|
67
|
+
* all back: the payment is no longer refunded in full.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getRestoredDays({ takenBackDays, refundedAmount, restoredAmount, policy }: RestoredDaysInput): number;
|
|
70
|
+
//# sourceMappingURL=refund.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refund.d.ts","sourceRoot":"","sources":["../src/refund.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAOvF,mHAAmH;AACnH,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAExE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,CAWlH;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIlF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGnH;AAED,6HAA6H;AAC7H,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAEnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAO/G;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,GAAG,IAAI,CAK5H;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErF;AAED,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,iBAAiB,GAAG,MAAM,CAOpH"}
|
package/dist/refund.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Refunds, pure: what one payment of a plan added to an account (stored with the payment) and the
|
|
2
|
+
// change a refund of it makes. A full refund takes back the part of the payment's period not used
|
|
3
|
+
// yet. Access ahead of now is one unbroken run (every grant starts where running access ends), so
|
|
4
|
+
// taking back those days moves the dated end back by as many local days; a period already used up
|
|
5
|
+
// takes nothing back, and a lapse between payments never lets an old refund eat a new period.
|
|
6
|
+
// A partial refund takes back the same share of those days as the share of the money not refunded
|
|
7
|
+
// before that it returns, rounded down: the refund that completes the amount has a share of one, so
|
|
8
|
+
// partial refunds that add up to the whole payment take back what one full refund would. A refund
|
|
9
|
+
// that fails later gives back the failed money's share of the days refunds took.
|
|
10
|
+
import { getDayNumber, getStartOfDay } from "./calendar.js";
|
|
11
|
+
/** The later of two instants. */
|
|
12
|
+
function getLater(first, second) {
|
|
13
|
+
return first > second ? first : second;
|
|
14
|
+
}
|
|
15
|
+
/** Where a grant added to `record` at `now` starts: the latest of the trial's end, dated paid access and `now`. */
|
|
16
|
+
export function getGrantStart(record, now) {
|
|
17
|
+
return [record.trialEndsAt, ...(record.paidUntil === null ? [] : [record.paidUntil])].reduce(getLater, now);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What a plan's event added to `record`: the period from where access ended (the trial, dated paid
|
|
21
|
+
* access or `now`, as `getPlanGrant` starts it) to the event's end, or lifetime access. Null for an
|
|
22
|
+
* event that is not a plan grant.
|
|
23
|
+
*/
|
|
24
|
+
export function getPaymentGrant(record, event, now) {
|
|
25
|
+
switch (event.type) {
|
|
26
|
+
case "grant_lifetime":
|
|
27
|
+
return { kind: "lifetime" };
|
|
28
|
+
case "grant": {
|
|
29
|
+
const from = getGrantStart(record, now);
|
|
30
|
+
return from < event.until ? { kind: "period", from, until: event.until } : null;
|
|
31
|
+
}
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `instant` moved back by `days` local days in `timezone`, at the same local time of day (the first
|
|
38
|
+
* instant of the day when that time does not exist there).
|
|
39
|
+
*/
|
|
40
|
+
export function moveBackByDays(instant, days, timezone) {
|
|
41
|
+
const day = getDayNumber(instant, timezone);
|
|
42
|
+
const timeOfDay = instant.getTime() - getStartOfDay(day, timezone).getTime();
|
|
43
|
+
return new Date(getStartOfDay(day - days, timezone).getTime() + timeOfDay);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The local days of a period not used yet at `now`, `[max(from, now), until)`, today included; 0
|
|
47
|
+
* once the period has ended. A refund moves dated access, and every period stacked after this one,
|
|
48
|
+
* back by this many days.
|
|
49
|
+
*/
|
|
50
|
+
export function getUnusedDays(grant, now, timezone) {
|
|
51
|
+
if (grant.until <= now)
|
|
52
|
+
return 0;
|
|
53
|
+
return Math.max(0, getDayNumber(grant.until, timezone) - getDayNumber(getLater(grant.from, now), timezone));
|
|
54
|
+
}
|
|
55
|
+
/** Whether `share` returns everything not refunded before (or is absent: a full refund). */
|
|
56
|
+
export function isFullShare(share) {
|
|
57
|
+
return share === undefined || share.refunded >= share.outstanding;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The local days a refund takes back from a period: all its unused days (`getUnusedDays`) for a
|
|
61
|
+
* full share, else that share of them rounded down, so the account keeps any part of a day.
|
|
62
|
+
*/
|
|
63
|
+
export function getTakenBackDays(grant, timing) {
|
|
64
|
+
const unusedDays = getUnusedDays(grant, timing.now, timing.timezone);
|
|
65
|
+
const { share } = timing;
|
|
66
|
+
if (share === undefined || isFullShare(share))
|
|
67
|
+
return unusedDays;
|
|
68
|
+
if (share.refunded <= 0)
|
|
69
|
+
return 0;
|
|
70
|
+
// Integers: at most 10^8 minor units times a period's days stays a safe integer.
|
|
71
|
+
return Math.floor((unusedDays * share.refunded) / share.outstanding);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The change a refund of a payment that granted `grant` makes to `record`: a period moves dated
|
|
75
|
+
* paid access back by the days it takes back (`getTakenBackDays`), and a full refund of a lifetime
|
|
76
|
+
* ends lifetime access. Null when nothing is taken back (the period is used up, the share rounds to
|
|
77
|
+
* no day, a partial refund of a lifetime, or dated access is already gone).
|
|
78
|
+
*/
|
|
79
|
+
export function getRefundEvent(record, grant, timing) {
|
|
80
|
+
if (grant.kind === "lifetime")
|
|
81
|
+
return isFullShare(timing.share) ? { type: "end_lifetime" } : null;
|
|
82
|
+
const days = getTakenBackDays(grant, timing);
|
|
83
|
+
if (record.paidUntil === null || days === 0)
|
|
84
|
+
return null;
|
|
85
|
+
return { type: "shorten", until: moveBackByDays(record.paidUntil, days, timing.timezone) };
|
|
86
|
+
}
|
|
87
|
+
/** `instant` moved forward by `days` local days in `timezone`, as `moveBackByDays` moves it back. */
|
|
88
|
+
export function moveForwardByDays(instant, days, timezone) {
|
|
89
|
+
return moveBackByDays(instant, -days, timezone);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The local days a failed refund gives back. Under `pro_rata` the share of the days refunds took
|
|
93
|
+
* that the failed money is of the money refunded, rounded down, and every day once nothing stays
|
|
94
|
+
* refunded, so failures that undo every refund give back exactly what was taken. Under
|
|
95
|
+
* `keep_access` only the refund that completed the payment took days, so any failure gives them
|
|
96
|
+
* all back: the payment is no longer refunded in full.
|
|
97
|
+
*/
|
|
98
|
+
export function getRestoredDays({ takenBackDays, refundedAmount, restoredAmount, policy }) {
|
|
99
|
+
if (takenBackDays <= 0 || restoredAmount <= 0 || refundedAmount <= 0)
|
|
100
|
+
return 0;
|
|
101
|
+
const remaining = refundedAmount - restoredAmount;
|
|
102
|
+
if (remaining <= 0)
|
|
103
|
+
return takenBackDays;
|
|
104
|
+
if (policy === "keep_access")
|
|
105
|
+
return takenBackDays;
|
|
106
|
+
// Integers: at most 10^8 minor units times a period's days stays a safe integer.
|
|
107
|
+
return Math.floor((takenBackDays * restoredAmount) / refundedAmount);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=refund.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refund.js","sourceRoot":"","sources":["../src/refund.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EntitlementRecord } from "./contract.js";
|
|
2
|
+
import { type EntitlementPolicy } from "./entitlement.js";
|
|
3
|
+
export declare const ACCESS_REMINDER_KINDS: readonly ["trial-ending", "paid-ending", "trial-ended", "paid-ended"];
|
|
4
|
+
export type AccessReminderKind = (typeof ACCESS_REMINDER_KINDS)[number];
|
|
5
|
+
/** The longest catch-up window for ended mails, in days. */
|
|
6
|
+
export declare const MAX_CATCH_UP_DAYS = 365;
|
|
7
|
+
/** Days after the day access ended during which its ended mail is still due. */
|
|
8
|
+
export declare const DEFAULT_CATCH_UP_DAYS = 3;
|
|
9
|
+
export interface AccessReminder {
|
|
10
|
+
readonly kind: AccessReminderKind;
|
|
11
|
+
/** The first instant without the access the mail is about. */
|
|
12
|
+
readonly endsAt: Date;
|
|
13
|
+
}
|
|
14
|
+
export interface AccessReminderInput {
|
|
15
|
+
readonly record: EntitlementRecord;
|
|
16
|
+
/** `auth.users.created_at`: a trial that ended by then was never a trial. */
|
|
17
|
+
readonly accountCreatedAt: Date;
|
|
18
|
+
readonly now: Date;
|
|
19
|
+
readonly policy: EntitlementPolicy;
|
|
20
|
+
/** An ended mail is due from the day access ended through this many local days after it. */
|
|
21
|
+
readonly catchUpDays: number;
|
|
22
|
+
}
|
|
23
|
+
/** The reminder `input.record` is due at `input.now`, or null (lifetime, outside every window). */
|
|
24
|
+
export declare function getAccessReminder(input: AccessReminderInput): AccessReminder | null;
|
|
25
|
+
/**
|
|
26
|
+
* The delivery scope of a reminder in `mailing.deliveries`: one mail per account, kind and end, so
|
|
27
|
+
* a new end (an extended trial, a renewal, a refund) is a new window and the same end never mails twice.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAccessReminderScope(kind: AccessReminderKind, userId: string, endsAt: Date): string;
|
|
30
|
+
//# sourceMappingURL=reminder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminder.d.ts","sourceRoot":"","sources":["../src/reminder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE9E,eAAO,MAAM,qBAAqB,uEAAwE,CAAC;AAC3G,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,mGAAmG;AACnG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,GAAG,cAAc,GAAG,IAAI,CAUnF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,CAErG"}
|
package/dist/reminder.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Which reminder mail an account is due, as a pure rule: the four states the in-app notice shows
|
|
2
|
+
// (trial or paid access ending, trial or paid access ended), decided from the account's record by
|
|
3
|
+
// the state machine. An ended mail is due only for a few days after the end, so turning reminders
|
|
4
|
+
// on never mails every account that lapsed long ago.
|
|
5
|
+
import { getDayNumber } from "./calendar.js";
|
|
6
|
+
import { resolveEntitlement } from "./entitlement.js";
|
|
7
|
+
export const ACCESS_REMINDER_KINDS = ["trial-ending", "paid-ending", "trial-ended", "paid-ended"];
|
|
8
|
+
/** The longest catch-up window for ended mails, in days. */
|
|
9
|
+
export const MAX_CATCH_UP_DAYS = 365;
|
|
10
|
+
/** Days after the day access ended during which its ended mail is still due. */
|
|
11
|
+
export const DEFAULT_CATCH_UP_DAYS = 3;
|
|
12
|
+
/** The reminder `input.record` is due at `input.now`, or null (lifetime, outside every window). */
|
|
13
|
+
export function getAccessReminder(input) {
|
|
14
|
+
const { record, accountCreatedAt, now, policy, catchUpDays } = input;
|
|
15
|
+
const entitlement = resolveEntitlement(record, now, policy);
|
|
16
|
+
if (entitlement.status !== "read_only") {
|
|
17
|
+
if (!entitlement.isEnding || entitlement.endsAt === null)
|
|
18
|
+
return null;
|
|
19
|
+
return { kind: entitlement.status === "trial" ? "trial-ending" : "paid-ending", endsAt: entitlement.endsAt };
|
|
20
|
+
}
|
|
21
|
+
if (getDayNumber(now, policy.timezone) - getDayNumber(entitlement.since, policy.timezone) > catchUpDays)
|
|
22
|
+
return null;
|
|
23
|
+
if (entitlement.reason === "paid_ended")
|
|
24
|
+
return { kind: "paid-ended", endsAt: entitlement.since };
|
|
25
|
+
return entitlement.since > accountCreatedAt ? { kind: "trial-ended", endsAt: entitlement.since } : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The delivery scope of a reminder in `mailing.deliveries`: one mail per account, kind and end, so
|
|
29
|
+
* a new end (an extended trial, a renewal, a refund) is a new window and the same end never mails twice.
|
|
30
|
+
*/
|
|
31
|
+
export function getAccessReminderScope(kind, userId, endsAt) {
|
|
32
|
+
return `billing.${kind}:${userId}:${String(endsAt.getTime())}`;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=reminder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminder.js","sourceRoot":"","sources":["../src/reminder.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,kGAAkG;AAClG,kGAAkG;AAClG,qDAAqD;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAA0B,MAAM,kBAAkB,CAAC;AAE9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,CAAU,CAAC;AAG3G,4DAA4D;AAC5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AACrC,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAkBvC,mGAAmG;AACnG,MAAM,UAAU,iBAAiB,CAAC,KAA0B;IAC1D,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IACrE,MAAM,WAAW,GAAG,kBAAkB,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,WAAW,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QACvC,IAAI,CAAC,WAAW,CAAC,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACtE,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC;IAC/G,CAAC;IACD,IAAI,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,WAAW;QAAE,OAAO,IAAI,CAAC;IACrH,IAAI,WAAW,CAAC,MAAM,KAAK,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC;IAClG,OAAO,WAAW,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1G,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAwB,EAAE,MAAc,EAAE,MAAY;IAC3F,OAAO,WAAW,IAAI,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;AACjE,CAAC"}
|