@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,154 @@
1
+ ---
2
+ name: managing-subscriptions
3
+ description: Creates and cancels AntelopeJS recurring billing and synchronizes access from lifecycle events. Use when adding subscriptions, recovering past_due accounts, or implementing a subscription provider.
4
+ category: antelopejs-interface
5
+ tags: [subscriptions, billing, recurring, payment, antelopejs]
6
+ ---
7
+
8
+ # subscriptions interface
9
+
10
+ ## Task workflow
11
+
12
+ 1. Read `README.md` for task paths and compatibility. Recommend Stripe native
13
+ billing with Stripe; select `@antelopejs/payment-subscriptions` only as an
14
+ explicit alternative. Assign one renewal owner per subscription.
15
+ 2. Follow `docs/2.subscriptions.md`: save a method on-session, confirm success
16
+ or authorization, then subscribe using IDs from the same provider account.
17
+ 3. Persist operation keys and subscription/payment IDs. Replay protection lasts
18
+ only for the provider-defined replay window. After expiration, reconcile
19
+ uncertain payments before retrying; the same key can charge again.
20
+ 4. Follow `docs/3.events.md` to deduplicate lifecycle events and synchronize
21
+ access from status, not an assumed retry date.
22
+ 5. For provider work, read `docs/4.implementing.md`, preserve replay-before-date
23
+ validation, run `pnpm test`, then run conformance with real provider fixtures.
24
+ Completion means creation, cancellation and recovery are checked and skipped
25
+ renewal/webhook coverage is reported explicitly.
26
+
27
+ Recurring billing without a billing cycle in your own module. Every call is an
28
+ `InterfaceFunction`, so it is async and queues until an implementation attaches.
29
+ This package declares the contract and contains no billing logic.
30
+
31
+ Implementations: a provider that fires renewals itself implements it natively; for
32
+ any other payment-interface provider, wire `@antelopejs/payment-subscriptions`
33
+ (it needs `@antelopejs/interface-database`, and uses the Redis scheduler when one
34
+ is wired).
35
+
36
+ ## Imports
37
+
38
+ Add `@antelopejs/interface-subscriptions` and `@antelopejs/interface-payment` to
39
+ the module's `dependencies`:
40
+
41
+ ```ts
42
+ import {
43
+ CreateSubscription,
44
+ GetSubscription,
45
+ ListSubscriptions,
46
+ CancelSubscription,
47
+ UpdateSubscriptionPaymentMethod,
48
+ SubscriptionEvents,
49
+ isTerminalSubscriptionStatus,
50
+ SubscriptionError,
51
+ SubscriptionNotFoundError,
52
+ InvalidSubscriptionRequestError,
53
+ InvalidSubscriptionStateError,
54
+ SubscriptionPaymentError,
55
+ SUBSCRIPTION_STATUSES,
56
+ SUBSCRIPTION_EVENT_TYPES,
57
+ SUBSCRIPTION_METADATA_KEY,
58
+ } from "@antelopejs/interface-subscriptions";
59
+ import type {
60
+ Subscription,
61
+ SubscriptionRequest,
62
+ SubscriptionStatus,
63
+ SubscriptionEvent,
64
+ SubscriptionEventHandler,
65
+ CancellationTiming,
66
+ } from "@antelopejs/interface-subscriptions";
67
+ ```
68
+
69
+ ## Five rules that are easy to get wrong
70
+
71
+ 1. **Take the mandate first, on-session.** `CreatePayment` with
72
+ `savePaymentMethod: true` and the payer present, then `ListPaymentMethods`.
73
+ `savePaymentMethod` and `offSession` cannot be combined.
74
+ 2. **A declined first period throws; a declined renewal does not.** Creation
75
+ rejects with `SubscriptionPaymentError` (and creates nothing). A later failure
76
+ is `status: "past_due"` plus a `subscription.payment_failed` event.
77
+ 3. **Decide access from `status`, never from a predicted retry date.** The retry
78
+ policy after `past_due` differs per implementation.
79
+ 4. **Events are at least once.** De-duplicate on `event.id`. Register with an id
80
+ unique within your module; handlers unload with it.
81
+ 5. **Subscription charges also arrive as `payment.*` webhooks.** Skip any whose
82
+ `payment.metadata[SUBSCRIPTION_METADATA_KEY]` is set; the subscription event is
83
+ the one that says what the charge meant.
84
+
85
+ ## Subscribing
86
+
87
+ ```ts
88
+ const signupPayment = await CreatePayment(
89
+ {
90
+ amount,
91
+ reference,
92
+ customer,
93
+ paymentMethod: token,
94
+ savePaymentMethod: true,
95
+ returnUrl,
96
+ },
97
+ `save-${accountId}`,
98
+ );
99
+ if (!["succeeded", "authorized"].includes(signupPayment.status)) {
100
+ // Persist the payment ID and resume after verified settlement or payer action.
101
+ return;
102
+ }
103
+ const [method] = await ListPaymentMethods(customer);
104
+ if (!method) throw new Error("No saved payment method after signup");
105
+
106
+ const subscription = await CreateSubscription(
107
+ {
108
+ reference: accountId,
109
+ customer,
110
+ paymentMethod: method.id,
111
+ amount: { value: 1900, currency: "EUR" },
112
+ interval: "month", // "day" | "week" | "month" | "year"
113
+ intervalCount: 1,
114
+ trialEnd: Date.now() + 14 * 24 * 60 * 60 * 1000, // optional; or billingAnchor, not both
115
+ },
116
+ `subscribe-${accountId}`, // idempotency key: derive it from your record
117
+ );
118
+ ```
119
+
120
+ Periods count from the anchor in UTC calendar units: anchored on 31 January, it
121
+ renews on 28 February, then 31 March.
122
+
123
+ ## Reacting
124
+
125
+ ```ts
126
+ SubscriptionEvents.register("billing", async (event) => {
127
+ // "subscription.created" | "subscription.payment_succeeded"
128
+ // | "subscription.payment_failed" | "subscription.canceled"
129
+ await syncAccess(event.subscription.reference, event.subscription.status);
130
+ });
131
+ ```
132
+
133
+ ## Recovering and ending
134
+
135
+ ```ts
136
+ await UpdateSubscriptionPaymentMethod(id, newMethodId, key); // next attempt uses it; charges nothing now
137
+ await CancelSubscription(id, key, "period_end"); // runs out the paid period
138
+ await CancelSubscription(id, key, "now"); // ends now; refunds are RefundPayment's job
139
+ ```
140
+
141
+ Changing a subscription that has ended raises `InvalidSubscriptionStateError`.
142
+
143
+ ## Not here, by design
144
+
145
+ Proration, dunning schedules, tax, coupons, metered billing, several items per
146
+ subscription, and writing provider settings through `vendorData` (it is a
147
+ read-only echo). For those, use the provider's own interface alongside this one.
148
+
149
+ ## Implementing it
150
+
151
+ `ImplementInterface(await import("@antelopejs/interface-subscriptions"), impl)`
152
+ with the five calls plus `SubscriptionEvents: { register, unregister }`, and list
153
+ the package in `antelopeJs.implements`. The conformance suite in `dist/tests`
154
+ then runs under `ajs module test`; `docs/4.implementing.md` lists its variables.