@mesub/node 0.0.0-stage → 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +840 -2
  3. package/dist/chunk-43Q4PNF7.cjs +480 -0
  4. package/dist/chunk-43Q4PNF7.cjs.map +1 -0
  5. package/dist/chunk-4AAPR37Y.js +120 -0
  6. package/dist/chunk-4AAPR37Y.js.map +1 -0
  7. package/dist/chunk-4JSJOX35.cjs +2040 -0
  8. package/dist/chunk-4JSJOX35.cjs.map +1 -0
  9. package/dist/chunk-66VVEXMU.cjs +340 -0
  10. package/dist/chunk-66VVEXMU.cjs.map +1 -0
  11. package/dist/chunk-J2CE4QWV.js +340 -0
  12. package/dist/chunk-J2CE4QWV.js.map +1 -0
  13. package/dist/chunk-JJWHMB2X.js +480 -0
  14. package/dist/chunk-JJWHMB2X.js.map +1 -0
  15. package/dist/chunk-NOS4FWWQ.js +2040 -0
  16. package/dist/chunk-NOS4FWWQ.js.map +1 -0
  17. package/dist/chunk-SJRQDWTI.cjs +120 -0
  18. package/dist/chunk-SJRQDWTI.cjs.map +1 -0
  19. package/dist/client-BEuDegEY.d.cts +765 -0
  20. package/dist/client-CXQLkSAJ.d.ts +765 -0
  21. package/dist/errors-BN4ZWUfM.d.ts +112 -0
  22. package/dist/errors-DcLfunS-.d.cts +112 -0
  23. package/dist/express.cjs +129 -0
  24. package/dist/express.cjs.map +1 -0
  25. package/dist/express.d.cts +57 -0
  26. package/dist/express.d.ts +57 -0
  27. package/dist/express.js +129 -0
  28. package/dist/express.js.map +1 -0
  29. package/dist/guard-CaXiHMVF.d.ts +51 -0
  30. package/dist/guard-D4AB5HtN.d.cts +51 -0
  31. package/dist/index.cjs +39 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +44 -0
  34. package/dist/index.d.ts +44 -0
  35. package/dist/index.js +39 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/nest.cjs +62 -0
  38. package/dist/nest.cjs.map +1 -0
  39. package/dist/nest.d.cts +57 -0
  40. package/dist/nest.d.ts +57 -0
  41. package/dist/nest.js +62 -0
  42. package/dist/nest.js.map +1 -0
  43. package/dist/next.cjs +113 -0
  44. package/dist/next.cjs.map +1 -0
  45. package/dist/next.d.cts +63 -0
  46. package/dist/next.d.ts +63 -0
  47. package/dist/next.js +113 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/routes-B7wN4sby.d.ts +130 -0
  50. package/dist/routes-CMzB52P6.d.cts +130 -0
  51. package/dist/situations.cjs +15 -0
  52. package/dist/situations.cjs.map +1 -0
  53. package/dist/situations.d.cts +607 -0
  54. package/dist/situations.d.ts +607 -0
  55. package/dist/situations.js +15 -0
  56. package/dist/situations.js.map +1 -0
  57. package/dist/subscriptions-B6Lin6i_.d.cts +775 -0
  58. package/dist/subscriptions-B6Lin6i_.d.ts +775 -0
  59. package/dist/testing.cjs +1488 -0
  60. package/dist/testing.cjs.map +1 -0
  61. package/dist/testing.d.cts +331 -0
  62. package/dist/testing.d.ts +331 -0
  63. package/dist/testing.js +1488 -0
  64. package/dist/testing.js.map +1 -0
  65. package/package.json +158 -4
@@ -0,0 +1,480 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/situations.ts
2
+ var SUBSCRIBE = {
3
+ by: "subscriber",
4
+ action: "subscribe",
5
+ sdk: "subscriptions.create",
6
+ route: "POST /v1/subscriptions"
7
+ };
8
+ var CANCEL = {
9
+ by: "subscriber",
10
+ action: "cancel",
11
+ sdk: "subscriptions.cancel",
12
+ route: "POST /v1/subscriptions/:id/cancel"
13
+ };
14
+ var RESUME = {
15
+ by: "subscriber",
16
+ action: "resume",
17
+ sdk: "subscriptions.resume",
18
+ route: "POST /v1/subscriptions/:id/resume",
19
+ until: "access_until"
20
+ };
21
+ var CLOSE = {
22
+ by: "subscriber",
23
+ action: "close",
24
+ sdk: "subscriptions.close",
25
+ route: "POST /v1/subscriptions/:id/close",
26
+ confirm: "confirm_close",
27
+ when: "closable"
28
+ };
29
+ var CLOSE_OVER = { ...CLOSE, confirm: "confirm_close_over" };
30
+ var ADD_FUNDS = { by: "subscriber", action: "add_funds" };
31
+ var RETRY = {
32
+ by: "merchant",
33
+ action: "retry",
34
+ route: "POST /merchant/subscriptions/:id/retry",
35
+ dashboard: true
36
+ };
37
+ var PAY_NOW = {
38
+ by: "subscriber",
39
+ action: "retry",
40
+ sdk: "subscriptions.retry",
41
+ route: "POST /v1/subscriptions/:id/retry"
42
+ };
43
+ var UPGRADE = {
44
+ by: "merchant",
45
+ action: "upgrade",
46
+ route: "POST /projects/:id/upgrade",
47
+ dashboard: true
48
+ };
49
+ var CANCEL_PAID = { ...CANCEL, confirm: "confirm_cancel_paid" };
50
+ var BLOCKED_BY_SETUP = "Mesub cannot take your payments for {plan_name} for now, because of how the plan is set up, not because of anything you did: there is nothing for you to do or sign, and Mesub keeps trying.[ You keep access until {access_until} if no payment goes through by then.] Access comes back on its own with the first payment that goes through.";
51
+ var BLOCKED_ACCESS = "[ Access ends on {access_until} if nothing is collected by then.] Access comes back with the first payment that goes through, with nothing for the subscriber to sign.";
52
+ var NEVER_BACK = "No payment can go through on this plan again, so once access ends it does not come back.";
53
+ var CANCEL_BEHIND = { ...CANCEL, confirm: "confirm_cancel_behind" };
54
+ var SITUATIONS = {
55
+ // Running
56
+ none: {
57
+ subscriber: "You are not subscribed to {plan_name}.",
58
+ merchant: "This customer has no subscription to {plan_name}.",
59
+ actions: [SUBSCRIBE]
60
+ },
61
+ active: {
62
+ subscriber: "Active. Your next payment of {amount} is on {next_charge_at}.",
63
+ merchant: "Active and paid up. Next payment of {amount} on {next_charge_at}.",
64
+ actions: [CANCEL_PAID]
65
+ },
66
+ // The plan ends before another charge: the last period, paid in full.
67
+ active_last_period: {
68
+ subscriber: "Active until {access_until}: {plan_name} is ending, and no further payment will be taken.",
69
+ merchant: "Active and paid up, in its last period: the plan is ending, so no further payment is taken. Access ends on {access_until}.",
70
+ actions: [CANCEL_PAID]
71
+ },
72
+ active_unbilled: {
73
+ subscriber: "Active until {access_until}. No further payment is scheduled.",
74
+ merchant: "Not billed: this subscription's terms were never signed through Mesub, so Mesub never takes a payment for it. Access ends on {access_until}.",
75
+ actions: []
76
+ },
77
+ // Late (`unpaid`)
78
+ // The plan ends before the next retry: none is scheduled, whatever the reason.
79
+ unpaid_last_period: {
80
+ subscriber: "Your payment of {amount} is late, and Mesub will not try again on its own: {plan_name} is ending. You keep access until {access_until}.",
81
+ merchant: "Payment late, with no retry ahead: the plan ends before the next one. Access continues until {access_until}, and the subscription ends with the plan.",
82
+ actions: [PAY_NOW, CANCEL_BEHIND, RETRY]
83
+ },
84
+ unpaid_insufficient_balance_retries: {
85
+ subscriber: "Your payment of {amount} is late because your wallet held too little, and nothing was taken. Add {amount} to your wallet, then pay it now, with nothing to sign; otherwise Mesub tries again on {next_retry_at}. Your subscription stops if no payment goes through by {access_until}.",
86
+ merchant: "Payment late: the wallet was short. Retry[ {next_retry_number} of {retries_allowed}] on {next_retry_at}, or sooner by hand, yours or the subscriber's. Access continues until {access_until}; the subscription stops then if every retry fails.",
87
+ actions: [ADD_FUNDS, PAY_NOW, CANCEL_BEHIND, RETRY]
88
+ },
89
+ unpaid_insufficient_balance_free: {
90
+ subscriber: "Your payment of {amount} is late because your wallet held too little, so your access is off. Add {amount} to your wallet, then pay it now, with nothing to sign, before {retry_deadline}; if no payment goes through by then, the subscription stops.",
91
+ merchant: "Payment late: the wallet was short. On Free, Mesub does not retry on its own: you or the subscriber can retry by hand, up to 3 times between you, 10 minutes apart, until {retry_deadline}. Access is off meanwhile, and it stops after that.",
92
+ actions: [
93
+ ADD_FUNDS,
94
+ { ...PAY_NOW, until: "retry_deadline" },
95
+ CANCEL_BEHIND,
96
+ { ...RETRY, until: "retry_deadline" }
97
+ ]
98
+ },
99
+ unpaid_approval_revoked: {
100
+ subscriber: "Mesub could not take your payment of {amount}: your wallet no longer approves it (the approval was removed, or replaced by another app's). Adding funds will not fix this, and removing the approval did not cancel the subscription: Cancel it if you want to stop.",
101
+ merchant: {
102
+ retries: "Payment late: the subscriber's wallet no longer approves Mesub (removed in the wallet, or replaced by another app's approval). Every retry fails until it is back, and no Mesub route restores it today. It stops on {access_until}.",
103
+ free: "Payment late: the subscriber's wallet no longer approves Mesub (removed in the wallet, or replaced by another app's approval). Every retry fails until it is back, and no Mesub route restores it today. It stops at {retry_deadline}."
104
+ },
105
+ // A retry by hand is accepted by the dashboard but fails, and the
106
+ // subscriber's is refused (`retry_cannot_succeed`): neither is offered.
107
+ actions: [CANCEL_BEHIND]
108
+ },
109
+ unpaid_authority_closed: {
110
+ subscriber: "Mesub can no longer take payments for this subscription: your wallet's approval for {token} was closed for good, so adding funds will not help. Cancel it, then close it once its period ends if you want to subscribe again.",
111
+ merchant: {
112
+ retries: "Payment late, for good: the subscriber's subscription authority for {token} was closed (or re-created), so no retry can succeed. It stops on {access_until}, unless the subscriber cancels first.",
113
+ free: "Payment late, for good: the subscriber's subscription authority for {token} was closed (or re-created), so no retry can succeed. It stops at {retry_deadline}, unless the subscriber cancels first."
114
+ },
115
+ // Close, then subscribe, come once it is cancelled and its period ended.
116
+ actions: [CANCEL_BEHIND]
117
+ },
118
+ unpaid_unknown: {
119
+ subscriber: "Your wallet refused the payment of {amount}, so nothing was taken. Check that it still holds {amount} in {token} and can send it, then pay it now, or Cancel to stop.",
120
+ merchant: "Payment late: the network refused the payment for a reason Mesub does not name (for example a frozen {token} account). See the last attempt's reason.",
121
+ actions: [PAY_NOW, CANCEL_BEHIND, RETRY]
122
+ },
123
+ // Blocked: every charge fails for a reason that is not the subscriber's
124
+ // (Mesub-io/backend#398). The status does not move, access has an end.
125
+ // Worded here from the API's own comments, after the table of 2026-10-05.
126
+ blocked_mesub: {
127
+ subscriber: "Mesub cannot take your payments for {plan_name} for now, because of a problem on Mesub's side: there is nothing for you to do or sign, and Mesub keeps trying.[ You keep access until {access_until} if no payment goes through by then.] Access comes back on its own with the first payment that goes through.",
128
+ merchant: `Payments blocked[ since {blocked_since}] by a problem on Mesub's side: nothing to fix on yours, and none of it is the subscriber's doing. Mesub keeps trying.${BLOCKED_ACCESS}`,
129
+ actions: [CANCEL]
130
+ },
131
+ blocked_puller_not_allowed: {
132
+ subscriber: BLOCKED_BY_SETUP,
133
+ merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: Mesub's puller was taken off the plan on chain, so Mesub can take no payment for it. Add Mesub back to the plan's pullers, with the wallet that owns the plan; Mesub keeps trying.${BLOCKED_ACCESS}`,
134
+ actions: [CANCEL]
135
+ },
136
+ blocked_receiver_not_allowed: {
137
+ subscriber: BLOCKED_BY_SETUP,
138
+ merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the wallet that receives its payments is not among the plan's destinations on chain. Choose one that is; Mesub keeps trying.${BLOCKED_ACCESS}`,
139
+ actions: [CANCEL]
140
+ },
141
+ blocked_receiver_account_missing: {
142
+ subscriber: BLOCKED_BY_SETUP,
143
+ merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the wallet that receives its payments has no token account for the plan's token, and nobody else on the plan can be paid. Open that account; Mesub keeps trying.${BLOCKED_ACCESS}`,
144
+ actions: [CANCEL]
145
+ },
146
+ blocked_receiver_account_frozen: {
147
+ subscriber: BLOCKED_BY_SETUP,
148
+ merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the token account of the wallet that receives its payments is frozen, so it cannot take payments. Choose another receiving wallet, or ask the token's issuer to unfreeze that account; Mesub keeps trying.${BLOCKED_ACCESS}`,
149
+ actions: [CANCEL]
150
+ },
151
+ blocked_mint_gone: {
152
+ subscriber: `Mesub can no longer take your payments for {plan_name}: the token it is paid in no longer exists. This is not because of anything you did, and there is nothing for you to do or sign.[ You keep access until {access_until}.] ${NEVER_BACK}`,
153
+ merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the plan's token no longer exists on chain, so nothing can be collected on it. Create a new plan with another token.[ Access ends on {access_until}.] ${NEVER_BACK}`,
154
+ actions: [CANCEL]
155
+ },
156
+ // Cancelled
157
+ cancelled_with_access: {
158
+ subscriber: "Cancelled: nothing more will be taken, and you keep access until {access_until}. Resume before then to keep it going.",
159
+ merchant: "Cancelled[ on {cancelled_at}]: access runs until {access_until}, and nothing more will be taken.",
160
+ actions: [{ ...RESUME, confirm: "confirm_resume" }]
161
+ },
162
+ cancelled_no_access: {
163
+ subscriber: "Cancelled. Your last payment was missed, so you have no access, and Mesub will not collect it; you can close the subscription[ from {closable_from}] to get your deposit back.",
164
+ merchant: "Cancelled while behind[ on {cancelled_at}]: no access, and Mesub will not collect the missed payment. The subscriber can close it[ from {closable_from}], and must do so before subscribing again.",
165
+ // Not resume (refused) and not subscribe (refused until it is closed).
166
+ actions: [CLOSE]
167
+ },
168
+ cancelled_ended: {
169
+ subscriber: "This subscription has ended. Close it to get your deposit back; you need to close it before subscribing to {plan_name} again.",
170
+ merchant: "Ended by the subscriber's cancellation. Its subscription account stays open until the subscriber closes it, which they must do before subscribing again.",
171
+ actions: [CLOSE]
172
+ },
173
+ // Stopped
174
+ stopped: {
175
+ subscriber: "Your subscription stopped after missed payments, and your access has ended; Mesub will not take payments for it again. Its subscription account stays open on your wallet, so the plan's owner could still charge it: Cancel if you are not coming back.",
176
+ // `{stop_cause}` from `stop_reason` (`STOP_CAUSES`), left out while it is null.
177
+ merchant: "Stopped[: {stop_cause}]. Mesub takes nothing more and access is off. The subscription account stays open on chain, and you, as the plan's owner, could still pull from it, until the subscriber cancels and closes it. The subscriber can come back over it.",
178
+ // Subscribing again restarts it, while it is not cancelled on chain,
179
+ // the plan's terms are unchanged and the project has a free seat. A
180
+ // close only once Mesub says it is `closable`: cancelled on chain
181
+ // without Mesub, and past its end.
182
+ actions: [CANCEL_BEHIND, SUBSCRIBE, CLOSE]
183
+ },
184
+ // The wallet closed its subscription authority for good (Mesub-io/backend#394,
185
+ // #402): nobody can charge it again, and it cannot be restarted. Worded
186
+ // from the stopped emails' `authority-closed` variants.
187
+ stopped_authority_closed: {
188
+ subscriber: "Your wallet's approval for {token} was closed for good, so no payment can go through for this subscription any more: it has stopped, your access is over, and it cannot be restarted. Close it to get your deposit back; you can then subscribe to {plan_name} again.",
189
+ merchant: "Stopped for good: the subscriber's wallet approval for {token} was closed, so nobody can take payments from this subscription any more, you as the plan's owner included, and the subscriber cannot come back over it. They can close it at once to get their deposit back.",
190
+ actions: [CLOSE]
191
+ },
192
+ // Paused
193
+ paused: {
194
+ subscriber: "Paused by {merchant_name}: nothing will be taken, and you keep access until {access_until}.",
195
+ merchant: "Paused: your project is over its tier's subscriber limit, so Mesub takes nothing from this subscriber. Access runs to the end of the paid period ({access_until}). Move back up a tier to restart payments.",
196
+ actions: [CANCEL_PAID, UPGRADE]
197
+ },
198
+ paused_access_over: {
199
+ subscriber: "Paused by {merchant_name}: nothing is taken, and your access has ended. If {merchant_name} unpauses it, a payment is due at once; Cancel to stop for good.",
200
+ merchant: "Paused over your tier limit, and its paid period is over, so access is off. Moving back up a tier makes a payment due at once.",
201
+ actions: [CANCEL, UPGRADE]
202
+ },
203
+ paused_cancelled: {
204
+ subscriber: "Cancelled: nothing more will be taken, and you keep access until {access_until}. You can resume before then, but it stays paused, with no payment, until {merchant_name} unpauses it.",
205
+ merchant: "Cancelled while paused over your tier limit; access until {access_until}.",
206
+ actions: [{ ...RESUME, confirm: "confirm_resume_paused" }]
207
+ },
208
+ // Ended
209
+ ended_closed: {
210
+ subscriber: "You closed this subscription, and its deposit is back in your wallet. You can subscribe to {plan_name} again at any time.",
211
+ merchant: "Ended: the subscriber closed it and got the deposit back. They can subscribe again.",
212
+ actions: [SUBSCRIBE]
213
+ },
214
+ ended_authority_closed: {
215
+ subscriber: "This subscription's account was closed outside Mesub, so it has ended and nothing more can be taken. You can subscribe to {plan_name} again.",
216
+ merchant: "Ended: its subscription account was found closed on chain, without Mesub. The subscriber can subscribe again.",
217
+ actions: [SUBSCRIBE]
218
+ },
219
+ ended_plan_removed: {
220
+ subscriber: "{merchant_name} removed {plan_name}, so your subscription has ended and nothing more will be taken.",
221
+ // Closed through Mesub since Mesub-io/backend#344.
222
+ merchant: "Ended: the plan account was deleted. The subscriber's subscription account may still hold their deposit, which they get back by closing it.",
223
+ actions: [CLOSE_OVER]
224
+ },
225
+ ended_plan_replaced: {
226
+ subscriber: "{merchant_name} replaced {plan_name} with another plan, so this subscription has ended and nothing more will be taken.",
227
+ merchant: "Ended: another plan now stands at this plan's address, and this subscription was never signed for it.",
228
+ actions: [CLOSE_OVER]
229
+ },
230
+ ended_plan_ended: {
231
+ // `{plan_ends_at}` is the plan's `ends_at`, not on a subscription.
232
+ subscriber: "{plan_name} reached its end date[ ({plan_ends_at})], so your subscription has ended and nothing more will be taken.",
233
+ merchant: "Ended: the plan passed its end date.",
234
+ // Also read off a running row in the minutes before Mesub ends it,
235
+ // which is not `closable` yet: the close shows once it is.
236
+ actions: [CLOSE_OVER]
237
+ },
238
+ ended_unknown: {
239
+ subscriber: "This subscription has ended, and nothing more will be taken.",
240
+ merchant: "Ended (before Mesub recorded reasons).",
241
+ actions: []
242
+ },
243
+ // Checkout and history
244
+ pending: {
245
+ subscriber: "Not signed yet. Approve it in your wallet to pay the first {amount} and start the subscription.",
246
+ merchant: "Checkout started, not signed yet: nothing is on chain and nothing was charged.",
247
+ // A fresh transaction when the wallet is ready, or after `transaction_expired`.
248
+ actions: [
249
+ {
250
+ by: "merchant",
251
+ action: "transaction",
252
+ sdk: "subscriptions.transaction",
253
+ route: "POST /v1/subscriptions/:id/transaction"
254
+ },
255
+ {
256
+ by: "merchant",
257
+ action: "submit",
258
+ sdk: "subscriptions.submit",
259
+ route: "POST /v1/subscriptions/:id/submit"
260
+ }
261
+ ]
262
+ },
263
+ expired: {
264
+ subscriber: "This checkout was never signed, so nothing was charged. Subscribe again to start.",
265
+ merchant: "Checkout expired unsigned. Nothing on chain.",
266
+ actions: [SUBSCRIBE]
267
+ },
268
+ failed: {
269
+ subscriber: "This subscription did not start: what reached the network was not the transaction prepared for it. If money left your wallet for it, contact {merchant_name}.",
270
+ merchant: "Failed: what landed on chain is not what Mesub built (see the submit reason). Check the wallet's transactions before creating a new one.",
271
+ actions: [
272
+ {
273
+ by: "merchant",
274
+ action: "retrieve",
275
+ sdk: "subscriptions.retrieve",
276
+ route: "GET /v1/subscriptions/:id"
277
+ },
278
+ SUBSCRIBE
279
+ ]
280
+ },
281
+ superseded: {
282
+ // Hide it from subscribers: the newer row is the one in force.
283
+ subscriber: "Replaced by your newer subscription to {plan_name}.",
284
+ // `{replaced_at}`, when the newer row was created, is not on this row.
285
+ merchant: "An earlier subscription of this wallet, replaced when it subscribed again[ on {replaced_at}]. Read the current one.",
286
+ actions: [
287
+ {
288
+ by: "merchant",
289
+ action: "list",
290
+ sdk: "subscriptions.list",
291
+ route: "GET /v1/subscriptions"
292
+ }
293
+ ]
294
+ }
295
+ };
296
+ var CONFIRMATIONS = {
297
+ confirm_cancel_paid: "Cancel {plan_name}? Nothing more will be taken, and you keep access until {access_until}.",
298
+ confirm_cancel_behind: "Cancel {plan_name}? Your access ends now and Mesub will not collect the missed payment; you can close it once the current period ends.",
299
+ confirm_resume: "Resume {plan_name}? Nothing is taken now; your next payment of {amount} is on {access_until}.",
300
+ confirm_resume_paused: "Resume {plan_name}? Nothing is taken now, and nothing will be until {merchant_name} unpauses it.",
301
+ confirm_close: "Close {plan_name}? This deletes its subscription account and returns its deposit to your wallet; you can then subscribe again.",
302
+ confirm_close_over: "Close {plan_name}? This deletes its subscription account and returns its deposit to your wallet."
303
+ };
304
+ var ACCESS_LINE = { granted: "Access until {access_until}", denied: "No access" };
305
+ var END_REASONS = {
306
+ cancelled: "cancelled_ended",
307
+ closed: "ended_closed",
308
+ closed_elsewhere: "ended_authority_closed",
309
+ authority_closed: "ended_authority_closed",
310
+ plan_removed: "ended_plan_removed",
311
+ plan_replaced: "ended_plan_replaced",
312
+ plan_ended: "ended_plan_ended"
313
+ };
314
+ var LATE_REASONS = {
315
+ approval_revoked: "unpaid_approval_revoked",
316
+ authority_closed: "unpaid_authority_closed"
317
+ };
318
+ var STOP_CAUSES = {
319
+ out_of_retries: "every retry of the missed payment failed",
320
+ retry_deadline_passed: "the last day to pay the missed payment passed"
321
+ };
322
+ var BLOCKED_REASONS = {
323
+ mesub: "blocked_mesub",
324
+ puller_not_allowed: "blocked_puller_not_allowed",
325
+ receiver_not_allowed: "blocked_receiver_not_allowed",
326
+ receiver_account_missing: "blocked_receiver_account_missing",
327
+ receiver_account_frozen: "blocked_receiver_account_frozen",
328
+ mint_gone: "blocked_mint_gone"
329
+ };
330
+ var BY_STATUS = /* @__PURE__ */ new Set(["none", "pending", "expired", "failed", "superseded"]);
331
+ function isFree(input) {
332
+ return input.retry_deadline != null;
333
+ }
334
+ function isBilled(input) {
335
+ return input.payment_status !== "none";
336
+ }
337
+ function situationOf(input) {
338
+ const status = input.status;
339
+ const paused = input.paused === true;
340
+ if (BY_STATUS.has(status)) return status;
341
+ if (paused && (status === "active" || status === "unpaid" || status === "cancelled")) {
342
+ if (status === "cancelled") return "paused_cancelled";
343
+ return input.access ? "paused" : "paused_access_over";
344
+ }
345
+ const blocked = _nullishCoalesce(input.blocked_reason, () => ( null));
346
+ if (blocked !== null && (status === "active" || status === "unpaid")) {
347
+ return Object.hasOwn(BLOCKED_REASONS, blocked) ? BLOCKED_REASONS[blocked] : "unknown";
348
+ }
349
+ switch (status) {
350
+ case "ended": {
351
+ const reason = _nullishCoalesce(input.end_reason, () => ( null));
352
+ if (reason === null) return "ended_unknown";
353
+ return Object.hasOwn(END_REASONS, reason) ? END_REASONS[reason] : "unknown";
354
+ }
355
+ case "cancelled":
356
+ if (input.access) return "cancelled_with_access";
357
+ return input.payment_status === "paid" ? "cancelled_ended" : "cancelled_no_access";
358
+ case "unpaid": {
359
+ const reason = _nullishCoalesce(input.late_reason, () => ( null));
360
+ const named = reason !== null && reason !== "insufficient_balance";
361
+ if (named && !Object.hasOwn(LATE_REASONS, reason)) return "unknown";
362
+ if (!isFree(input) && isBilled(input) && input.next_retry_at == null) {
363
+ if (!input.access) return "ended_plan_ended";
364
+ if (input.access_until != null) return "unpaid_last_period";
365
+ }
366
+ if (reason === null) return "unpaid_unknown";
367
+ if (reason === "insufficient_balance") {
368
+ return isFree(input) ? "unpaid_insufficient_balance_free" : "unpaid_insufficient_balance_retries";
369
+ }
370
+ return LATE_REASONS[reason];
371
+ }
372
+ case "active":
373
+ if (!isBilled(input)) return "active_unbilled";
374
+ if (!input.access) return "ended_plan_ended";
375
+ return input.access_until != null && input.next_charge_at == null ? "active_last_period" : "active";
376
+ case "stopped": {
377
+ const reason = _nullishCoalesce(input.stop_reason, () => ( null));
378
+ if (reason === "authority_closed") return "stopped_authority_closed";
379
+ return reason === null || Object.hasOwn(STOP_CAUSES, reason) ? "stopped" : "unknown";
380
+ }
381
+ default:
382
+ return "unknown";
383
+ }
384
+ }
385
+ function explain(input, options = {}) {
386
+ const key = situationOf(input);
387
+ const values = valuesOf(input, options);
388
+ const missing = /* @__PURE__ */ new Set();
389
+ const fill = (template) => render(template, values, missing);
390
+ const accessLine = fill(input.access ? ACCESS_LINE.granted : ACCESS_LINE.denied);
391
+ if (key === "unknown") {
392
+ return {
393
+ key,
394
+ subscriber: accessLine,
395
+ merchant: accessLine,
396
+ access: accessLine,
397
+ actions: [],
398
+ missing: [...missing]
399
+ };
400
+ }
401
+ const situation = SITUATIONS[key];
402
+ const tier = isFree(input) ? "free" : "retries";
403
+ const pick = (text) => typeof text === "string" ? text : text[tier];
404
+ const now = (_nullishCoalesce(options.now, () => ( /* @__PURE__ */ new Date()))).getTime();
405
+ const actions = [];
406
+ for (const template of situation.actions) {
407
+ const { until: field, confirm, when, ...rest } = template;
408
+ const action = { ...rest };
409
+ if (when === "closable" && input.closable === false) continue;
410
+ if (field !== void 0) {
411
+ const until = input[field];
412
+ if (until != null) {
413
+ if (Date.parse(until) <= now) continue;
414
+ action.until = until;
415
+ }
416
+ }
417
+ if (confirm !== void 0) action.confirm = fill(CONFIRMATIONS[confirm]);
418
+ actions.push(action);
419
+ }
420
+ return {
421
+ key,
422
+ subscriber: fill(pick(situation.subscriber)),
423
+ merchant: fill(pick(situation.merchant)),
424
+ access: accessLine,
425
+ actions,
426
+ missing: [...missing]
427
+ };
428
+ }
429
+ function valuesOf(input, options) {
430
+ const date = _nullishCoalesce(options.formatDate, () => ( ((iso) => iso)));
431
+ const asDate = (iso) => iso == null ? void 0 : date(iso);
432
+ const asNumber = (n) => n == null ? void 0 : String(n);
433
+ const names = _nullishCoalesce(options.names, () => ( {}));
434
+ return {
435
+ plan_name: _nullishCoalesce(_nullishCoalesce(names.plan, () => ( input.plan)), () => ( void 0)),
436
+ merchant_name: names.merchant,
437
+ amount: names.amount,
438
+ token: names.token,
439
+ access_until: asDate(input.access_until),
440
+ next_charge_at: asDate(input.next_charge_at),
441
+ next_retry_at: asDate(input.next_retry_at),
442
+ retry_deadline: asDate(input.retry_deadline),
443
+ blocked_since: asDate(input.blocked_since),
444
+ // `/v1/access` only.
445
+ cancelled_at: asDate("cancelled_at" in input ? input.cancelled_at : null),
446
+ // On a subscription only.
447
+ next_retry_number: asNumber("next_retry_number" in input ? input.next_retry_number : null),
448
+ retries_allowed: asNumber("retries_allowed" in input ? input.retries_allowed : null),
449
+ closable_from: asDate(input.closable_from),
450
+ stop_cause: stopCauseOf(input.stop_reason),
451
+ // Not served on `/v1` (yet): their segments are left out.
452
+ plan_ends_at: void 0,
453
+ replaced_at: void 0
454
+ };
455
+ }
456
+ function stopCauseOf(reason) {
457
+ return reason != null && Object.hasOwn(STOP_CAUSES, reason) ? STOP_CAUSES[reason] : void 0;
458
+ }
459
+ var PLACEHOLDER = /\{([a-z_]+)\}/g;
460
+ function render(template, values, missing) {
461
+ const segments = template.replace(
462
+ /\[([^[\]]*)\]/g,
463
+ (_, segment) => [...segment.matchAll(PLACEHOLDER)].every(([, name]) => values[name] !== void 0) ? segment : ""
464
+ );
465
+ return segments.replace(PLACEHOLDER, (whole, name) => {
466
+ const value = values[name];
467
+ if (value !== void 0) return value;
468
+ missing.add(name);
469
+ return whole;
470
+ });
471
+ }
472
+
473
+
474
+
475
+
476
+
477
+
478
+
479
+ exports.SITUATIONS = SITUATIONS; exports.CONFIRMATIONS = CONFIRMATIONS; exports.ACCESS_LINE = ACCESS_LINE; exports.situationOf = situationOf; exports.explain = explain;
480
+ //# sourceMappingURL=chunk-43Q4PNF7.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/node-sdk/node-sdk/dist/chunk-43Q4PNF7.cjs","../src/situations.ts"],"names":[],"mappings":"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","file":"/home/runner/work/node-sdk/node-sdk/dist/chunk-43Q4PNF7.cjs","sourcesContent":[null,"/**\n * One sentence per subscription situation, for the subscriber and for the\n * merchant, and what each of them can do about it: the canonical wording of\n * 2026-10-05, agreed with the backend. Show these instead of writing your\n * own: hand-written ones drift, and some are false (\"add funds\" on a removed\n * approval, \"nothing more is charged\" on a stopped subscription).\n *\n * Browser-safe: this module imports types only, so `@mesub/node/situations`\n * loads in a page, a widget or a docs site with nothing of Node.\n *\n * Templates carry `{placeholders}`. A `[segment]` holds a placeholder `/v1`\n * does not always serve: it is shown when every placeholder in it has a\n * value, and left out otherwise, so the sentence stays true without it.\n */\nimport type { AccessAnswer, BlockedReason, EndReason, LateReason, StopReason } from './answer.js';\nimport type { ServerSubscription } from './subscriptions.js';\n\n/** Who acts. */\nexport type SituationActor = 'subscriber' | 'merchant';\n\n/**\n * What can be done. `add_funds` has no route (the wallet does it).\n * `transaction` hands a pending checkout a fresh transaction to sign\n * (`subscriptions.transaction`, Mesub-io/backend#427). `retry` is\n * the subscriber's \"Pay now\" with the API key (`subscriptions.retry`,\n * Mesub-io/backend#354), and the merchant's on the Mesub dashboard; `upgrade`\n * exists on the dashboard only.\n */\nexport type SituationActionId =\n | 'subscribe'\n | 'transaction'\n | 'submit'\n | 'cancel'\n | 'resume'\n | 'close'\n | 'add_funds'\n | 'retry'\n | 'upgrade'\n | 'retrieve'\n | 'list';\n\n/** One action of a situation, as the table lists it. */\nexport interface SituationActionTemplate {\n readonly by: SituationActor;\n readonly action: SituationActionId;\n /** The method of `@mesub/node`, when there is one: `subscriptions.cancel`, … */\n readonly sdk?: string;\n /** The API route behind it. */\n readonly route?: string;\n /** On the Mesub dashboard only: an API key cannot call it. */\n readonly dashboard?: true;\n /** The sentence to show before the subscriber signs it, a key of `CONFIRMATIONS`. */\n readonly confirm?: ConfirmationKey;\n /** Only while the date in this field of the subscription is ahead. */\n readonly until?: 'access_until' | 'retry_deadline';\n /**\n * Only while the subscription is `closable` (Mesub-io/backend#402): left\n * out where Mesub says `closable: false`, its `closable_from` in the\n * sentence when a date is known.\n */\n readonly when?: 'closable';\n}\n\n/**\n * A sentence, or one per tier where the canonical sentence names both:\n * `retries` for Dev and Business (Mesub retries on its own), `free` for Free\n * (hand retries until `retry_deadline`).\n */\nexport type SituationText = string | { readonly retries: string; readonly free: string };\n\nexport interface Situation {\n readonly subscriber: SituationText;\n readonly merchant: SituationText;\n readonly actions: readonly SituationActionTemplate[];\n}\n\nconst SUBSCRIBE = {\n by: 'subscriber',\n action: 'subscribe',\n sdk: 'subscriptions.create',\n route: 'POST /v1/subscriptions',\n} as const;\nconst CANCEL = {\n by: 'subscriber',\n action: 'cancel',\n sdk: 'subscriptions.cancel',\n route: 'POST /v1/subscriptions/:id/cancel',\n} as const;\nconst RESUME = {\n by: 'subscriber',\n action: 'resume',\n sdk: 'subscriptions.resume',\n route: 'POST /v1/subscriptions/:id/resume',\n until: 'access_until',\n} as const;\nconst CLOSE = {\n by: 'subscriber',\n action: 'close',\n sdk: 'subscriptions.close',\n route: 'POST /v1/subscriptions/:id/close',\n confirm: 'confirm_close',\n when: 'closable',\n} as const;\n/** A close on a plan that is gone or over: nothing to subscribe to again after it. */\nconst CLOSE_OVER = { ...CLOSE, confirm: 'confirm_close_over' } as const;\nconst ADD_FUNDS = { by: 'subscriber', action: 'add_funds' } as const;\n/** Does not count as a retry, and doubles the wait before the next one. */\nconst RETRY = {\n by: 'merchant',\n action: 'retry',\n route: 'POST /merchant/subscriptions/:id/retry',\n dashboard: true,\n} as const;\n/**\n * The subscriber's own, shown as \"Pay now\" (Mesub-io/backend#354): the same\n * rules as the merchant's, nothing to sign, and refused first when it cannot\n * succeed (a short wallet, an approval gone), so no retry is spent for nothing.\n */\nconst PAY_NOW = {\n by: 'subscriber',\n action: 'retry',\n sdk: 'subscriptions.retry',\n route: 'POST /v1/subscriptions/:id/retry',\n} as const;\nconst UPGRADE = {\n by: 'merchant',\n action: 'upgrade',\n route: 'POST /projects/:id/upgrade',\n dashboard: true,\n} as const;\nconst CANCEL_PAID = { ...CANCEL, confirm: 'confirm_cancel_paid' } as const;\n\n/**\n * A block of the merchant's setup, as the subscriber reads it: one sentence for\n * all but a gone token, which no payment can lift.\n */\nconst BLOCKED_BY_SETUP =\n 'Mesub cannot take your payments for {plan_name} for now, because of how the plan is set up, not because of anything you did: there is nothing for you to do or sign, and Mesub keeps trying.[ You keep access until {access_until} if no payment goes through by then.] Access comes back on its own with the first payment that goes through.';\n/** How a merchant's sentence on a block ends, whatever the cause but a gone token. */\nconst BLOCKED_ACCESS =\n '[ Access ends on {access_until} if nothing is collected by then.] Access comes back with the first payment that goes through, with nothing for the subscriber to sign.';\n/**\n * How both sentences on a gone token end: Mesub still tries, and no payment\n * can land on a token that no longer exists.\n */\nconst NEVER_BACK =\n 'No payment can go through on this plan again, so once access ends it does not come back.';\nconst CANCEL_BEHIND = { ...CANCEL, confirm: 'confirm_cancel_behind' } as const;\n\n/**\n * The canonical table, by situation key. The renewal checks\n * (`active_renewal_*`) are left out: Mesub serves them on its dashboard\n * only, never on `/v1`.\n */\nexport const SITUATIONS = {\n // Running\n none: {\n subscriber: 'You are not subscribed to {plan_name}.',\n merchant: 'This customer has no subscription to {plan_name}.',\n actions: [SUBSCRIBE],\n },\n active: {\n subscriber: 'Active. Your next payment of {amount} is on {next_charge_at}.',\n merchant: 'Active and paid up. Next payment of {amount} on {next_charge_at}.',\n actions: [CANCEL_PAID],\n },\n // The plan ends before another charge: the last period, paid in full.\n active_last_period: {\n subscriber:\n 'Active until {access_until}: {plan_name} is ending, and no further payment will be taken.',\n merchant:\n 'Active and paid up, in its last period: the plan is ending, so no further payment is taken. Access ends on {access_until}.',\n actions: [CANCEL_PAID],\n },\n active_unbilled: {\n subscriber: 'Active until {access_until}. No further payment is scheduled.',\n merchant:\n \"Not billed: this subscription's terms were never signed through Mesub, so Mesub never takes a payment for it. Access ends on {access_until}.\",\n actions: [],\n },\n\n // Late (`unpaid`)\n // The plan ends before the next retry: none is scheduled, whatever the reason.\n unpaid_last_period: {\n subscriber:\n 'Your payment of {amount} is late, and Mesub will not try again on its own: {plan_name} is ending. You keep access until {access_until}.',\n merchant:\n 'Payment late, with no retry ahead: the plan ends before the next one. Access continues until {access_until}, and the subscription ends with the plan.',\n actions: [PAY_NOW, CANCEL_BEHIND, RETRY],\n },\n unpaid_insufficient_balance_retries: {\n subscriber:\n 'Your payment of {amount} is late because your wallet held too little, and nothing was taken. Add {amount} to your wallet, then pay it now, with nothing to sign; otherwise Mesub tries again on {next_retry_at}. Your subscription stops if no payment goes through by {access_until}.',\n merchant:\n \"Payment late: the wallet was short. Retry[ {next_retry_number} of {retries_allowed}] on {next_retry_at}, or sooner by hand, yours or the subscriber's. Access continues until {access_until}; the subscription stops then if every retry fails.\",\n actions: [ADD_FUNDS, PAY_NOW, CANCEL_BEHIND, RETRY],\n },\n unpaid_insufficient_balance_free: {\n subscriber:\n 'Your payment of {amount} is late because your wallet held too little, so your access is off. Add {amount} to your wallet, then pay it now, with nothing to sign, before {retry_deadline}; if no payment goes through by then, the subscription stops.',\n merchant:\n 'Payment late: the wallet was short. On Free, Mesub does not retry on its own: you or the subscriber can retry by hand, up to 3 times between you, 10 minutes apart, until {retry_deadline}. Access is off meanwhile, and it stops after that.',\n actions: [\n ADD_FUNDS,\n { ...PAY_NOW, until: 'retry_deadline' },\n CANCEL_BEHIND,\n { ...RETRY, until: 'retry_deadline' },\n ],\n },\n unpaid_approval_revoked: {\n subscriber:\n \"Mesub could not take your payment of {amount}: your wallet no longer approves it (the approval was removed, or replaced by another app's). Adding funds will not fix this, and removing the approval did not cancel the subscription: Cancel it if you want to stop.\",\n merchant: {\n retries:\n \"Payment late: the subscriber's wallet no longer approves Mesub (removed in the wallet, or replaced by another app's approval). Every retry fails until it is back, and no Mesub route restores it today. It stops on {access_until}.\",\n free: \"Payment late: the subscriber's wallet no longer approves Mesub (removed in the wallet, or replaced by another app's approval). Every retry fails until it is back, and no Mesub route restores it today. It stops at {retry_deadline}.\",\n },\n // A retry by hand is accepted by the dashboard but fails, and the\n // subscriber's is refused (`retry_cannot_succeed`): neither is offered.\n actions: [CANCEL_BEHIND],\n },\n unpaid_authority_closed: {\n subscriber:\n \"Mesub can no longer take payments for this subscription: your wallet's approval for {token} was closed for good, so adding funds will not help. Cancel it, then close it once its period ends if you want to subscribe again.\",\n merchant: {\n retries:\n \"Payment late, for good: the subscriber's subscription authority for {token} was closed (or re-created), so no retry can succeed. It stops on {access_until}, unless the subscriber cancels first.\",\n free: \"Payment late, for good: the subscriber's subscription authority for {token} was closed (or re-created), so no retry can succeed. It stops at {retry_deadline}, unless the subscriber cancels first.\",\n },\n // Close, then subscribe, come once it is cancelled and its period ended.\n actions: [CANCEL_BEHIND],\n },\n unpaid_unknown: {\n subscriber:\n 'Your wallet refused the payment of {amount}, so nothing was taken. Check that it still holds {amount} in {token} and can send it, then pay it now, or Cancel to stop.',\n merchant:\n \"Payment late: the network refused the payment for a reason Mesub does not name (for example a frozen {token} account). See the last attempt's reason.\",\n actions: [PAY_NOW, CANCEL_BEHIND, RETRY],\n },\n\n // Blocked: every charge fails for a reason that is not the subscriber's\n // (Mesub-io/backend#398). The status does not move, access has an end.\n // Worded here from the API's own comments, after the table of 2026-10-05.\n blocked_mesub: {\n subscriber:\n \"Mesub cannot take your payments for {plan_name} for now, because of a problem on Mesub's side: there is nothing for you to do or sign, and Mesub keeps trying.[ You keep access until {access_until} if no payment goes through by then.] Access comes back on its own with the first payment that goes through.\",\n merchant: `Payments blocked[ since {blocked_since}] by a problem on Mesub's side: nothing to fix on yours, and none of it is the subscriber's doing. Mesub keeps trying.${BLOCKED_ACCESS}`,\n actions: [CANCEL],\n },\n blocked_puller_not_allowed: {\n subscriber: BLOCKED_BY_SETUP,\n merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: Mesub's puller was taken off the plan on chain, so Mesub can take no payment for it. Add Mesub back to the plan's pullers, with the wallet that owns the plan; Mesub keeps trying.${BLOCKED_ACCESS}`,\n actions: [CANCEL],\n },\n blocked_receiver_not_allowed: {\n subscriber: BLOCKED_BY_SETUP,\n merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the wallet that receives its payments is not among the plan's destinations on chain. Choose one that is; Mesub keeps trying.${BLOCKED_ACCESS}`,\n actions: [CANCEL],\n },\n blocked_receiver_account_missing: {\n subscriber: BLOCKED_BY_SETUP,\n merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the wallet that receives its payments has no token account for the plan's token, and nobody else on the plan can be paid. Open that account; Mesub keeps trying.${BLOCKED_ACCESS}`,\n actions: [CANCEL],\n },\n blocked_receiver_account_frozen: {\n subscriber: BLOCKED_BY_SETUP,\n merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the token account of the wallet that receives its payments is frozen, so it cannot take payments. Choose another receiving wallet, or ask the token's issuer to unfreeze that account; Mesub keeps trying.${BLOCKED_ACCESS}`,\n actions: [CANCEL],\n },\n blocked_mint_gone: {\n subscriber: `Mesub can no longer take your payments for {plan_name}: the token it is paid in no longer exists. This is not because of anything you did, and there is nothing for you to do or sign.[ You keep access until {access_until}.] ${NEVER_BACK}`,\n merchant: `Payments blocked[ since {blocked_since}] by your plan's setup: the plan's token no longer exists on chain, so nothing can be collected on it. Create a new plan with another token.[ Access ends on {access_until}.] ${NEVER_BACK}`,\n actions: [CANCEL],\n },\n\n // Cancelled\n cancelled_with_access: {\n subscriber:\n 'Cancelled: nothing more will be taken, and you keep access until {access_until}. Resume before then to keep it going.',\n merchant:\n 'Cancelled[ on {cancelled_at}]: access runs until {access_until}, and nothing more will be taken.',\n actions: [{ ...RESUME, confirm: 'confirm_resume' }],\n },\n cancelled_no_access: {\n subscriber:\n 'Cancelled. Your last payment was missed, so you have no access, and Mesub will not collect it; you can close the subscription[ from {closable_from}] to get your deposit back.',\n merchant:\n 'Cancelled while behind[ on {cancelled_at}]: no access, and Mesub will not collect the missed payment. The subscriber can close it[ from {closable_from}], and must do so before subscribing again.',\n // Not resume (refused) and not subscribe (refused until it is closed).\n actions: [CLOSE],\n },\n cancelled_ended: {\n subscriber:\n 'This subscription has ended. Close it to get your deposit back; you need to close it before subscribing to {plan_name} again.',\n merchant:\n \"Ended by the subscriber's cancellation. Its subscription account stays open until the subscriber closes it, which they must do before subscribing again.\",\n actions: [CLOSE],\n },\n\n // Stopped\n stopped: {\n subscriber:\n \"Your subscription stopped after missed payments, and your access has ended; Mesub will not take payments for it again. Its subscription account stays open on your wallet, so the plan's owner could still charge it: Cancel if you are not coming back.\",\n // `{stop_cause}` from `stop_reason` (`STOP_CAUSES`), left out while it is null.\n merchant:\n \"Stopped[: {stop_cause}]. Mesub takes nothing more and access is off. The subscription account stays open on chain, and you, as the plan's owner, could still pull from it, until the subscriber cancels and closes it. The subscriber can come back over it.\",\n // Subscribing again restarts it, while it is not cancelled on chain,\n // the plan's terms are unchanged and the project has a free seat. A\n // close only once Mesub says it is `closable`: cancelled on chain\n // without Mesub, and past its end.\n actions: [CANCEL_BEHIND, SUBSCRIBE, CLOSE],\n },\n // The wallet closed its subscription authority for good (Mesub-io/backend#394,\n // #402): nobody can charge it again, and it cannot be restarted. Worded\n // from the stopped emails' `authority-closed` variants.\n stopped_authority_closed: {\n subscriber:\n \"Your wallet's approval for {token} was closed for good, so no payment can go through for this subscription any more: it has stopped, your access is over, and it cannot be restarted. Close it to get your deposit back; you can then subscribe to {plan_name} again.\",\n merchant:\n \"Stopped for good: the subscriber's wallet approval for {token} was closed, so nobody can take payments from this subscription any more, you as the plan's owner included, and the subscriber cannot come back over it. They can close it at once to get their deposit back.\",\n actions: [CLOSE],\n },\n\n // Paused\n paused: {\n subscriber:\n 'Paused by {merchant_name}: nothing will be taken, and you keep access until {access_until}.',\n merchant:\n \"Paused: your project is over its tier's subscriber limit, so Mesub takes nothing from this subscriber. Access runs to the end of the paid period ({access_until}). Move back up a tier to restart payments.\",\n actions: [CANCEL_PAID, UPGRADE],\n },\n paused_access_over: {\n subscriber:\n 'Paused by {merchant_name}: nothing is taken, and your access has ended. If {merchant_name} unpauses it, a payment is due at once; Cancel to stop for good.',\n merchant:\n 'Paused over your tier limit, and its paid period is over, so access is off. Moving back up a tier makes a payment due at once.',\n actions: [CANCEL, UPGRADE],\n },\n paused_cancelled: {\n subscriber:\n 'Cancelled: nothing more will be taken, and you keep access until {access_until}. You can resume before then, but it stays paused, with no payment, until {merchant_name} unpauses it.',\n merchant: 'Cancelled while paused over your tier limit; access until {access_until}.',\n actions: [{ ...RESUME, confirm: 'confirm_resume_paused' }],\n },\n\n // Ended\n ended_closed: {\n subscriber:\n 'You closed this subscription, and its deposit is back in your wallet. You can subscribe to {plan_name} again at any time.',\n merchant:\n 'Ended: the subscriber closed it and got the deposit back. They can subscribe again.',\n actions: [SUBSCRIBE],\n },\n ended_authority_closed: {\n subscriber:\n \"This subscription's account was closed outside Mesub, so it has ended and nothing more can be taken. You can subscribe to {plan_name} again.\",\n merchant:\n 'Ended: its subscription account was found closed on chain, without Mesub. The subscriber can subscribe again.',\n actions: [SUBSCRIBE],\n },\n ended_plan_removed: {\n subscriber:\n '{merchant_name} removed {plan_name}, so your subscription has ended and nothing more will be taken.',\n // Closed through Mesub since Mesub-io/backend#344.\n merchant:\n \"Ended: the plan account was deleted. The subscriber's subscription account may still hold their deposit, which they get back by closing it.\",\n actions: [CLOSE_OVER],\n },\n ended_plan_replaced: {\n subscriber:\n '{merchant_name} replaced {plan_name} with another plan, so this subscription has ended and nothing more will be taken.',\n merchant:\n \"Ended: another plan now stands at this plan's address, and this subscription was never signed for it.\",\n actions: [CLOSE_OVER],\n },\n ended_plan_ended: {\n // `{plan_ends_at}` is the plan's `ends_at`, not on a subscription.\n subscriber:\n '{plan_name} reached its end date[ ({plan_ends_at})], so your subscription has ended and nothing more will be taken.',\n merchant: 'Ended: the plan passed its end date.',\n // Also read off a running row in the minutes before Mesub ends it,\n // which is not `closable` yet: the close shows once it is.\n actions: [CLOSE_OVER],\n },\n ended_unknown: {\n subscriber: 'This subscription has ended, and nothing more will be taken.',\n merchant: 'Ended (before Mesub recorded reasons).',\n actions: [],\n },\n\n // Checkout and history\n pending: {\n subscriber:\n 'Not signed yet. Approve it in your wallet to pay the first {amount} and start the subscription.',\n merchant: 'Checkout started, not signed yet: nothing is on chain and nothing was charged.',\n // A fresh transaction when the wallet is ready, or after `transaction_expired`.\n actions: [\n {\n by: 'merchant',\n action: 'transaction',\n sdk: 'subscriptions.transaction',\n route: 'POST /v1/subscriptions/:id/transaction',\n },\n {\n by: 'merchant',\n action: 'submit',\n sdk: 'subscriptions.submit',\n route: 'POST /v1/subscriptions/:id/submit',\n },\n ],\n },\n expired: {\n subscriber:\n 'This checkout was never signed, so nothing was charged. Subscribe again to start.',\n merchant: 'Checkout expired unsigned. Nothing on chain.',\n actions: [SUBSCRIBE],\n },\n failed: {\n subscriber:\n 'This subscription did not start: what reached the network was not the transaction prepared for it. If money left your wallet for it, contact {merchant_name}.',\n merchant:\n \"Failed: what landed on chain is not what Mesub built (see the submit reason). Check the wallet's transactions before creating a new one.\",\n actions: [\n {\n by: 'merchant',\n action: 'retrieve',\n sdk: 'subscriptions.retrieve',\n route: 'GET /v1/subscriptions/:id',\n },\n SUBSCRIBE,\n ],\n },\n superseded: {\n // Hide it from subscribers: the newer row is the one in force.\n subscriber: 'Replaced by your newer subscription to {plan_name}.',\n // `{replaced_at}`, when the newer row was created, is not on this row.\n merchant:\n 'An earlier subscription of this wallet, replaced when it subscribed again[ on {replaced_at}]. Read the current one.',\n actions: [\n {\n by: 'merchant',\n action: 'list',\n sdk: 'subscriptions.list',\n route: 'GET /v1/subscriptions',\n },\n ],\n },\n} as const satisfies Record<string, Situation>;\n\n/** The sentences to show before the subscriber signs an action. */\nexport const CONFIRMATIONS = {\n confirm_cancel_paid:\n 'Cancel {plan_name}? Nothing more will be taken, and you keep access until {access_until}.',\n confirm_cancel_behind:\n 'Cancel {plan_name}? Your access ends now and Mesub will not collect the missed payment; you can close it once the current period ends.',\n confirm_resume:\n 'Resume {plan_name}? Nothing is taken now; your next payment of {amount} is on {access_until}.',\n confirm_resume_paused:\n 'Resume {plan_name}? Nothing is taken now, and nothing will be until {merchant_name} unpauses it.',\n confirm_close:\n 'Close {plan_name}? This deletes its subscription account and returns its deposit to your wallet; you can then subscribe again.',\n confirm_close_over:\n 'Close {plan_name}? This deletes its subscription account and returns its deposit to your wallet.',\n} as const;\n\n/** Shown beside every sentence, which therefore need not repeat it. */\nexport const ACCESS_LINE = { granted: 'Access until {access_until}', denied: 'No access' } as const;\n\nexport type ConfirmationKey = keyof typeof CONFIRMATIONS;\n\n/**\n * A situation of the table, or `unknown`: a status or a reason newer than\n * this release, whose sentences are the access line, true of any situation.\n */\nexport type SituationKey = keyof typeof SITUATIONS | 'unknown';\n\nexport interface ExplainOptions {\n /** What \"now\" is, for actions open until a date. Defaults to the clock. */\n now?: Date;\n /** What neither `/v1/access` nor a subscription carries, as you show it. */\n names?: {\n /** `{plan_name}`. Defaults to the plan's slug. */\n plan?: string;\n /** `{merchant_name}`: your name, as subscribers know it. */\n merchant?: string;\n /** `{amount}`: the plan's amount with its symbol, such as `9.99 USDC`. */\n amount?: string;\n /** `{token}`: the plan's token symbol, such as `USDC`. */\n token?: string;\n };\n /**\n * How a date is written. Defaults to the ISO 8601 string as Mesub serves\n * it, the same in every locale: pass your own to show local time.\n */\n formatDate?: (iso: string) => string;\n}\n\nexport interface ExplainedAction {\n by: SituationActor;\n action: SituationActionId;\n sdk?: string;\n route?: string;\n dashboard?: true;\n /** Until when it can be done, as served (ISO 8601). */\n until?: string;\n /** The sentence to show before the subscriber signs it, filled. */\n confirm?: string;\n}\n\nexport interface Explanation {\n key: SituationKey;\n /** For the subscriber, filled. */\n subscriber: string;\n /** For the merchant, filled. */\n merchant: string;\n /** \"Access until …\" or \"No access\", to show beside either sentence. */\n access: string;\n /** What can be done now, by whom. */\n actions: ExplainedAction[];\n /**\n * Placeholders left as `{name}` in a sentence, since nothing gave them a\n * value: pass them in `options.names`, or check the answer's fields.\n */\n missing: string[];\n}\n\n/** What `explain` reads: an answer of `/v1/access` or a subscription. */\nexport type Explainable = AccessAnswer | ServerSubscription;\n\n/**\n * `closed_elsewhere` keeps the situation `authority_closed` had before\n * Mesub-io/backend#476 renamed it, id included: both read as one.\n */\nconst END_REASONS: Record<EndReason, keyof typeof SITUATIONS> = {\n cancelled: 'cancelled_ended',\n closed: 'ended_closed',\n closed_elsewhere: 'ended_authority_closed',\n authority_closed: 'ended_authority_closed',\n plan_removed: 'ended_plan_removed',\n plan_replaced: 'ended_plan_replaced',\n plan_ended: 'ended_plan_ended',\n};\n\nconst LATE_REASONS: Record<Exclude<LateReason, 'insufficient_balance'>, keyof typeof SITUATIONS> = {\n approval_revoked: 'unpaid_approval_revoked',\n authority_closed: 'unpaid_authority_closed',\n};\n\n/**\n * What `{stop_cause}` reads for each reason that leaves the subscription to\n * come back over. `authority_closed` has a situation of its own.\n */\nconst STOP_CAUSES: Record<Exclude<StopReason, 'authority_closed'>, string> = {\n out_of_retries: 'every retry of the missed payment failed',\n retry_deadline_passed: 'the last day to pay the missed payment passed',\n};\n\nconst BLOCKED_REASONS: Record<BlockedReason, keyof typeof SITUATIONS> = {\n mesub: 'blocked_mesub',\n puller_not_allowed: 'blocked_puller_not_allowed',\n receiver_not_allowed: 'blocked_receiver_not_allowed',\n receiver_account_missing: 'blocked_receiver_account_missing',\n receiver_account_frozen: 'blocked_receiver_account_frozen',\n mint_gone: 'blocked_mint_gone',\n};\n\n/** A key that is its status, whatever else the answer says. */\nconst BY_STATUS = new Set(['none', 'pending', 'expired', 'failed', 'superseded']);\n\n/** Free is read as a `retry_deadline` set: `/v1` does not serve the tier. */\nfunction isFree(input: Explainable): boolean {\n return input.retry_deadline != null;\n}\n\n/** Billed and running: neither parked nor one Mesub never pulls, which `payment_status` says. */\nfunction isBilled(input: Explainable): boolean {\n return input.payment_status !== 'none';\n}\n\n/**\n * Which situation an answer is in, in the canonical order: status first,\n * then paused, then blocked, then the end or late reason. Never throws: a\n * status or a reason this release does not know is `unknown`.\n *\n * A plan's end is read from what it leaves on the answer, which carries no\n * `ends_at`: access with no charge or retry ahead is the last period, and a\n * running status without access is the minutes between the end and the job\n * that ends the row. That one reads as ended already.\n */\nexport function situationOf(input: Explainable): SituationKey {\n const status: string = input.status;\n const paused = input.paused === true;\n\n if (BY_STATUS.has(status)) return status as SituationKey;\n\n if (paused && (status === 'active' || status === 'unpaid' || status === 'cancelled')) {\n if (status === 'cancelled') return 'paused_cancelled';\n return input.access ? 'paused' : 'paused_access_over';\n }\n\n // Before the plan's end is read off a running row without access: a\n // blocked one loses its access too, and has not ended.\n const blocked: string | null = input.blocked_reason ?? null;\n if (blocked !== null && (status === 'active' || status === 'unpaid')) {\n return Object.hasOwn(BLOCKED_REASONS, blocked)\n ? BLOCKED_REASONS[blocked as BlockedReason]\n : 'unknown';\n }\n\n switch (status) {\n case 'ended': {\n const reason: string | null = input.end_reason ?? null;\n if (reason === null) return 'ended_unknown';\n return Object.hasOwn(END_REASONS, reason)\n ? END_REASONS[reason as EndReason]\n : 'unknown';\n }\n case 'cancelled':\n if (input.access) return 'cancelled_with_access';\n // Paid up and cut short by the plan's end: it ends as cancelled.\n return input.payment_status === 'paid' ? 'cancelled_ended' : 'cancelled_no_access';\n case 'unpaid': {\n const reason: string | null = input.late_reason ?? null;\n const named = reason !== null && reason !== 'insufficient_balance';\n if (named && !Object.hasOwn(LATE_REASONS, reason)) return 'unknown';\n // On Free a late one has no access and no retry of Mesub's, end or not.\n if (!isFree(input) && isBilled(input) && input.next_retry_at == null) {\n if (!input.access) return 'ended_plan_ended';\n if (input.access_until != null) return 'unpaid_last_period';\n }\n if (reason === null) return 'unpaid_unknown';\n if (reason === 'insufficient_balance') {\n return isFree(input)\n ? 'unpaid_insufficient_balance_free'\n : 'unpaid_insufficient_balance_retries';\n }\n return LATE_REASONS[reason as keyof typeof LATE_REASONS];\n }\n case 'active':\n if (!isBilled(input)) return 'active_unbilled';\n if (!input.access) return 'ended_plan_ended';\n return input.access_until != null && input.next_charge_at == null\n ? 'active_last_period'\n : 'active';\n case 'stopped': {\n const reason: string | null = input.stop_reason ?? null;\n if (reason === 'authority_closed') return 'stopped_authority_closed';\n // A reason newer than this release may not leave it to come back over.\n return reason === null || Object.hasOwn(STOP_CAUSES, reason) ? 'stopped' : 'unknown';\n }\n default:\n return 'unknown';\n }\n}\n\n/**\n * The situation of a subscription, in words: one sentence for the\n * subscriber, one for the merchant, the access line, and what each can do\n * now. Takes an answer of `/v1/access` or a subscription as `@mesub/node`\n * returns them. Never throws.\n *\n * ```ts\n * const { subscriber, access, actions } = explain(answer, {\n * names: { plan: 'Pro', merchant: 'Acme', amount: '9.99 USDC', token: 'USDC' },\n * });\n * ```\n */\nexport function explain(input: Explainable, options: ExplainOptions = {}): Explanation {\n const key = situationOf(input);\n const values = valuesOf(input, options);\n const missing = new Set<string>();\n const fill = (template: string): string => render(template, values, missing);\n const accessLine = fill(input.access ? ACCESS_LINE.granted : ACCESS_LINE.denied);\n\n if (key === 'unknown') {\n return {\n key,\n subscriber: accessLine,\n merchant: accessLine,\n access: accessLine,\n actions: [],\n missing: [...missing],\n };\n }\n\n const situation: Situation = SITUATIONS[key];\n const tier = isFree(input) ? 'free' : 'retries';\n const pick = (text: SituationText): string => (typeof text === 'string' ? text : text[tier]);\n const now = (options.now ?? new Date()).getTime();\n const actions: ExplainedAction[] = [];\n\n for (const template of situation.actions) {\n const { until: field, confirm, when, ...rest } = template;\n const action: ExplainedAction = { ...rest };\n\n // Undefined only on an answer cached by an older SDK: offered, as the table says.\n if (when === 'closable' && input.closable === false) continue;\n\n if (field !== undefined) {\n const until = input[field];\n if (until != null) {\n if (Date.parse(until) <= now) continue;\n action.until = until;\n }\n }\n if (confirm !== undefined) action.confirm = fill(CONFIRMATIONS[confirm]);\n actions.push(action);\n }\n\n return {\n key,\n subscriber: fill(pick(situation.subscriber)),\n merchant: fill(pick(situation.merchant)),\n access: accessLine,\n actions,\n missing: [...missing],\n };\n}\n\n/** Every value a placeholder can take from this answer and these options. */\nfunction valuesOf(input: Explainable, options: ExplainOptions): Record<string, string | undefined> {\n const date = options.formatDate ?? ((iso: string) => iso);\n const asDate = (iso: string | null | undefined) => (iso == null ? undefined : date(iso));\n const asNumber = (n: number | null | undefined) => (n == null ? undefined : String(n));\n const names = options.names ?? {};\n\n return {\n plan_name: names.plan ?? input.plan ?? undefined,\n merchant_name: names.merchant,\n amount: names.amount,\n token: names.token,\n access_until: asDate(input.access_until),\n next_charge_at: asDate(input.next_charge_at),\n next_retry_at: asDate(input.next_retry_at),\n retry_deadline: asDate(input.retry_deadline),\n blocked_since: asDate(input.blocked_since),\n // `/v1/access` only.\n cancelled_at: asDate('cancelled_at' in input ? input.cancelled_at : null),\n // On a subscription only.\n next_retry_number: asNumber('next_retry_number' in input ? input.next_retry_number : null),\n retries_allowed: asNumber('retries_allowed' in input ? input.retries_allowed : null),\n closable_from: asDate(input.closable_from),\n stop_cause: stopCauseOf(input.stop_reason),\n // Not served on `/v1` (yet): their segments are left out.\n plan_ends_at: undefined,\n replaced_at: undefined,\n };\n}\n\n/** The words of a stop reason this release names, undefined for any other. */\nfunction stopCauseOf(reason: string | null | undefined): string | undefined {\n return reason != null && Object.hasOwn(STOP_CAUSES, reason)\n ? STOP_CAUSES[reason as keyof typeof STOP_CAUSES]\n : undefined;\n}\n\nconst PLACEHOLDER = /\\{([a-z_]+)\\}/g;\n\n/**\n * Fills a template: a `[segment]` with a placeholder lacking a value is left\n * out, and a placeholder outside one is kept as `{name}` and noted missing.\n */\nfunction render(\n template: string,\n values: Record<string, string | undefined>,\n missing: Set<string>,\n): string {\n const segments = template.replace(/\\[([^[\\]]*)\\]/g, (_, segment: string) =>\n [...segment.matchAll(PLACEHOLDER)].every(([, name]) => values[name!] !== undefined)\n ? segment\n : '',\n );\n\n return segments.replace(PLACEHOLDER, (whole, name: string) => {\n const value = values[name];\n if (value !== undefined) return value;\n missing.add(name);\n return whole;\n });\n}\n"]}