@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.
@@ -0,0 +1,98 @@
1
+ # Introduction
2
+
3
+ `@antelopejs/interface-subscriptions` defines provider-agnostic recurring
4
+ billing for AntelopeJS applications. A backend written against it bills a saved
5
+ payment method every period and learns what happened — paid, failed, ended —
6
+ without naming a provider, and without running a billing cycle of its own.
7
+
8
+ ## Why a second interface
9
+
10
+ Every call in `@antelopejs/interface-payment` is request and response against
11
+ the provider. A subscription is different in kind: it is a long-lived object
12
+ that must be acted on **when nobody is calling**. It needs two things the
13
+ payment contract never needed:
14
+
15
+ 1. **Durable state** — the subscription, its current period, its status.
16
+ 2. **The passage of time** — something must fire at each renewal.
17
+
18
+ A provider with native subscriptions supplies both. A provider without them
19
+ leaves the merchant to fire each charge, and then something has to supply both
20
+ on the merchant's side. That something is a module with a database and a clock,
21
+ which is a subscriptions module's business and not a payment module's — so the
22
+ contract is its own interface, and:
23
+
24
+ - a payment module whose provider cannot do subscriptions does not implement it,
25
+ rather than implementing it badly;
26
+ - the generic fallback, `@antelopejs/payment-subscriptions`, can depend on a
27
+ database and a scheduler without forcing either on every payment consumer;
28
+ - consumers that do not bill recurrently do not carry the surface.
29
+
30
+ ## Who fires the renewal
31
+
32
+ The question that sorts providers is not "is there a subscription object" but
33
+ "who fires each renewal charge". That is exactly what the fallback has to
34
+ supply.
35
+
36
+ | The provider fires renewals | The merchant fires them |
37
+ | --------------------------- | ----------------------------- |
38
+ | Stripe, PayPal, Mollie | Adyen, Checkout.com, Mangopay |
39
+
40
+ The split is even, so the fallback is half the providers rather than a rare edge.
41
+ [The capability matrix](./5.provider-capability-matrix.md) has the detail.
42
+
43
+ ## The portable core
44
+
45
+ Expressible everywhere, including by the fallback:
46
+
47
+ - an amount, an interval and an interval count;
48
+ - a billing anchor (when the first paid period starts) or a trial end;
49
+ - a status, including `past_due`;
50
+ - cancelling now, or at the end of the period paid for;
51
+ - the current period's start and end.
52
+
53
+ Deliberately not modelled, because the fallback cannot honour them and a
54
+ contract only some implementations keep is a lie: proration, dunning schedules,
55
+ tax, coupons and discounts, metered billing, and more than one item per
56
+ subscription. A backend that needs one of these uses the provider's own
57
+ interface (`@antelopejs/interface-stripe`) alongside this one.
58
+
59
+ ## Built on the payment interface
60
+
61
+ A subscription charges a method saved through `@antelopejs/interface-payment`,
62
+ off-session. That pair — one on-session payment that saves a method, then any
63
+ number of off-session charges against it — is the payment interface's portable
64
+ recurring primitive, and this interface adds nothing underneath it.
65
+
66
+ Identifiers cross the two interfaces unchanged: a subscription's `customer`,
67
+ `paymentMethod` and `latestPayment` are payment-interface identifiers from the
68
+ same account, and `provider` selects that account in both.
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pnpm add @antelopejs/interface-subscriptions
74
+ ```
75
+
76
+ Wire a module that implements it alongside a payment module.
77
+
78
+ ## Conventions
79
+
80
+ **Amounts are the payment interface's `Amount`**: integer minor units and an
81
+ uppercase ISO-4217 code.
82
+
83
+ **Times are Unix epoch milliseconds**, and periods are counted in UTC.
84
+
85
+ **Every mutating call takes an idempotency key**, positionally. Replaying it
86
+ returns the original outcome within the provider-defined replay window,
87
+ including rejections where the provider retains them. After expiration,
88
+ applications must reconcile uncertain payments before retrying; replay
89
+ protection is not indefinite.
90
+
91
+ **A failed renewal is a status, not an exception.** The subscription goes
92
+ `past_due` and a `subscription.payment_failed` event is raised. Only a declined
93
+ _first_ period rejects, because then there is no subscription to put in a state.
94
+
95
+ **`vendorData` is a read-only echo.** An implementation may surface what its
96
+ provider said; no request accepts provider-specific settings, because a
97
+ `Record<string, unknown>` that compiles against every provider and works on one
98
+ hides exactly the coupling this interface exists to prevent.
@@ -0,0 +1,147 @@
1
+ # Subscriptions
2
+
3
+ ## Creating one
4
+
5
+ The payer is not present when a subscription charges, so the mandate is taken
6
+ first, through the payment interface. Create an on-session payment with
7
+ `savePaymentMethod: true`, `customer` and `returnUrl`, and persist its ID.
8
+ If it returns `requires_action`, send the payer to `redirectUrl`. Do not list
9
+ saved methods or start the subscription while the payment is still pending or
10
+ requires action, or if it failed or was canceled.
11
+
12
+ After a verified `payment.succeeded` or `payment.authorized` event, or a server
13
+ read with `GetPayment` reporting one of those states, use `ListPaymentMethods`
14
+ to find the saved method matching that payment's `paymentMethod`. Do not select
15
+ an unrelated method just because it appears first. The [README quick start](../README.md#quick-start)
16
+ shows the checkout and reconciliation steps separately.
17
+
18
+ `savePaymentMethod` cannot be combined with `offSession` — a mandate can only be
19
+ taken from a payer who is there. That first payment is an ordinary one-off; the
20
+ subscription starts after it, and its first period is a separate charge.
21
+
22
+ ```ts
23
+ const subscription = await CreateSubscription(
24
+ {
25
+ reference: accountId,
26
+ customer,
27
+ paymentMethod: method.id,
28
+ amount: { value: 1900, currency: "EUR" },
29
+ interval: "month",
30
+ intervalCount: 1,
31
+ },
32
+ `subscribe-${accountId}`,
33
+ );
34
+ ```
35
+
36
+ Without a trial or a billing anchor, the first period is charged off-session
37
+ before `CreateSubscription` resolves:
38
+
39
+ | First charge | Result |
40
+ | ------------------------ | --------------------------------------------------------------------------------------------- |
41
+ | Succeeded | An `active` subscription |
42
+ | Still settling (a debit) | An `active` subscription; if the charge later fails it goes `past_due` |
43
+ | Declined | Rejects with `SubscriptionPaymentError`, carrying the declined `Payment`. Nothing is created. |
44
+
45
+ Replaying the idempotency key within the provider-defined replay window returns
46
+ the saved outcome, including a rejection where retained by the provider.
47
+ After expiration, applications must reconcile uncertain payments and subscription
48
+ creation before retrying; the same key can create another charge.
49
+
50
+ ## Periods
51
+
52
+ A period is `intervalCount` × `interval`, where the interval is `day`, `week`,
53
+ `month` or `year`. Every implementation accepts periods up to a year; a longer
54
+ one may be refused with `InvalidSubscriptionRequestError`.
55
+
56
+ Periods are counted from the billing anchor in calendar units, in UTC, and never
57
+ from the end of the previous period. A monthly subscription anchored on 31
58
+ January renews on 28 February (29 in a leap year), then on 31 March — not on the
59
+ 28th of every month from then on.
60
+
61
+ `currentPeriodStart` and `currentPeriodEnd` describe the period most recently
62
+ billed for. The next charge is made at `currentPeriodEnd`.
63
+
64
+ ## Trials and billing anchors
65
+
66
+ Both defer the first charge, and they are mutually exclusive.
67
+
68
+ - **`trialEnd`**: the subscription is `trialing` until then, nothing is charged,
69
+ and the first paid period starts at the trial end.
70
+ - **`billingAnchor`**: the subscription is `active` until then — it exists and is
71
+ in good standing — but nothing is charged until the anchor, where the first paid
72
+ period starts.
73
+
74
+ In both cases the period before the first charge runs from creation to that
75
+ time, so `currentPeriodEnd` is the trial end or the anchor.
76
+
77
+ ## Statuses
78
+
79
+ | Status | Meaning |
80
+ | ---------- | ---------------------------------------------------------------------------------------------- |
81
+ | `trialing` | Before `trialEnd`. Nothing charged. |
82
+ | `active` | The current period is paid for, or not yet due. |
83
+ | `past_due` | Charging the current period failed. The implementation is retrying under its own policy. |
84
+ | `canceled` | Ended. Terminal. `endedAt` and `endReason` (`requested` or `payment_failed`) say when and why. |
85
+
86
+ When a renewal fails, the period still advances: the new period is the current
87
+ one, and `past_due` says it is unpaid. When a retry succeeds the subscription is
88
+ `active` again in the same period; the cycle does not move.
89
+
90
+ ### `past_due` is portable to report, not to predict
91
+
92
+ What happens after a failure is the implementation's: a native provider follows
93
+ the retry schedule configured in its dashboard, and the generic fallback follows
94
+ its documented policy (three retries, one, three and seven days apart, then the
95
+ subscription ends with `endReason: "payment_failed"`). Two subscriptions in the
96
+ same state may therefore behave differently on different providers. Decide access
97
+ from the status; do not schedule anything on the assumption of a retry date.
98
+
99
+ ## Recovering a `past_due` subscription
100
+
101
+ If the saved method will not work again — an expired card — take a new mandate
102
+ on-session and switch to it:
103
+
104
+ ```ts
105
+ await UpdateSubscriptionPaymentMethod(
106
+ subscription.id,
107
+ newMethodId,
108
+ idempotencyKey,
109
+ );
110
+ ```
111
+
112
+ Nothing is charged by the switch. The next scheduled attempt uses the new method.
113
+
114
+ ## Cancelling
115
+
116
+ ```ts
117
+ await CancelSubscription(id, `cancel-period-end-${id}`, "period_end"); // run out the paid period
118
+ await CancelSubscription(id, `cancel-now-${id}`, "now"); // end immediately
119
+ ```
120
+
121
+ Use distinct keys for distinct operations. Reuse a key only when retrying the
122
+ same request; reusing the period-end key for immediate cancellation replays the
123
+ first outcome instead of applying the change.
124
+
125
+ - `period_end` sets `cancelAtPeriodEnd`. The status does not change until the
126
+ period ends, at which point the subscription ends instead of renewing. Asking
127
+ again is not an error.
128
+ - `now` ends it at once. Nothing is refunded; refunds are `RefundPayment` on the
129
+ payment interface. A subscription set to end at period end can still be ended
130
+ now.
131
+ - Either on a subscription that has already ended raises
132
+ `InvalidSubscriptionStateError`, unless the idempotency key is a replay.
133
+
134
+ ## Errors
135
+
136
+ | Error | When |
137
+ | --------------------------------- | ---------------------------------------------------------------------------- |
138
+ | `SubscriptionNotFoundError` | No subscription has that identifier under that `provider` |
139
+ | `InvalidSubscriptionRequestError` | A request outside the contract: a non-positive amount, a past trial end, ... |
140
+ | `InvalidSubscriptionStateError` | A change to a subscription that has ended |
141
+ | `SubscriptionPaymentError` | The first period was charged and declined; nothing was created |
142
+ | `SubscriptionError` | The base class; also the call could not be made at all |
143
+
144
+ Each carries `providerCode` where the provider gave one.
145
+ `InvalidSubscriptionStateError.status` is an optional `SubscriptionStatus`
146
+ describing the actual state that blocked the operation. Its constructor is
147
+ `new InvalidSubscriptionStateError(message, status?, providerCode?)`.
@@ -0,0 +1,79 @@
1
+ # Events
2
+
3
+ A subscription changes while nobody is calling, so a backend learns of renewals,
4
+ failures and endings from events.
5
+
6
+ ## The event set
7
+
8
+ | Type | When |
9
+ | -------------------------------- | ---------------------------------------------------------------------------- |
10
+ | `subscription.created` | The subscription exists |
11
+ | `subscription.payment_succeeded` | A period was paid for, including the first |
12
+ | `subscription.payment_failed` | Charging a period failed; the status is `past_due`, or the retries are spent |
13
+ | `subscription.canceled` | It ended, for any reason |
14
+
15
+ An event carries the subscription as it was when the event happened, and, for
16
+ the two payment events, the payment-interface identifier of the charge in
17
+ `payment`. Read the charge with `GetPayment` for the amount or the decline
18
+ reason. A `subscription.payment_failed` has no `payment` when the provider
19
+ refused to make the charge at all — the saved method had been removed, say.
20
+
21
+ Changing the payment method and scheduling a cancellation at period end raise no
22
+ event: the caller made those changes and already knows.
23
+
24
+ ## Receiving them
25
+
26
+ ```ts
27
+ import { SubscriptionEvents } from "@antelopejs/interface-subscriptions";
28
+
29
+ SubscriptionEvents.register("billing", async (event) => {
30
+ if (await alreadyHandled(event.id)) return;
31
+ await applyToAccount(event);
32
+ });
33
+ ```
34
+
35
+ `SubscriptionEvents` is a registering proxy, not a webhook route. Handlers are
36
+ registered under an identifier unique within your module, and unregistered
37
+ automatically when your module unloads.
38
+
39
+ Register handlers when your module starts. An event is delivered to the
40
+ handlers registered at the time; one raised while none are registered reaches
41
+ nobody and is not kept.
42
+
43
+ Delivery is **at least once**:
44
+
45
+ - A handler that throws or rejects has not handled the event, and it is delivered
46
+ again later — to every handler, so de-duplicate on `event.id`.
47
+ - A crash between a change and its delivery means the event is delivered when the
48
+ implementation next looks at that subscription.
49
+ - In a cluster, each event is delivered by one instance, to the handlers
50
+ registered in that instance.
51
+
52
+ Events may arrive late, and should not be read as more recent than state you
53
+ already hold. `GetSubscription` is always current.
54
+
55
+ ## How implementations learn of them
56
+
57
+ That is each implementation's business, and invisible to a consumer:
58
+
59
+ - the generic fallback raises them from its own renewal clock, recording each in
60
+ its database with the change that caused it;
61
+ - a native provider raises them from the provider's webhooks.
62
+
63
+ ## Relation to payment events
64
+
65
+ Every subscription charge is an ordinary payment-interface payment, so its
66
+ `payment.*` events reach your payment webhook route as well. They carry
67
+ `SUBSCRIPTION_METADATA_KEY` (`antelopeSubscription`) in their metadata, set to
68
+ the subscription's identifier:
69
+
70
+ ```ts
71
+ const event = await VerifyWebhook(request);
72
+ if (event?.payment?.metadata?.[SUBSCRIPTION_METADATA_KEY]) {
73
+ return; // a subscription charge: handled by the subscription event
74
+ }
75
+ ```
76
+
77
+ Handle a subscription charge in one place. The subscription event is the one
78
+ that says what the charge meant — which period it paid for, whether the
79
+ subscription is now `past_due`.
@@ -0,0 +1,158 @@
1
+ # Implementing the interface
2
+
3
+ There are two kinds of implementation, and both are expected from the start.
4
+
5
+ ## Native: the provider fires renewals
6
+
7
+ A payment module whose provider has first-class subscriptions (Stripe, PayPal,
8
+ Mollie) implements this interface beside `@antelopejs/interface-payment`, in the
9
+ same module, mapping each call onto the provider's subscription object. It owns
10
+ no clock and no database.
11
+
12
+ What it must do that the provider will not do for it:
13
+
14
+ - **Validate with `ValidateSubscriptionRequest`** before anything reaches the
15
+ provider, so every implementation enforces the portable request shape. Defer
16
+ relative date validation to a provider that checks its idempotency cache
17
+ first; a replay must not fail because its original date has passed. An
18
+ unknown customer or payment method is an `InvalidSubscriptionRequestError`
19
+ too, never a `SubscriptionNotFoundError`, which is reserved for the
20
+ subscription itself.
21
+ - **Report only its own subscriptions.** A provider account may hold
22
+ subscriptions made elsewhere — a hosted checkout, the dashboard. They were not
23
+ created through this interface, have no `reference`, and must not be listed,
24
+ read or raised as events.
25
+ - **Charge the first period at creation, or refuse.** A declined first charge
26
+ rejects with `SubscriptionPaymentError` and leaves nothing behind. On Stripe
27
+ this is `payment_behavior: "error_if_incomplete"`.
28
+ - **Count periods from the anchor**, in UTC calendar units. Providers that bill
29
+ from an anchor already do.
30
+ - **Map the provider's statuses onto four.** Anything that means "a charge
31
+ failed and the provider is retrying" is `past_due`; anything that has ended or
32
+ will never charge again is `canceled`. Where the provider can hold a
33
+ subscription that will never charge again without ending it — Stripe's
34
+ `unpaid` — say so, and tell the merchant to configure it to cancel instead.
35
+ - **Raise the four events** from the provider's webhooks. A module that already
36
+ normalises webhooks for the payment interface can raise subscription events from
37
+ the same deliveries.
38
+ - **Mark charges** with `SUBSCRIPTION_METADATA_KEY` when it normalises the
39
+ underlying payments, so a consumer can tell them from one-off payments.
40
+ - **Echo, never accept, `vendorData`.**
41
+
42
+ ## Fallback: the merchant fires renewals
43
+
44
+ For every provider that leaves each charge to the merchant, wire
45
+ `@antelopejs/payment-subscriptions`. It implements this interface on top of any
46
+ payment-interface provider, keeps every subscription in
47
+ `@antelopejs/interface-database`, and fires renewals from its own clock — the
48
+ Redis scheduler when one is wired, polling the database when not.
49
+
50
+ A payment module should not implement this interface by embedding a clock of its
51
+ own: that would put a mandatory database and scheduler into a payment module.
52
+
53
+ ## Wiring an implementation
54
+
55
+ The calls are declared directly as the public names, so the whole module is the
56
+ declaration:
57
+
58
+ ```ts
59
+ import * as subscriptions from "@antelopejs/interface-subscriptions";
60
+ import { ImplementInterface } from "@antelopejs/interface-core";
61
+
62
+ export function construct(): void {
63
+ ImplementInterface(subscriptions, {
64
+ CreateSubscription,
65
+ GetSubscription,
66
+ ListSubscriptions,
67
+ CancelSubscription,
68
+ UpdateSubscriptionPaymentMethod,
69
+ SubscriptionEvents: { register, unregister },
70
+ });
71
+ }
72
+ ```
73
+
74
+ `SubscriptionEvents` is a registering proxy: its implementation is a `register`
75
+ and `unregister` pair that keep the handlers your module delivers to.
76
+
77
+ List the interface in `antelopeJs.implements`, and the payment interface in
78
+ `dependencies`.
79
+
80
+ ## `ValidateSubscriptionRequest`
81
+
82
+ `ValidateSubscriptionRequest(request: SubscriptionRequest, now?: number): void`
83
+ checks the portable request rules synchronously. Supply `now` to also check
84
+ that trial and billing dates are in the future. It is Unix epoch
85
+ **milliseconds**, such as `Date.now()`, not seconds. Pass your injected clock
86
+ in tests or in an implementation that controls time.
87
+
88
+ ```ts docs-check
89
+ import {
90
+ ValidateSubscriptionRequest,
91
+ type SubscriptionRequest,
92
+ } from "@antelopejs/interface-subscriptions";
93
+
94
+ export function validateBeforeCreation(request: SubscriptionRequest): void {
95
+ ValidateSubscriptionRequest(request, Date.now());
96
+ }
97
+ ```
98
+
99
+ The function returns `undefined` on success and throws
100
+ `InvalidSubscriptionRequestError` with a message naming the first failed rule:
101
+
102
+ - `reference`, `customer` and `paymentMethod` must be non-empty.
103
+ - `amount.value` must be a positive integer of minor units; `amount.currency`
104
+ must contain exactly three uppercase letters. The helper checks the format,
105
+ not membership in an ISO currency registry.
106
+ - `interval` must be `day`, `week`, `month` or `year`; an explicit
107
+ `intervalCount` must be a positive integer.
108
+ - `trialEnd` and `billingAnchor` are mutually exclusive. Each, if supplied,
109
+ must be finite. When `now` is supplied, each must also be strictly greater
110
+ than `now` (equality is invalid).
111
+
112
+ An implementation may omit `now` only when its provider validates future dates
113
+ after resolving idempotent replays. Stripe uses this form: a retry with the
114
+ original key and parameters can return the saved subscription after its trial
115
+ or billing anchor has passed. A new request with a past date still fails at
116
+ the provider. Implementations that own their idempotency storage must resolve
117
+ replays before applying relative date validation themselves.
118
+
119
+ This helper does not contact a provider, verify customer or saved-method
120
+ existence, or enforce provider-specific maximum periods. Implementations must
121
+ perform those checks separately and also report invalid requests with
122
+ `InvalidSubscriptionRequestError`.
123
+
124
+ ## The conformance suite
125
+
126
+ `dist/tests/*.test.js` ships in the package. `ajs module test` runs it
127
+ automatically against any module that lists this interface in
128
+ `antelopeJs.implements`.
129
+
130
+ It is weaker than the payment interface's suite, and the reason matters before
131
+ anyone reads a green run as a promise: **a test cannot wait a month.** Stripe has
132
+ test clocks, but Mollie and PayPal do not, so a portable suite cannot pin a
133
+ renewal, a trial ending, a failed retry or a period end. What it covers:
134
+
135
+ - creating a subscription that charges at once, a trial, a billing anchor;
136
+ - the first charge's shape and its metadata marker;
137
+ - idempotent replay of creation and cancellation;
138
+ - reading back, listing newest first and per customer;
139
+ - cancelling now and at period end, and refusing to change one that has ended;
140
+ - switching the payment method;
141
+ - unknown identifiers and every invalid request;
142
+ - a declined first period, when the provider can supply a method that saves and
143
+ then declines;
144
+ - events, when they can reach the test process.
145
+
146
+ Everything time-driven is pinned instead by the generic fallback's own suite,
147
+ which drives an injected clock. That suite is the executable specification a
148
+ native implementation's renewal behaviour is checked against by eye.
149
+
150
+ | Variable | Purpose |
151
+ | --------------------------------------------------- | --------------------------------------------------------------------------- |
152
+ | `ANTELOPE_PAYMENT_TEST_METHOD` | A token that saves on-session and charges off-session. |
153
+ | `ANTELOPE_SUBSCRIPTIONS_TEST_METHOD_DECLINES_LATER` | A token that saves on-session and is declined off-session. |
154
+ | `ANTELOPE_SUBSCRIPTIONS_TEST_EVENTS` | Set when the implementation's events reach the test process during the run. |
155
+ | `ANTELOPE_PAYMENT_TEST_CURRENCY` | ISO-4217 code to run against. Defaults to `EUR`. |
156
+
157
+ With nothing implementing the interface, the calls queue and the suite times
158
+ out; it cannot report green by accident.
@@ -0,0 +1,61 @@
1
+ # Provider capability matrix
2
+
3
+ Which providers fire renewals themselves decides which implementation a
4
+ deployment needs. Checked against each provider's documentation on 2026-09-18;
5
+ Stripe and PayPal were not re-checked then, both having first-class subscription
6
+ objects that bill on their own schedule.
7
+
8
+ | Provider | Who fires renewals | Implementation |
9
+ | ------------ | ------------------ | ------------------------------------------------------------- |
10
+ | Stripe | Provider | Native, in `@antelopejs/stripe` |
11
+ | PayPal | Provider | Native |
12
+ | Mollie | Provider | Native |
13
+ | Adyen | Merchant | `@antelopejs/payment-subscriptions` |
14
+ | Checkout.com | Merchant | `@antelopejs/payment-subscriptions` |
15
+ | Mangopay | Merchant | `@antelopejs/payment-subscriptions`, with a provider schedule |
16
+
17
+ - **Mollie**: _"When using this API, we automatically create recurring payments
18
+ for you at the interval you specified."_
19
+ - **Adyen**: subscriptions are _"recurring transactions you initiate at regular
20
+ intervals"_, against a stored token and `shopperReference`.
21
+ - **Checkout.com**: no subscription or billing-plan resource at all. Recurring
22
+ is a stored instrument plus `payment_type: Recurring` and
23
+ `merchant_initiated: true` on an ordinary payment.
24
+ - **Mangopay**: the Recurring PayIn Registration stores frequency, dates and
25
+ amounts, and handles the authentication chain between charges, but _"the
26
+ platform can initiate payments"_ — Mangopay does not fire them. The fallback
27
+ owns the clock and can record the schedule with Mangopay through its provider
28
+ schedule seam.
29
+
30
+ Implemented: the generic fallback, and Stripe natively in `@antelopejs/stripe`,
31
+ which passes the conformance suite against live Stripe and is further tested on
32
+ Stripe test clocks — renewal, trial end, `past_due` and cancellation at period
33
+ end — which the portable suite cannot pin. PayPal and Mollie are plans.
34
+
35
+ Times are kept at each provider's precision, which the contract only promises
36
+ to the second: Stripe stores seconds. Mollie's subscription `startDate` is a
37
+ calendar date, so on Mollie a trial end or billing anchor lands on a day rather
38
+ than an instant — a native Mollie implementation has to say so, or charge the
39
+ first period itself. Unverified against Mollie.
40
+
41
+ ## Where native and fallback differ, and the contract allows it
42
+
43
+ | Behaviour | Native | Fallback |
44
+ | ----------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
45
+ | What happens after `past_due` | The provider's configured retry schedule | Three retries, one, three and seven days apart, then ends |
46
+ | When a renewal is charged | The provider's scheduler, near the time | At the time with Redis wired; within one poll otherwise |
47
+ | `vendorData` | What the provider said | Absent |
48
+ | Events | From the provider's webhooks | From its own clock, recorded with each change |
49
+ | An event's subscription | Stripe invoice events: read back at delivery, so a redelivery may show a later state | As it was when the event happened |
50
+
51
+ ## Out of scope, and why
52
+
53
+ | Feature | Why it is out |
54
+ | ------------------------------ | ------------------------------------------------------------------------------------ |
55
+ | Proration | Arithmetic every provider does differently, and the fallback would have to invent it |
56
+ | Dunning schedules | Native providers configure retries in their dashboard; only the status is portable |
57
+ | Tax | A tax engine is a product of its own |
58
+ | Coupons and discounts | Provider objects with provider semantics |
59
+ | Metered and usage billing | Needs usage reporting the fallback has no source for |
60
+ | Several items per subscription | Needs proration to change any one of them |
61
+ | Write-through `vendorData` | Compiles everywhere, works on one provider: invisible coupling |
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "composite": false,
6
+ "incremental": false,
7
+ "rootDir": "..",
8
+ "baseUrl": "..",
9
+ "paths": {
10
+ "@antelopejs/interface-subscriptions": ["src/index.ts"]
11
+ }
12
+ },
13
+ "include": ["../src", "./**/*.ts"]
14
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-subscriptions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.2",
4
+ "description": "Provider-agnostic AntelopeJS subscriptions interface: recurring billing of a saved payment method, with trials, cancellation and normalised lifecycle events",
5
+ "keywords": [
6
+ "antelopejs",
7
+ "billing",
8
+ "interface",
9
+ "recurring",
10
+ "subscription"
11
+ ],
12
+ "homepage": "https://github.com/AntelopeJS/interface-subscriptions/tree/main/docs",
13
+ "bugs": {
14
+ "url": "https://github.com/AntelopeJS/interface-subscriptions/issues"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/AntelopeJS/interface-subscriptions.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "docs",
24
+ "skills"
25
+ ],
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./tests/*": {
34
+ "types": "./dist/tests/*.d.ts",
35
+ "default": "./dist/tests/*.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "scripts": {
44
+ "build": "rimraf dist && tsc",
45
+ "test": "pnpm build && ajs module test .",
46
+ "format": "oxfmt .",
47
+ "lint:fix": "oxlint --fix",
48
+ "lint": "oxlint",
49
+ "prepack": "pnpm run build",
50
+ "release": "pnpm run lint && pnpm run prepack && release-it",
51
+ "format:check": "oxfmt --check .",
52
+ "knip": "knip"
53
+ },
54
+ "devDependencies": {
55
+ "@antelopejs/core": "1.16.0",
56
+ "@antelopejs/interface-core": ">=0.0.5 <1.0.0",
57
+ "@antelopejs/interface-payment": ">=0.0.2 <1.0.0",
58
+ "@antelopejs/tooling-configs": "^0.0.8",
59
+ "@types/mocha": "^10.0.10",
60
+ "@types/node": "^22.19.15",
61
+ "eslint-plugin-perfectionist": "^5.11.0",
62
+ "knip": "^6.34.0",
63
+ "oxfmt": "^0.66.0",
64
+ "oxlint": "1.81.0",
65
+ "oxlint-tsgolint": "^7.0.2001",
66
+ "release-it": "^19.2.4",
67
+ "release-it-changelogen": "^0.1.0",
68
+ "rimraf": "^6.1.3",
69
+ "typescript": "^5.9.3"
70
+ },
71
+ "peerDependencies": {
72
+ "@antelopejs/interface-core": ">=0.0.5 <1.0.0",
73
+ "@antelopejs/interface-payment": ">=0.0.2 <1.0.0"
74
+ },
75
+ "packageManager": "pnpm@10.6.5",
76
+ "antelopeJs": {
77
+ "test": "src/antelope.test.ts",
78
+ "skills": [
79
+ "./skills"
80
+ ]
81
+ }
82
+ }