@antelopejs/interface-subscriptions 0.0.0-stage → 0.0.2
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 +190 -0
- package/README.md +215 -2
- package/dist/antelope.test.d.ts +2 -0
- package/dist/antelope.test.js +10 -0
- package/dist/antelope.test.js.map +1 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.js +85 -0
- package/dist/errors.js.map +1 -0
- package/dist/functions.d.ts +101 -0
- package/dist/functions.js +104 -0
- package/dist/functions.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/subscriptions-changes.test.d.ts +1 -0
- package/dist/tests/subscriptions-changes.test.js +194 -0
- package/dist/tests/subscriptions-changes.test.js.map +1 -0
- package/dist/tests/subscriptions.test.d.ts +1 -0
- package/dist/tests/subscriptions.test.js +144 -0
- package/dist/tests/subscriptions.test.js.map +1 -0
- package/dist/tests/support.d.ts +77 -0
- package/dist/tests/support.js +181 -0
- package/dist/tests/support.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.js +71 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +16 -0
- package/dist/validation.js +59 -0
- package/dist/validation.js.map +1 -0
- package/docs/1.introduction.md +98 -0
- package/docs/2.subscriptions.md +147 -0
- package/docs/3.events.md +79 -0
- package/docs/4.implementing.md +158 -0
- package/docs/5.provider-capability-matrix.md +61 -0
- package/docs/tsconfig.json +14 -0
- package/package.json +80 -4
- package/skills/managing-subscriptions/SKILL.md +154 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Amount, type Payment } from "@antelopejs/interface-payment";
|
|
2
|
+
import { type Subscription, type SubscriptionRequest } from "../index";
|
|
3
|
+
export declare const SUITE_TIMEOUT = 60000;
|
|
4
|
+
export declare const PERIOD_VALUE = 1900;
|
|
5
|
+
export declare const RETURN_URL = "https://merchant.invalid/return/subscription";
|
|
6
|
+
export declare const DAY_MS: number;
|
|
7
|
+
export declare const SHORTEST_MONTH_MS: number;
|
|
8
|
+
export declare const LONGEST_MONTH_MS: number;
|
|
9
|
+
export declare const TRIAL_DAYS = 14;
|
|
10
|
+
/**
|
|
11
|
+
* Providers keep creation times and schedule times to the second. Two
|
|
12
|
+
* subscriptions created within the same second may list in either order, and a
|
|
13
|
+
* requested trial end or billing anchor comes back truncated to the second.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROVIDER_CLOCK_RESOLUTION_MS = 1100;
|
|
16
|
+
/** A time `ahead` from now, on a whole second, which every provider can echo exactly. */
|
|
17
|
+
export declare function inWholeSeconds(ahead: number): number;
|
|
18
|
+
/** An identifier no provider has a subscription for. */
|
|
19
|
+
export declare const ABSENT_ID: string;
|
|
20
|
+
/**
|
|
21
|
+
* A provider-specific token that the payment interface charges successfully
|
|
22
|
+
* on-session with `savePaymentMethod`, and whose saved method it then charges
|
|
23
|
+
* off-session. The same variable the payment interface's suite reads.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TEST_METHOD: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* A provider-specific token that saves on-session, and whose saved method the
|
|
28
|
+
* provider declines off-session.
|
|
29
|
+
*
|
|
30
|
+
* There is no portable way to obtain one. Without it, the declined-first-period
|
|
31
|
+
* test is skipped.
|
|
32
|
+
*/
|
|
33
|
+
export declare const TEST_METHOD_DECLINES_LATER: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Set when the implementation's events can reach this process during a test
|
|
36
|
+
* run. A native provider delivers them from webhooks, which a test container
|
|
37
|
+
* cannot usually receive; the generic fallback raises them in-process.
|
|
38
|
+
*/
|
|
39
|
+
export declare const EVENTS_OBSERVABLE: boolean;
|
|
40
|
+
export declare const CHARGEABLE = "requires ANTELOPE_PAYMENT_TEST_METHOD";
|
|
41
|
+
export declare const DECLINES_LATER = "requires ANTELOPE_SUBSCRIPTIONS_TEST_METHOD_DECLINES_LATER";
|
|
42
|
+
export declare const OBSERVABLE = "requires ANTELOPE_SUBSCRIPTIONS_TEST_EVENTS";
|
|
43
|
+
export declare function amount(value: number): Amount;
|
|
44
|
+
export declare function key(): string;
|
|
45
|
+
export declare function reference(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Asserts a timestamp is epoch milliseconds rather than epoch seconds, and is
|
|
48
|
+
* not further from now than `horizon` allows.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assertEpochMillis(value: number, label: string, horizon?: number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Asserts the invariants every subscription holds, whatever its state.
|
|
53
|
+
*/
|
|
54
|
+
export declare function assertSubscriptionShape(subscription: Subscription, request: SubscriptionRequest): void;
|
|
55
|
+
/**
|
|
56
|
+
* Polls until the payment leaves `pending`, the only status an off-session
|
|
57
|
+
* charge can sit in.
|
|
58
|
+
*/
|
|
59
|
+
export declare function settledPayment(id: string): Promise<Payment>;
|
|
60
|
+
/**
|
|
61
|
+
* A customer with a method saved the only way the contract allows: an
|
|
62
|
+
* on-session payment that saves it, taken with the payer present.
|
|
63
|
+
*/
|
|
64
|
+
export interface Payer {
|
|
65
|
+
customer: string;
|
|
66
|
+
paymentMethod: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function newPayer(token?: string | undefined): Promise<Payer>;
|
|
69
|
+
/** Saves another method against an existing customer and returns its id. */
|
|
70
|
+
export declare function saveMethod(customer: string, token?: string | undefined): Promise<string>;
|
|
71
|
+
export declare function monthly(payer: Payer, overrides?: Partial<SubscriptionRequest>): SubscriptionRequest;
|
|
72
|
+
export declare function subscribe(payer: Payer, overrides?: Partial<SubscriptionRequest>): Promise<Subscription>;
|
|
73
|
+
/**
|
|
74
|
+
* Asserts that a call rejects with exactly the given error class, rather than
|
|
75
|
+
* any Error at all.
|
|
76
|
+
*/
|
|
77
|
+
export declare function rejectsWith(work: Promise<unknown>, expected: abstract new (...args: never[]) => Error, label: string): Promise<Error>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OBSERVABLE = exports.DECLINES_LATER = exports.CHARGEABLE = exports.EVENTS_OBSERVABLE = exports.TEST_METHOD_DECLINES_LATER = exports.TEST_METHOD = exports.ABSENT_ID = exports.PROVIDER_CLOCK_RESOLUTION_MS = exports.TRIAL_DAYS = exports.LONGEST_MONTH_MS = exports.SHORTEST_MONTH_MS = exports.DAY_MS = exports.RETURN_URL = exports.PERIOD_VALUE = exports.SUITE_TIMEOUT = void 0;
|
|
7
|
+
exports.inWholeSeconds = inWholeSeconds;
|
|
8
|
+
exports.amount = amount;
|
|
9
|
+
exports.key = key;
|
|
10
|
+
exports.reference = reference;
|
|
11
|
+
exports.assertEpochMillis = assertEpochMillis;
|
|
12
|
+
exports.assertSubscriptionShape = assertSubscriptionShape;
|
|
13
|
+
exports.settledPayment = settledPayment;
|
|
14
|
+
exports.newPayer = newPayer;
|
|
15
|
+
exports.saveMethod = saveMethod;
|
|
16
|
+
exports.monthly = monthly;
|
|
17
|
+
exports.subscribe = subscribe;
|
|
18
|
+
exports.rejectsWith = rejectsWith;
|
|
19
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
20
|
+
const node_crypto_1 = require("node:crypto");
|
|
21
|
+
const promises_1 = require("node:timers/promises");
|
|
22
|
+
const interface_payment_1 = require("@antelopejs/interface-payment");
|
|
23
|
+
const index_1 = require("../index");
|
|
24
|
+
exports.SUITE_TIMEOUT = 60_000;
|
|
25
|
+
const SETTLE_TIMEOUT = 30_000;
|
|
26
|
+
const SETTLE_INTERVAL = 500;
|
|
27
|
+
exports.PERIOD_VALUE = 1900;
|
|
28
|
+
exports.RETURN_URL = "https://merchant.invalid/return/subscription";
|
|
29
|
+
exports.DAY_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
const SHORTEST_MONTH_DAYS = 28;
|
|
31
|
+
const LONGEST_MONTH_DAYS = 31;
|
|
32
|
+
exports.SHORTEST_MONTH_MS = SHORTEST_MONTH_DAYS * exports.DAY_MS;
|
|
33
|
+
exports.LONGEST_MONTH_MS = LONGEST_MONTH_DAYS * exports.DAY_MS;
|
|
34
|
+
exports.TRIAL_DAYS = 14;
|
|
35
|
+
/**
|
|
36
|
+
* Providers keep creation times and schedule times to the second. Two
|
|
37
|
+
* subscriptions created within the same second may list in either order, and a
|
|
38
|
+
* requested trial end or billing anchor comes back truncated to the second.
|
|
39
|
+
*/
|
|
40
|
+
exports.PROVIDER_CLOCK_RESOLUTION_MS = 1100;
|
|
41
|
+
const SECOND_MS = 1000;
|
|
42
|
+
/** A time `ahead` from now, on a whole second, which every provider can echo exactly. */
|
|
43
|
+
function inWholeSeconds(ahead) {
|
|
44
|
+
return Math.floor((Date.now() + ahead) / SECOND_MS) * SECOND_MS;
|
|
45
|
+
}
|
|
46
|
+
/** An identifier no provider has a subscription for. */
|
|
47
|
+
exports.ABSENT_ID = `antelope-absent-${"0".repeat(24)}`;
|
|
48
|
+
/**
|
|
49
|
+
* A provider-specific token that the payment interface charges successfully
|
|
50
|
+
* on-session with `savePaymentMethod`, and whose saved method it then charges
|
|
51
|
+
* off-session. The same variable the payment interface's suite reads.
|
|
52
|
+
*/
|
|
53
|
+
exports.TEST_METHOD = process.env.ANTELOPE_PAYMENT_TEST_METHOD;
|
|
54
|
+
/**
|
|
55
|
+
* A provider-specific token that saves on-session, and whose saved method the
|
|
56
|
+
* provider declines off-session.
|
|
57
|
+
*
|
|
58
|
+
* There is no portable way to obtain one. Without it, the declined-first-period
|
|
59
|
+
* test is skipped.
|
|
60
|
+
*/
|
|
61
|
+
exports.TEST_METHOD_DECLINES_LATER = process.env.ANTELOPE_SUBSCRIPTIONS_TEST_METHOD_DECLINES_LATER;
|
|
62
|
+
/**
|
|
63
|
+
* Set when the implementation's events can reach this process during a test
|
|
64
|
+
* run. A native provider delivers them from webhooks, which a test container
|
|
65
|
+
* cannot usually receive; the generic fallback raises them in-process.
|
|
66
|
+
*/
|
|
67
|
+
exports.EVENTS_OBSERVABLE = Boolean(process.env.ANTELOPE_SUBSCRIPTIONS_TEST_EVENTS);
|
|
68
|
+
const CURRENCY = process.env.ANTELOPE_PAYMENT_TEST_CURRENCY || "EUR";
|
|
69
|
+
exports.CHARGEABLE = "requires ANTELOPE_PAYMENT_TEST_METHOD";
|
|
70
|
+
exports.DECLINES_LATER = "requires ANTELOPE_SUBSCRIPTIONS_TEST_METHOD_DECLINES_LATER";
|
|
71
|
+
exports.OBSERVABLE = "requires ANTELOPE_SUBSCRIPTIONS_TEST_EVENTS";
|
|
72
|
+
function amount(value) {
|
|
73
|
+
return { value, currency: CURRENCY };
|
|
74
|
+
}
|
|
75
|
+
function key() {
|
|
76
|
+
return `antelope-conformance-${(0, node_crypto_1.randomUUID)()}`;
|
|
77
|
+
}
|
|
78
|
+
function reference() {
|
|
79
|
+
return `conformance-${(0, node_crypto_1.randomUUID)()}`;
|
|
80
|
+
}
|
|
81
|
+
const EPOCH_MILLIS_FLOOR = Date.UTC(2020, 0, 1);
|
|
82
|
+
const CLOCK_SKEW_ALLOWANCE = 5 * 60 * 1000;
|
|
83
|
+
/**
|
|
84
|
+
* Asserts a timestamp is epoch milliseconds rather than epoch seconds, and is
|
|
85
|
+
* not further from now than `horizon` allows.
|
|
86
|
+
*/
|
|
87
|
+
function assertEpochMillis(value, label, horizon = CLOCK_SKEW_ALLOWANCE) {
|
|
88
|
+
strict_1.default.ok(Number.isInteger(value), `${label} must be an integer`);
|
|
89
|
+
strict_1.default.ok(value > EPOCH_MILLIS_FLOOR, `${label} is ${value}, which is not epoch milliseconds (seconds, perhaps?)`);
|
|
90
|
+
strict_1.default.ok(value < Date.now() + horizon, `${label} is ${value}, further ahead than expected`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Asserts the invariants every subscription holds, whatever its state.
|
|
94
|
+
*/
|
|
95
|
+
function assertSubscriptionShape(subscription, request) {
|
|
96
|
+
strict_1.default.ok(subscription.id.length > 0, "subscription id must be non-empty");
|
|
97
|
+
strict_1.default.ok(index_1.SUBSCRIPTION_STATUSES.includes(subscription.status), `status ${subscription.status} is outside the contract`);
|
|
98
|
+
strict_1.default.equal(subscription.reference, request.reference);
|
|
99
|
+
strict_1.default.equal(subscription.customer, request.customer);
|
|
100
|
+
strict_1.default.equal(subscription.interval, request.interval);
|
|
101
|
+
strict_1.default.equal(subscription.intervalCount, request.intervalCount ?? 1);
|
|
102
|
+
strict_1.default.deepEqual(subscription.amount, request.amount);
|
|
103
|
+
strict_1.default.equal(typeof subscription.cancelAtPeriodEnd, "boolean");
|
|
104
|
+
assertEpochMillis(subscription.createdAt, "createdAt");
|
|
105
|
+
assertEpochMillis(subscription.currentPeriodStart, "currentPeriodStart");
|
|
106
|
+
assertEpochMillis(subscription.currentPeriodEnd, "currentPeriodEnd", 400 * exports.DAY_MS);
|
|
107
|
+
strict_1.default.ok(subscription.currentPeriodEnd > subscription.currentPeriodStart, "a period must end after it starts");
|
|
108
|
+
const isCanceled = subscription.status === "canceled";
|
|
109
|
+
strict_1.default.equal(subscription.endedAt !== undefined, isCanceled, "endedAt is present exactly when the subscription has ended");
|
|
110
|
+
strict_1.default.equal(subscription.endReason !== undefined, isCanceled, "endReason is present exactly when the subscription has ended");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Polls until the payment leaves `pending`, the only status an off-session
|
|
114
|
+
* charge can sit in.
|
|
115
|
+
*/
|
|
116
|
+
async function settledPayment(id) {
|
|
117
|
+
const deadline = Date.now() + SETTLE_TIMEOUT;
|
|
118
|
+
let payment = await (0, interface_payment_1.GetPayment)(id);
|
|
119
|
+
while (!(0, interface_payment_1.isTerminalPaymentStatus)(payment.status) && Date.now() < deadline) {
|
|
120
|
+
await (0, promises_1.setTimeout)(SETTLE_INTERVAL);
|
|
121
|
+
payment = await (0, interface_payment_1.GetPayment)(id);
|
|
122
|
+
}
|
|
123
|
+
return payment;
|
|
124
|
+
}
|
|
125
|
+
const customers = [];
|
|
126
|
+
after(async () => {
|
|
127
|
+
await Promise.all(customers.map((id) => (0, interface_payment_1.DeleteCustomer)(id, key()).catch(() => undefined)));
|
|
128
|
+
});
|
|
129
|
+
async function newPayer(token = exports.TEST_METHOD) {
|
|
130
|
+
const customer = await (0, interface_payment_1.CreateCustomer)({ email: `${(0, node_crypto_1.randomUUID)()}@merchant.invalid`, name: "Conformance Payer" }, key());
|
|
131
|
+
customers.push(customer.id);
|
|
132
|
+
return {
|
|
133
|
+
customer: customer.id,
|
|
134
|
+
paymentMethod: await saveMethod(customer.id, token),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Saves another method against an existing customer and returns its id. */
|
|
138
|
+
async function saveMethod(customer, token = exports.TEST_METHOD) {
|
|
139
|
+
const before = await (0, interface_payment_1.ListPaymentMethods)(customer);
|
|
140
|
+
const payment = await (0, interface_payment_1.CreatePayment)({
|
|
141
|
+
amount: amount(exports.PERIOD_VALUE),
|
|
142
|
+
reference: reference(),
|
|
143
|
+
customer,
|
|
144
|
+
paymentMethod: token,
|
|
145
|
+
savePaymentMethod: true,
|
|
146
|
+
returnUrl: exports.RETURN_URL,
|
|
147
|
+
}, key());
|
|
148
|
+
const settled = await settledPayment(payment.id);
|
|
149
|
+
strict_1.default.equal(settled.status, "succeeded", "the saving payment must succeed");
|
|
150
|
+
const known = new Set(before.map((method) => method.id));
|
|
151
|
+
const saved = (await (0, interface_payment_1.ListPaymentMethods)(customer)).find((method) => !known.has(method.id));
|
|
152
|
+
strict_1.default.ok(saved, "the saving payment must leave a method saved");
|
|
153
|
+
return saved.id;
|
|
154
|
+
}
|
|
155
|
+
function monthly(payer, overrides = {}) {
|
|
156
|
+
return {
|
|
157
|
+
reference: reference(),
|
|
158
|
+
customer: payer.customer,
|
|
159
|
+
paymentMethod: payer.paymentMethod,
|
|
160
|
+
amount: amount(exports.PERIOD_VALUE),
|
|
161
|
+
interval: "month",
|
|
162
|
+
description: "Conformance subscription",
|
|
163
|
+
...overrides,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function subscribe(payer, overrides = {}) {
|
|
167
|
+
const request = monthly(payer, overrides);
|
|
168
|
+
const subscription = await (0, index_1.CreateSubscription)(request, key());
|
|
169
|
+
assertSubscriptionShape(subscription, request);
|
|
170
|
+
return subscription;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Asserts that a call rejects with exactly the given error class, rather than
|
|
174
|
+
* any Error at all.
|
|
175
|
+
*/
|
|
176
|
+
async function rejectsWith(work, expected, label) {
|
|
177
|
+
const error = await work.then(() => undefined, (reason) => reason);
|
|
178
|
+
strict_1.default.ok(error instanceof expected, `${label}: expected ${expected.name}, got ${String(error)}`);
|
|
179
|
+
return error;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=support.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"support.js","sourceRoot":"","sources":["../../src/tests/support.ts"],"names":[],"mappings":";;;;;;AA0CA,wCAEC;AAsCD,wBAEC;AAED,kBAEC;AAED,8BAEC;AASD,8CAcC;AAKD,0DAqCC;AAMD,wCAQC;AAmBD,4BAUC;AAGD,gCAwBC;AAED,0BAaC;AAED,8BAQC;AAMD,kCAcC;AAhRD,gEAAwC;AACxC,6CAAyC;AACzC,mDAA2D;AAC3D,qEASuC;AAEvC,oCAKkB;AAEL,QAAA,aAAa,GAAG,MAAM,CAAC;AACpC,MAAM,cAAc,GAAG,MAAM,CAAC;AAC9B,MAAM,eAAe,GAAG,GAAG,CAAC;AACf,QAAA,YAAY,GAAG,IAAI,CAAC;AACpB,QAAA,UAAU,GAAG,8CAA8C,CAAC;AAC5D,QAAA,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC1C,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACjB,QAAA,iBAAiB,GAAG,mBAAmB,GAAG,cAAM,CAAC;AACjD,QAAA,gBAAgB,GAAG,kBAAkB,GAAG,cAAM,CAAC;AAC/C,QAAA,UAAU,GAAG,EAAE,CAAC;AAE7B;;;;GAIG;AACU,QAAA,4BAA4B,GAAG,IAAI,CAAC;AACjD,MAAM,SAAS,GAAG,IAAI,CAAC;AAEvB,yFAAyF;AACzF,SAAgB,cAAc,CAAC,KAAa;IAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;AAClE,CAAC;AAED,wDAAwD;AAC3C,QAAA,SAAS,GAAG,mBAAmB,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;AAE7D;;;;GAIG;AACU,QAAA,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC;AAEpE;;;;;;GAMG;AACU,QAAA,0BAA0B,GACrC,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC;AAEhE;;;;GAIG;AACU,QAAA,iBAAiB,GAAG,OAAO,CACtC,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAC/C,CAAC;AAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,KAAK,CAAC;AAExD,QAAA,UAAU,GAAG,uCAAuC,CAAC;AACrD,QAAA,cAAc,GACzB,4DAA4D,CAAC;AAClD,QAAA,UAAU,GAAG,6CAA6C,CAAC;AAExE,SAAgB,MAAM,CAAC,KAAa;IAClC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACvC,CAAC;AAED,SAAgB,GAAG;IACjB,OAAO,wBAAwB,IAAA,wBAAU,GAAE,EAAE,CAAC;AAChD,CAAC;AAED,SAAgB,SAAS;IACvB,OAAO,eAAe,IAAA,wBAAU,GAAE,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAChD,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C;;;GAGG;AACH,SAAgB,iBAAiB,CAC/B,KAAa,EACb,KAAa,EACb,OAAO,GAAG,oBAAoB;IAE9B,gBAAM,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,qBAAqB,CAAC,CAAC;IAClE,gBAAM,CAAC,EAAE,CACP,KAAK,GAAG,kBAAkB,EAC1B,GAAG,KAAK,OAAO,KAAK,uDAAuD,CAC5E,CAAC;IACF,gBAAM,CAAC,EAAE,CACP,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAC5B,GAAG,KAAK,OAAO,KAAK,+BAA+B,CACpD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,uBAAuB,CACrC,YAA0B,EAC1B,OAA4B;IAE5B,gBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,gBAAM,CAAC,EAAE,CACP,6BAAqB,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,EACnD,UAAU,YAAY,CAAC,MAAM,0BAA0B,CACxD,CAAC;IACF,gBAAM,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,gBAAM,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtD,gBAAM,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtD,gBAAM,CAAC,KAAK,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC;IACrE,gBAAM,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,gBAAM,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAC;IAC/D,iBAAiB,CAAC,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACvD,iBAAiB,CAAC,YAAY,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAC;IACzE,iBAAiB,CACf,YAAY,CAAC,gBAAgB,EAC7B,kBAAkB,EAClB,GAAG,GAAG,cAAM,CACb,CAAC;IACF,gBAAM,CAAC,EAAE,CACP,YAAY,CAAC,gBAAgB,GAAG,YAAY,CAAC,kBAAkB,EAC/D,mCAAmC,CACpC,CAAC;IACF,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,KAAK,UAAU,CAAC;IACtD,gBAAM,CAAC,KAAK,CACV,YAAY,CAAC,OAAO,KAAK,SAAS,EAClC,UAAU,EACV,4DAA4D,CAC7D,CAAC;IACF,gBAAM,CAAC,KAAK,CACV,YAAY,CAAC,SAAS,KAAK,SAAS,EACpC,UAAU,EACV,8DAA8D,CAC/D,CAAC;AACJ,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAAC,EAAU;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC;IAC7C,IAAI,OAAO,GAAG,MAAM,IAAA,8BAAU,EAAC,EAAE,CAAC,CAAC;IACnC,OAAO,CAAC,IAAA,2CAAuB,EAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QACzE,MAAM,IAAA,qBAAK,EAAC,eAAe,CAAC,CAAC;QAC7B,OAAO,GAAG,MAAM,IAAA,8BAAU,EAAC,EAAE,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,SAAS,GAAa,EAAE,CAAC;AAE/B,KAAK,CAAC,KAAK,IAAI,EAAE;IACf,MAAM,OAAO,CAAC,GAAG,CACf,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAA,kCAAc,EAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CACxE,CAAC;AACJ,CAAC,CAAC,CAAC;AAWI,KAAK,UAAU,QAAQ,CAAC,KAAK,GAAG,mBAAW;IAChD,MAAM,QAAQ,GAAG,MAAM,IAAA,kCAAc,EACnC,EAAE,KAAK,EAAE,GAAG,IAAA,wBAAU,GAAE,mBAAmB,EAAE,IAAI,EAAE,mBAAmB,EAAE,EACxE,GAAG,EAAE,CACN,CAAC;IACF,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC5B,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,aAAa,EAAE,MAAM,UAAU,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,4EAA4E;AACrE,KAAK,UAAU,UAAU,CAC9B,QAAgB,EAChB,KAAK,GAAG,mBAAW;IAEnB,MAAM,MAAM,GAAG,MAAM,IAAA,sCAAkB,EAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,MAAM,IAAA,iCAAa,EACjC;QACE,MAAM,EAAE,MAAM,CAAC,oBAAY,CAAC;QAC5B,SAAS,EAAE,SAAS,EAAE;QACtB,QAAQ;QACR,aAAa,EAAE,KAAK;QACpB,iBAAiB,EAAE,IAAI;QACvB,SAAS,EAAE,kBAAU;KACtB,EACD,GAAG,EAAE,CACN,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACjD,gBAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,iCAAiC,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,CAAC,MAAM,IAAA,sCAAkB,EAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CACrD,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAClC,CAAC;IACF,gBAAM,CAAC,EAAE,CAAC,KAAK,EAAE,8CAA8C,CAAC,CAAC;IACjE,OAAO,KAAK,CAAC,EAAE,CAAC;AAClB,CAAC;AAED,SAAgB,OAAO,CACrB,KAAY,EACZ,YAA0C,EAAE;IAE5C,OAAO;QACL,SAAS,EAAE,SAAS,EAAE;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,MAAM,EAAE,MAAM,CAAC,oBAAY,CAAC;QAC5B,QAAQ,EAAE,OAAO;QACjB,WAAW,EAAE,0BAA0B;QACvC,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,KAAY,EACZ,YAA0C,EAAE;IAE5C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,MAAM,IAAA,0BAAkB,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9D,uBAAuB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,WAAW,CAC/B,IAAsB,EACtB,QAAkD,EAClD,KAAa;IAEb,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAC3B,GAAG,EAAE,CAAC,SAAS,EACf,CAAC,MAAe,EAAE,EAAE,CAAC,MAAM,CAC5B,CAAC;IACF,gBAAM,CAAC,EAAE,CACP,KAAK,YAAY,QAAQ,EACzB,GAAG,KAAK,cAAc,QAAQ,CAAC,IAAI,SAAS,MAAM,CAAC,KAAK,CAAC,EAAE,CAC5D,CAAC;IACF,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Amount } from "@antelopejs/interface-payment";
|
|
2
|
+
/**
|
|
3
|
+
* Every value {@link SubscriptionInterval} can take.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SUBSCRIPTION_INTERVALS: readonly ["day", "week", "month", "year"];
|
|
6
|
+
/**
|
|
7
|
+
* The unit a billing period is counted in.
|
|
8
|
+
*
|
|
9
|
+
* Calendar units are calendar units: a `month` period that starts on 31 January
|
|
10
|
+
* ends on the last day of February, and the one after it ends on 31 March. The
|
|
11
|
+
* day of the month comes from the billing anchor, not from the end of the
|
|
12
|
+
* previous period, so short months do not drift the cycle.
|
|
13
|
+
*/
|
|
14
|
+
export type SubscriptionInterval = (typeof SUBSCRIPTION_INTERVALS)[number];
|
|
15
|
+
/**
|
|
16
|
+
* The longest billing period every implementation must accept: one year.
|
|
17
|
+
*
|
|
18
|
+
* Providers cap the period differently. A request for a longer one may be
|
|
19
|
+
* refused with {@link InvalidSubscriptionRequestError}.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_PORTABLE_PERIOD_DAYS = 366;
|
|
22
|
+
/**
|
|
23
|
+
* Every value {@link SubscriptionStatus} can take, in lifecycle order.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SUBSCRIPTION_STATUSES: readonly ["trialing", "active", "past_due", "canceled"];
|
|
26
|
+
/**
|
|
27
|
+
* Normalised subscription status.
|
|
28
|
+
*
|
|
29
|
+
* | Status | Meaning |
|
|
30
|
+
* | --- | --- |
|
|
31
|
+
* | `trialing` | Before {@link Subscription.trialEnd}. Nothing has been charged. |
|
|
32
|
+
* | `active` | The current period is paid for, or not yet due. |
|
|
33
|
+
* | `past_due` | Charging the current period failed. The implementation is retrying under its own policy. |
|
|
34
|
+
* | `canceled` | Ended. Terminal: nothing is charged again. |
|
|
35
|
+
*
|
|
36
|
+
* `past_due` is reported the same way everywhere, but what happens next is
|
|
37
|
+
* not portable: native providers follow their configured retry schedule, and
|
|
38
|
+
* the generic fallback follows its documented policy. Decide access by status;
|
|
39
|
+
* do not predict when a `past_due` subscription will recover or end.
|
|
40
|
+
*/
|
|
41
|
+
export type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
|
|
42
|
+
/**
|
|
43
|
+
* Whether a subscription has ended for good.
|
|
44
|
+
*
|
|
45
|
+
* @param status - The status to classify
|
|
46
|
+
* @returns true for `canceled` only
|
|
47
|
+
*/
|
|
48
|
+
export declare function isTerminalSubscriptionStatus(status: SubscriptionStatus): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Every value {@link SubscriptionEndReason} can take.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SUBSCRIPTION_END_REASONS: readonly ["requested", "payment_failed"];
|
|
53
|
+
/**
|
|
54
|
+
* Why a subscription ended: someone cancelled it, or the implementation gave up
|
|
55
|
+
* charging it.
|
|
56
|
+
*/
|
|
57
|
+
export type SubscriptionEndReason = (typeof SUBSCRIPTION_END_REASONS)[number];
|
|
58
|
+
/**
|
|
59
|
+
* Every value {@link CancellationTiming} can take.
|
|
60
|
+
*/
|
|
61
|
+
export declare const CANCELLATION_TIMINGS: readonly ["now", "period_end"];
|
|
62
|
+
/**
|
|
63
|
+
* When {@link CancelSubscription} takes effect.
|
|
64
|
+
*
|
|
65
|
+
* - `now` ends the subscription immediately. Nothing is refunded: a refund is a
|
|
66
|
+
* separate `RefundPayment` through the payment interface.
|
|
67
|
+
* - `period_end` lets the period that has been paid for run out, and ends the
|
|
68
|
+
* subscription then instead of renewing it. The status does not change until
|
|
69
|
+
* it ends.
|
|
70
|
+
*/
|
|
71
|
+
export type CancellationTiming = (typeof CANCELLATION_TIMINGS)[number];
|
|
72
|
+
/**
|
|
73
|
+
* A subscription, in whatever state it was last reported in.
|
|
74
|
+
*
|
|
75
|
+
* `id` is opaque. Correlate with your own records through
|
|
76
|
+
* {@link Subscription.reference}.
|
|
77
|
+
*/
|
|
78
|
+
export interface Subscription {
|
|
79
|
+
/** Opaque identifier. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** The caller's own identifier, echoed back from the request. */
|
|
82
|
+
reference: string;
|
|
83
|
+
/** Opaque payment-interface customer identifier. */
|
|
84
|
+
customer: string;
|
|
85
|
+
/** Opaque payment-interface identifier of the saved method charged. */
|
|
86
|
+
paymentMethod: string;
|
|
87
|
+
/** Charged once per period. */
|
|
88
|
+
amount: Amount;
|
|
89
|
+
/** The unit a period is counted in. */
|
|
90
|
+
interval: SubscriptionInterval;
|
|
91
|
+
/** How many `interval`s make one period. At least 1. */
|
|
92
|
+
intervalCount: number;
|
|
93
|
+
/** Normalised status. */
|
|
94
|
+
status: SubscriptionStatus;
|
|
95
|
+
/**
|
|
96
|
+
* Unix epoch milliseconds. Start of the period most recently billed for, or
|
|
97
|
+
* of the trial or pre-anchor period before the first charge.
|
|
98
|
+
*/
|
|
99
|
+
currentPeriodStart: number;
|
|
100
|
+
/**
|
|
101
|
+
* Unix epoch milliseconds. When the current period ends and the next one is
|
|
102
|
+
* charged, unless {@link Subscription.cancelAtPeriodEnd} is set.
|
|
103
|
+
*/
|
|
104
|
+
currentPeriodEnd: number;
|
|
105
|
+
/** Unix epoch milliseconds. Present when the subscription began with a trial. */
|
|
106
|
+
trialEnd?: number;
|
|
107
|
+
/** The subscription ends at `currentPeriodEnd` instead of renewing. */
|
|
108
|
+
cancelAtPeriodEnd: boolean;
|
|
109
|
+
/** Unix epoch milliseconds. Present exactly when `status` is `canceled`. */
|
|
110
|
+
endedAt?: number;
|
|
111
|
+
/** Present exactly when `status` is `canceled`. */
|
|
112
|
+
endReason?: SubscriptionEndReason;
|
|
113
|
+
/**
|
|
114
|
+
* Opaque payment-interface identifier of the most recent charge attempt.
|
|
115
|
+
* Absent until something has been charged.
|
|
116
|
+
*/
|
|
117
|
+
latestPayment?: string;
|
|
118
|
+
/** Unix epoch milliseconds. */
|
|
119
|
+
createdAt: number;
|
|
120
|
+
/** The metadata the subscription was created with. */
|
|
121
|
+
metadata?: Record<string, string>;
|
|
122
|
+
/**
|
|
123
|
+
* What the provider said about this subscription, verbatim, for logs and
|
|
124
|
+
* support tickets.
|
|
125
|
+
*
|
|
126
|
+
* Read-only. Nothing a caller does with this object reaches the provider, and
|
|
127
|
+
* no request type accepts it: provider-specific behaviour belongs in the
|
|
128
|
+
* provider's own interface, where the coupling is visible.
|
|
129
|
+
*/
|
|
130
|
+
vendorData?: Readonly<Record<string, unknown>>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Parameters for {@link CreateSubscription}.
|
|
134
|
+
*
|
|
135
|
+
* The payer is not present. Take the mandate first, with an on-session
|
|
136
|
+
* payment-interface payment that sets `savePaymentMethod`, and pass the method
|
|
137
|
+
* it saved.
|
|
138
|
+
*/
|
|
139
|
+
export interface SubscriptionRequest {
|
|
140
|
+
/** Your own identifier for this subscription. */
|
|
141
|
+
reference: string;
|
|
142
|
+
/** Opaque payment-interface customer identifier. */
|
|
143
|
+
customer: string;
|
|
144
|
+
/**
|
|
145
|
+
* Opaque payment-interface identifier of a method saved against `customer`
|
|
146
|
+
* and chargeable off-session, from `ListPaymentMethods`.
|
|
147
|
+
*/
|
|
148
|
+
paymentMethod: string;
|
|
149
|
+
/** Charged once per period. A positive integer of minor units. */
|
|
150
|
+
amount: Amount;
|
|
151
|
+
/** The unit a period is counted in. */
|
|
152
|
+
interval: SubscriptionInterval;
|
|
153
|
+
/** How many `interval`s make one period. Defaults to 1. */
|
|
154
|
+
intervalCount?: number;
|
|
155
|
+
/**
|
|
156
|
+
* Unix epoch milliseconds. Start with a free trial that ends here; the first
|
|
157
|
+
* charge is made at this time. Must be in the future. Mutually exclusive with
|
|
158
|
+
* `billingAnchor`. Kept at the provider's precision, which is at least the
|
|
159
|
+
* second: pass a whole second to have it echoed exactly.
|
|
160
|
+
*/
|
|
161
|
+
trialEnd?: number;
|
|
162
|
+
/**
|
|
163
|
+
* Unix epoch milliseconds. Start the first paid period here rather than now;
|
|
164
|
+
* nothing is charged before it, and the status is `active` meanwhile. Must
|
|
165
|
+
* be in the future. Mutually exclusive with `trialEnd`. Kept at the
|
|
166
|
+
* provider's precision, which is at least the second.
|
|
167
|
+
*/
|
|
168
|
+
billingAnchor?: number;
|
|
169
|
+
/** Shown to the payer where supported. */
|
|
170
|
+
description?: string;
|
|
171
|
+
/** Round-tripped opaque key/value pairs. */
|
|
172
|
+
metadata?: Record<string, string>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every value {@link SubscriptionEventType} can take.
|
|
176
|
+
*/
|
|
177
|
+
export declare const SUBSCRIPTION_EVENT_TYPES: readonly ["subscription.created", "subscription.payment_succeeded", "subscription.payment_failed", "subscription.canceled"];
|
|
178
|
+
/**
|
|
179
|
+
* Normalised subscription event type.
|
|
180
|
+
*
|
|
181
|
+
* | Type | When |
|
|
182
|
+
* | --- | --- |
|
|
183
|
+
* | `subscription.created` | The subscription exists. |
|
|
184
|
+
* | `subscription.payment_succeeded` | A period was paid for, including the first. |
|
|
185
|
+
* | `subscription.payment_failed` | Charging a period failed; the status is `past_due`. |
|
|
186
|
+
* | `subscription.canceled` | The subscription ended, for any {@link SubscriptionEndReason}. |
|
|
187
|
+
*/
|
|
188
|
+
export type SubscriptionEventType = (typeof SUBSCRIPTION_EVENT_TYPES)[number];
|
|
189
|
+
/**
|
|
190
|
+
* Something that happened to a subscription, normalised.
|
|
191
|
+
*
|
|
192
|
+
* Delivered at least once. De-duplicate on {@link SubscriptionEvent.id}.
|
|
193
|
+
*/
|
|
194
|
+
export interface SubscriptionEvent {
|
|
195
|
+
/** Opaque identifier, stable across redeliveries of the same event. */
|
|
196
|
+
id: string;
|
|
197
|
+
/** Normalised type. */
|
|
198
|
+
type: SubscriptionEventType;
|
|
199
|
+
/** Unix epoch milliseconds. */
|
|
200
|
+
createdAt: number;
|
|
201
|
+
/** The subscription as it was when the event happened. */
|
|
202
|
+
subscription: Subscription;
|
|
203
|
+
/**
|
|
204
|
+
* Opaque payment-interface identifier of the charge behind a
|
|
205
|
+
* `subscription.payment_*` event. Read it with `GetPayment`.
|
|
206
|
+
*
|
|
207
|
+
* Absent from a `subscription.payment_failed` when the provider refused to
|
|
208
|
+
* make the charge at all — the saved method was removed, say — since there is
|
|
209
|
+
* no payment to point at.
|
|
210
|
+
*/
|
|
211
|
+
payment?: string;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Receives subscription events.
|
|
215
|
+
*
|
|
216
|
+
* A rejection, or a throw, means the event was not handled and will be
|
|
217
|
+
* delivered again.
|
|
218
|
+
*/
|
|
219
|
+
export type SubscriptionEventHandler = (event: SubscriptionEvent) => void | Promise<void>;
|
|
220
|
+
/**
|
|
221
|
+
* The metadata key every charge made for a subscription carries, set to the
|
|
222
|
+
* subscription's identifier.
|
|
223
|
+
*
|
|
224
|
+
* Those charges are ordinary payment-interface payments, so their
|
|
225
|
+
* `payment.*` events reach your payment webhook route as well. Use this key to
|
|
226
|
+
* tell them from one-off payments.
|
|
227
|
+
*/
|
|
228
|
+
export declare const SUBSCRIPTION_METADATA_KEY = "antelopeSubscription";
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUBSCRIPTION_METADATA_KEY = exports.SUBSCRIPTION_EVENT_TYPES = exports.CANCELLATION_TIMINGS = exports.SUBSCRIPTION_END_REASONS = exports.SUBSCRIPTION_STATUSES = exports.MAX_PORTABLE_PERIOD_DAYS = exports.SUBSCRIPTION_INTERVALS = void 0;
|
|
4
|
+
exports.isTerminalSubscriptionStatus = isTerminalSubscriptionStatus;
|
|
5
|
+
/**
|
|
6
|
+
* Every value {@link SubscriptionInterval} can take.
|
|
7
|
+
*/
|
|
8
|
+
exports.SUBSCRIPTION_INTERVALS = Object.freeze([
|
|
9
|
+
"day",
|
|
10
|
+
"week",
|
|
11
|
+
"month",
|
|
12
|
+
"year",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* The longest billing period every implementation must accept: one year.
|
|
16
|
+
*
|
|
17
|
+
* Providers cap the period differently. A request for a longer one may be
|
|
18
|
+
* refused with {@link InvalidSubscriptionRequestError}.
|
|
19
|
+
*/
|
|
20
|
+
exports.MAX_PORTABLE_PERIOD_DAYS = 366;
|
|
21
|
+
/**
|
|
22
|
+
* Every value {@link SubscriptionStatus} can take, in lifecycle order.
|
|
23
|
+
*/
|
|
24
|
+
exports.SUBSCRIPTION_STATUSES = Object.freeze([
|
|
25
|
+
"trialing",
|
|
26
|
+
"active",
|
|
27
|
+
"past_due",
|
|
28
|
+
"canceled",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Whether a subscription has ended for good.
|
|
32
|
+
*
|
|
33
|
+
* @param status - The status to classify
|
|
34
|
+
* @returns true for `canceled` only
|
|
35
|
+
*/
|
|
36
|
+
function isTerminalSubscriptionStatus(status) {
|
|
37
|
+
return status === "canceled";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every value {@link SubscriptionEndReason} can take.
|
|
41
|
+
*/
|
|
42
|
+
exports.SUBSCRIPTION_END_REASONS = Object.freeze([
|
|
43
|
+
"requested",
|
|
44
|
+
"payment_failed",
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Every value {@link CancellationTiming} can take.
|
|
48
|
+
*/
|
|
49
|
+
exports.CANCELLATION_TIMINGS = Object.freeze([
|
|
50
|
+
"now",
|
|
51
|
+
"period_end",
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Every value {@link SubscriptionEventType} can take.
|
|
55
|
+
*/
|
|
56
|
+
exports.SUBSCRIPTION_EVENT_TYPES = Object.freeze([
|
|
57
|
+
"subscription.created",
|
|
58
|
+
"subscription.payment_succeeded",
|
|
59
|
+
"subscription.payment_failed",
|
|
60
|
+
"subscription.canceled",
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* The metadata key every charge made for a subscription carries, set to the
|
|
64
|
+
* subscription's identifier.
|
|
65
|
+
*
|
|
66
|
+
* Those charges are ordinary payment-interface payments, so their
|
|
67
|
+
* `payment.*` events reach your payment webhook route as well. Use this key to
|
|
68
|
+
* tell them from one-off payments.
|
|
69
|
+
*/
|
|
70
|
+
exports.SUBSCRIPTION_METADATA_KEY = "antelopeSubscription";
|
|
71
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AA+DA,oEAIC;AAjED;;GAEG;AACU,QAAA,sBAAsB,GAAG,MAAM,CAAC,MAAM,CAAC;IAClD,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;CACE,CAAC,CAAC;AAYZ;;;;;GAKG;AACU,QAAA,wBAAwB,GAAG,GAAG,CAAC;AAE5C;;GAEG;AACU,QAAA,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IACjD,UAAU;IACV,QAAQ;IACR,UAAU;IACV,UAAU;CACF,CAAC,CAAC;AAmBZ;;;;;GAKG;AACH,SAAgB,4BAA4B,CAC1C,MAA0B;IAE1B,OAAO,MAAM,KAAK,UAAU,CAAC;AAC/B,CAAC;AAED;;GAEG;AACU,QAAA,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC;IACpD,WAAW;IACX,gBAAgB;CACR,CAAC,CAAC;AAQZ;;GAEG;AACU,QAAA,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;IAChD,KAAK;IACL,YAAY;CACJ,CAAC,CAAC;AAqHZ;;GAEG;AACU,QAAA,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC;IACpD,sBAAsB;IACtB,gCAAgC;IAChC,6BAA6B;IAC7B,uBAAuB;CACf,CAAC,CAAC;AAiDZ;;;;;;;GAOG;AACU,QAAA,yBAAyB,GAAG,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type SubscriptionRequest } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Checks a {@link SubscriptionRequest} against the portable contract, so every
|
|
4
|
+
* implementation refuses the same request shapes. Consumers can supply `now`
|
|
5
|
+
* to also check that trial and billing dates are in the future.
|
|
6
|
+
*
|
|
7
|
+
* Implementations may omit `now` only when their provider validates future
|
|
8
|
+
* dates after checking for an idempotent replay. This lets a saved result be
|
|
9
|
+
* returned after its original date has passed without accepting a new request
|
|
10
|
+
* with a past date. Finite timestamps and all other rules are always checked.
|
|
11
|
+
*
|
|
12
|
+
* @param request - The request to check
|
|
13
|
+
* @param now - Optional Unix epoch milliseconds for relative date validation
|
|
14
|
+
* @throws InvalidSubscriptionRequestError naming the first rule broken
|
|
15
|
+
*/
|
|
16
|
+
export declare function ValidateSubscriptionRequest(request: SubscriptionRequest, now?: number): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValidateSubscriptionRequest = ValidateSubscriptionRequest;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
function positiveInteger(value) {
|
|
7
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
8
|
+
}
|
|
9
|
+
function validTime(value, now) {
|
|
10
|
+
return (value === undefined ||
|
|
11
|
+
(Number.isFinite(value) && (now === undefined || value > now)));
|
|
12
|
+
}
|
|
13
|
+
const RULES = [
|
|
14
|
+
(request) => request.reference ? undefined : "reference must be a non-empty string",
|
|
15
|
+
(request) => (request.customer ? undefined : "customer is required"),
|
|
16
|
+
(request) => request.paymentMethod ? undefined : "paymentMethod is required",
|
|
17
|
+
(request) => positiveInteger(request.amount?.value)
|
|
18
|
+
? undefined
|
|
19
|
+
: "amount must be a positive integer of minor units",
|
|
20
|
+
(request) => /^[A-Z]{3}$/.test(request.amount?.currency ?? "")
|
|
21
|
+
? undefined
|
|
22
|
+
: "amount.currency must be an uppercase ISO-4217 code",
|
|
23
|
+
(request) => types_1.SUBSCRIPTION_INTERVALS.includes(request.interval)
|
|
24
|
+
? undefined
|
|
25
|
+
: `interval must be one of ${types_1.SUBSCRIPTION_INTERVALS.join(", ")}`,
|
|
26
|
+
(request) => request.intervalCount === undefined ||
|
|
27
|
+
positiveInteger(request.intervalCount)
|
|
28
|
+
? undefined
|
|
29
|
+
: "intervalCount must be a positive integer",
|
|
30
|
+
(request) => request.trialEnd !== undefined && request.billingAnchor !== undefined
|
|
31
|
+
? "trialEnd and billingAnchor are mutually exclusive"
|
|
32
|
+
: undefined,
|
|
33
|
+
(request, now) => validTime(request.trialEnd, now)
|
|
34
|
+
? undefined
|
|
35
|
+
: "trialEnd must be a finite timestamp in the future",
|
|
36
|
+
(request, now) => validTime(request.billingAnchor, now)
|
|
37
|
+
? undefined
|
|
38
|
+
: "billingAnchor must be a finite timestamp in the future",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Checks a {@link SubscriptionRequest} against the portable contract, so every
|
|
42
|
+
* implementation refuses the same request shapes. Consumers can supply `now`
|
|
43
|
+
* to also check that trial and billing dates are in the future.
|
|
44
|
+
*
|
|
45
|
+
* Implementations may omit `now` only when their provider validates future
|
|
46
|
+
* dates after checking for an idempotent replay. This lets a saved result be
|
|
47
|
+
* returned after its original date has passed without accepting a new request
|
|
48
|
+
* with a past date. Finite timestamps and all other rules are always checked.
|
|
49
|
+
*
|
|
50
|
+
* @param request - The request to check
|
|
51
|
+
* @param now - Optional Unix epoch milliseconds for relative date validation
|
|
52
|
+
* @throws InvalidSubscriptionRequestError naming the first rule broken
|
|
53
|
+
*/
|
|
54
|
+
function ValidateSubscriptionRequest(request, now) {
|
|
55
|
+
const problem = RULES.map((rule) => rule(request, now)).find(Boolean);
|
|
56
|
+
if (problem)
|
|
57
|
+
throw new errors_1.InvalidSubscriptionRequestError(problem);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":";;AAmEA,kEAMC;AAzED,qCAA2D;AAC3D,mCAA2E;AAI3E,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,SAAS,CAAC,KAAyB,EAAE,GAAY;IACxD,OAAO,CACL,KAAK,KAAK,SAAS;QACnB,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,GAAG,CAAC,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED,MAAM,KAAK,GAAW;IACpB,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,sCAAsC;IACxE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC;IACpE,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,2BAA2B;IACjE,CAAC,OAAO,EAAE,EAAE,CACV,eAAe,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC;QACpC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,kDAAkD;IACxD,CAAC,OAAO,EAAE,EAAE,CACV,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;QAC/C,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,oDAAoD;IAC1D,CAAC,OAAO,EAAE,EAAE,CACV,8BAAsB,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC;QAC/C,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,2BAA2B,8BAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;IACpE,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,aAAa,KAAK,SAAS;QACnC,eAAe,CAAC,OAAO,CAAC,aAAa,CAAC;QACpC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,0CAA0C;IAChD,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS;QACnE,CAAC,CAAC,mDAAmD;QACrD,CAAC,CAAC,SAAS;IACf,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CACf,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;QAC9B,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,mDAAmD;IACzD,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CACf,SAAS,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;QACnC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,wDAAwD;CAC/D,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAgB,2BAA2B,CACzC,OAA4B,EAC5B,GAAY;IAEZ,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtE,IAAI,OAAO;QAAE,MAAM,IAAI,wCAA+B,CAAC,OAAO,CAAC,CAAC;AAClE,CAAC"}
|