@openlup/core 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -30
- package/README.md +6 -5
- package/dist/subscription/cycleHardening.d.ts +8 -11
- package/dist/subscription/cycleHardening.js +8 -11
- package/dist/subscription/subscriptionEngineLifecycle.d.ts +9 -16
- package/dist/subscription/subscriptionEngineLifecycle.js +12 -39
- package/docs/SUBSCRIPTION_ENGINE.md +17 -20
- package/package.json +1 -1
- package/release-gates.json +2 -2
- package/src/subscription/cycleHardening.ts +8 -11
- package/src/subscription/subscriptionEngineLifecycle.ts +12 -40
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ in the source repository.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.26.0]
|
|
14
|
+
|
|
15
|
+
- Publishable on the npm `latest` dist-tag as `0.26.0`, from tag
|
|
16
|
+
`openlup-core-v0.26.0`.
|
|
17
|
+
|
|
18
|
+
## [0.25.0]
|
|
19
|
+
|
|
20
|
+
- Publishable on the npm `latest` dist-tag as `0.25.0`, from tag
|
|
21
|
+
`openlup-core-v0.25.0`.
|
|
22
|
+
|
|
23
|
+
- Keep active subscriptions unchanged when the retry ladder is exhausted; the compatibility function emits no pause event and retains historical paused-row recovery.
|
|
24
|
+
|
|
25
|
+
Migration notes: Exhaustion no longer changes an active subscription to paused. Existing paused records are not rewritten; explicit customer lifecycle commands and historical recovery remain separate.
|
|
26
|
+
|
|
13
27
|
## [0.24.0]
|
|
14
28
|
|
|
15
29
|
- Publishable on the npm `latest` dist-tag as `0.24.0`, from tag
|
|
@@ -26,33 +40,3 @@ Migration: upgrade the complete set together for exact internal peers; core intr
|
|
|
26
40
|
### Known limitations
|
|
27
41
|
|
|
28
42
|
The fulfillment status map retains localized adopter labels; hosts still need injected labels or internationalization keys.
|
|
29
|
-
|
|
30
|
-
## [0.23.0]
|
|
31
|
-
|
|
32
|
-
- Publishable on the npm `latest` dist-tag as `0.23.0`, from tag
|
|
33
|
-
`openlup-core-v0.23.0`.
|
|
34
|
-
|
|
35
|
-
- Republish unchanged APIs, runtime behavior and shipped SQL from `0.22.0` as part of the complete `0.23.0` package set.
|
|
36
|
-
|
|
37
|
-
Migration: upgrade the complete set together for exact internal peers; core introduces no API or schema migration.
|
|
38
|
-
|
|
39
|
-
### Known limitations
|
|
40
|
-
|
|
41
|
-
The fulfillment status map retains localized adopter labels; hosts still need injected labels or internationalization keys.
|
|
42
|
-
|
|
43
|
-
## [0.22.0]
|
|
44
|
-
|
|
45
|
-
- Publishable on the npm `latest` dist-tag as `0.22.0`, from tag
|
|
46
|
-
`openlup-core-v0.22.0`.
|
|
47
|
-
|
|
48
|
-
- Add pure fulfillment status, tracking-progress, current-parcel and canonical
|
|
49
|
-
fact-parser helpers to the existing `./fulfillment` subpath. Generic fact types
|
|
50
|
-
retain their names; narrowed fact aliases use the `Canonical` prefix. New
|
|
51
|
-
fulfillment re-exports carry the same `@beta` annotation as existing facts.
|
|
52
|
-
|
|
53
|
-
Migration: existing platform fulfillment imports remain compatible re-exports;
|
|
54
|
-
no application import or schema migration is required.
|
|
55
|
-
|
|
56
|
-
### Known limitations
|
|
57
|
-
|
|
58
|
-
- `FULFILLMENT_STATUS_MAP` carries localized adopter labels. Replace them with host-injected labels or internationalization keys before any stable-framework claim.
|
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@ application" section, which `docs:check` requires, tells an application's agent
|
|
|
8
8
|
not to edit or patch the installed package and to upgrade every `@openlup/*`
|
|
9
9
|
package together.
|
|
10
10
|
|
|
11
|
-
The prepared `0.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
The size baseline is the published `0.
|
|
15
|
-
the source-only `CHANGELOG-HISTORY.md`.
|
|
11
|
+
The prepared `0.26.0` set republishes this kernel with the platform's managed
|
|
12
|
+
dunning attempt-replay changes. Upgrade all packages together and read
|
|
13
|
+
[the migration notes](CHANGELOG.md#0260), including the platform's required
|
|
14
|
+
nullable failure-attempt field. The size baseline is the published `0.25.0`
|
|
15
|
+
artifact; older releases remain in the source-only `CHANGELOG-HISTORY.md`.
|
|
16
|
+
Preparation establishes no npm publication.
|
|
16
17
|
|
|
17
18
|
The prepared set requires Zod `^4.6.6` for GHSA-mrhx-c7xc-m3w5.
|
|
18
19
|
Oversized constrained collections fail before per-element errors accumulate;
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* - `ladderTerminatedByClass(failureClass, cadence?)` — whether the REASON for
|
|
12
12
|
* the refusal, rather than the rung reached, ends the ladder.
|
|
13
13
|
* - `isDunningLadderExhausted(cycle, cadence?)` — whether a refused cycle has
|
|
14
|
-
* run past the last rung
|
|
14
|
+
* run past the last rung; exhaustion leaves the subscription active.
|
|
15
15
|
*
|
|
16
16
|
* TERMINATING, not capping. An attempt past the end of the ladder answers
|
|
17
|
-
* `null`, which
|
|
17
|
+
* `null`, which ends automatic retries without pausing the subscription; it never
|
|
18
|
+
* answers "the last slot again".
|
|
18
19
|
* Callers therefore never need a `attempt <= maxRetryAttempts()` guard of their
|
|
19
20
|
* own — asking for the next slot IS the decision, and a second copy of the
|
|
20
21
|
* budget check is exactly how two rails drift apart.
|
|
@@ -76,15 +77,11 @@ export declare function maxRetryAttempts(cadence?: CycleRetryCadence): number;
|
|
|
76
77
|
* Whether a refused cycle has EXHAUSTED the ladder: it failed, nothing is
|
|
77
78
|
* scheduled after it, and the attempt that failed sits past the last rung.
|
|
78
79
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* the same cadence here and to `recordPaymentFailure`. The managed SQL dunning
|
|
85
|
-
* boundary spells the shipped ladder's rung as a fixed constant
|
|
86
|
-
* (`v_ladder_exhausted_from`, 4); a deployment that publishes a different
|
|
87
|
-
* ladder must move that constant with it.
|
|
80
|
+
* Exhaustion stops automatic retries; it never pauses the subscription.
|
|
81
|
+
* An absent schedule alone is not exhaustion: a terminating refusal class can
|
|
82
|
+
* leave no retry on the first rung. Pass the same cadence here and to
|
|
83
|
+
* `recordPaymentFailure`. Legacy expired-dunning recovery also uses this
|
|
84
|
+
* predicate to distinguish an exhausted cycle from an early refusal.
|
|
88
85
|
*
|
|
89
86
|
* @beta
|
|
90
87
|
*/
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* - `ladderTerminatedByClass(failureClass, cadence?)` — whether the REASON for
|
|
12
12
|
* the refusal, rather than the rung reached, ends the ladder.
|
|
13
13
|
* - `isDunningLadderExhausted(cycle, cadence?)` — whether a refused cycle has
|
|
14
|
-
* run past the last rung
|
|
14
|
+
* run past the last rung; exhaustion leaves the subscription active.
|
|
15
15
|
*
|
|
16
16
|
* TERMINATING, not capping. An attempt past the end of the ladder answers
|
|
17
|
-
* `null`, which
|
|
17
|
+
* `null`, which ends automatic retries without pausing the subscription; it never
|
|
18
|
+
* answers "the last slot again".
|
|
18
19
|
* Callers therefore never need a `attempt <= maxRetryAttempts()` guard of their
|
|
19
20
|
* own — asking for the next slot IS the decision, and a second copy of the
|
|
20
21
|
* budget check is exactly how two rails drift apart.
|
|
@@ -99,15 +100,11 @@ export function maxRetryAttempts(cadence = DEFAULT_CYCLE_RETRY_CADENCE) {
|
|
|
99
100
|
* Whether a refused cycle has EXHAUSTED the ladder: it failed, nothing is
|
|
100
101
|
* scheduled after it, and the attempt that failed sits past the last rung.
|
|
101
102
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* the same cadence here and to `recordPaymentFailure`. The managed SQL dunning
|
|
108
|
-
* boundary spells the shipped ladder's rung as a fixed constant
|
|
109
|
-
* (`v_ladder_exhausted_from`, 4); a deployment that publishes a different
|
|
110
|
-
* ladder must move that constant with it.
|
|
103
|
+
* Exhaustion stops automatic retries; it never pauses the subscription.
|
|
104
|
+
* An absent schedule alone is not exhaustion: a terminating refusal class can
|
|
105
|
+
* leave no retry on the first rung. Pass the same cadence here and to
|
|
106
|
+
* `recordPaymentFailure`. Legacy expired-dunning recovery also uses this
|
|
107
|
+
* predicate to distinguish an exhausted cycle from an early refusal.
|
|
111
108
|
*
|
|
112
109
|
* @beta
|
|
113
110
|
*/
|
|
@@ -72,19 +72,11 @@ export declare function reactivateSubscription(input: {
|
|
|
72
72
|
reason?: string;
|
|
73
73
|
}): SubscriptionEngineResult<SubscriptionMutationResult>;
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* {@link isDunningLadderExhausted} holds for the refused cycle. A ladder cut
|
|
81
|
-
* short by a terminating refusal class, or a failure on any rung that still has
|
|
82
|
-
* a retry after it, is refused as `dunning_not_exhausted` and leaves the
|
|
83
|
-
* subscription untouched. A subscription that is not active (already paused,
|
|
84
|
-
* cancelled or completed) is returned unchanged with no event, which is what
|
|
85
|
-
* the managed SQL dunning boundary does and what makes a replay harmless.
|
|
86
|
-
*
|
|
87
|
-
* The schedule is not touched: pausing never moves `nextCycleAt`.
|
|
75
|
+
* Compatibility entry point for an exhausted retry ladder. Exhaustion leaves
|
|
76
|
+
* the subscription unchanged and emits no pause event. The unpaid cycle stays
|
|
77
|
+
* available for authorized recovery; it never starts a new cycle implicitly.
|
|
78
|
+
* Below exhaustion the call still refuses as `dunning_not_exhausted`.
|
|
79
|
+
* Explicit owner pauses and legacy expired-dunning recovery remain separate.
|
|
88
80
|
*
|
|
89
81
|
* @beta
|
|
90
82
|
*/
|
|
@@ -95,7 +87,7 @@ export declare function pauseSubscriptionForExpiredDunning(input: {
|
|
|
95
87
|
cadence?: CycleRetryCadence;
|
|
96
88
|
}): SubscriptionEngineResult<SubscriptionMutationResult>;
|
|
97
89
|
/**
|
|
98
|
-
* Customer-initiated recovery out of
|
|
90
|
+
* Customer-initiated recovery out of a historical non-payment pause.
|
|
99
91
|
*
|
|
100
92
|
* Mirrors the managed SQL `subscription_resume_after_expired_dunning`: the
|
|
101
93
|
* subscription must be paused and the cycle it passes must be the refused cycle
|
|
@@ -140,8 +132,9 @@ export declare const RESTART_LEAD_DAYS = 2;
|
|
|
140
132
|
* sides. `cancelled` -> `active` is the owner's win-back.
|
|
141
133
|
*
|
|
142
134
|
* Which OPERATION may take an edge is narrower than the matrix: `active` ->
|
|
143
|
-
* `paused` is the owner's pause
|
|
144
|
-
* and `cancelled` -> `active` is only
|
|
135
|
+
* `paused` is now the owner's pause; historical non-payment pauses remain stored
|
|
136
|
+
* and use their separate recovery path. `cancelled` -> `active` is only
|
|
137
|
+
* {@link reactivateSubscription}.
|
|
145
138
|
*
|
|
146
139
|
* @beta
|
|
147
140
|
*/
|
|
@@ -130,7 +130,7 @@ export function pauseSubscription(input) {
|
|
|
130
130
|
export function resumeSubscription(input) {
|
|
131
131
|
// Resume leaves `paused` only. Leaving `cancelled` is a win-back and goes
|
|
132
132
|
// through reactivateSubscription, which re-arms the schedule. A subscription
|
|
133
|
-
//
|
|
133
|
+
// a legacy exhausted ladder paused must resume through
|
|
134
134
|
// resumeSubscriptionFromExpiredDunning, which skips the uncollected cycle.
|
|
135
135
|
return changeSubscriptionStatus(input.subscription, "active", input.now, "subscription.resumed", {}, ["paused"]);
|
|
136
136
|
}
|
|
@@ -182,19 +182,11 @@ export function reactivateSubscription(input) {
|
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* {@link isDunningLadderExhausted} holds for the refused cycle. A ladder cut
|
|
191
|
-
* short by a terminating refusal class, or a failure on any rung that still has
|
|
192
|
-
* a retry after it, is refused as `dunning_not_exhausted` and leaves the
|
|
193
|
-
* subscription untouched. A subscription that is not active (already paused,
|
|
194
|
-
* cancelled or completed) is returned unchanged with no event, which is what
|
|
195
|
-
* the managed SQL dunning boundary does and what makes a replay harmless.
|
|
196
|
-
*
|
|
197
|
-
* The schedule is not touched: pausing never moves `nextCycleAt`.
|
|
185
|
+
* Compatibility entry point for an exhausted retry ladder. Exhaustion leaves
|
|
186
|
+
* the subscription unchanged and emits no pause event. The unpaid cycle stays
|
|
187
|
+
* available for authorized recovery; it never starts a new cycle implicitly.
|
|
188
|
+
* Below exhaustion the call still refuses as `dunning_not_exhausted`.
|
|
189
|
+
* Explicit owner pauses and legacy expired-dunning recovery remain separate.
|
|
198
190
|
*
|
|
199
191
|
* @beta
|
|
200
192
|
*/
|
|
@@ -206,32 +198,12 @@ export function pauseSubscriptionForExpiredDunning(input) {
|
|
|
206
198
|
if (mismatch)
|
|
207
199
|
return mismatch;
|
|
208
200
|
if (!isDunningLadderExhausted(input.cycle, input.cadence ?? DEFAULT_CYCLE_RETRY_CADENCE)) {
|
|
209
|
-
return failure("dunning_not_exhausted", "
|
|
210
|
-
}
|
|
211
|
-
if (input.subscription.status !== "active") {
|
|
212
|
-
return success({ subscription: touchSubscription(input.subscription, input.subscription.updatedAt, {}), events: [] });
|
|
201
|
+
return failure("dunning_not_exhausted", "the cycle did not exhaust the retry ladder");
|
|
213
202
|
}
|
|
214
|
-
|
|
215
|
-
return success({
|
|
216
|
-
subscription,
|
|
217
|
-
events: [
|
|
218
|
-
makeEvent({
|
|
219
|
-
eventType: "subscription.paused",
|
|
220
|
-
subscription,
|
|
221
|
-
cycleNumber: input.cycle.cycleNumber,
|
|
222
|
-
idempotencyKey: `${input.cycle.engineIdempotencyKey}:subscription_paused`,
|
|
223
|
-
occurredAt: now.toISOString(),
|
|
224
|
-
payload: {
|
|
225
|
-
reason: "payment_failed_expired",
|
|
226
|
-
retryAttempt: input.cycle.retryAttempt,
|
|
227
|
-
failureReason: input.cycle.failureReason,
|
|
228
|
-
},
|
|
229
|
-
}),
|
|
230
|
-
],
|
|
231
|
-
});
|
|
203
|
+
return success({ subscription: touchSubscription(input.subscription, input.subscription.updatedAt, {}), events: [] });
|
|
232
204
|
}
|
|
233
205
|
/**
|
|
234
|
-
* Customer-initiated recovery out of
|
|
206
|
+
* Customer-initiated recovery out of a historical non-payment pause.
|
|
235
207
|
*
|
|
236
208
|
* Mirrors the managed SQL `subscription_resume_after_expired_dunning`: the
|
|
237
209
|
* subscription must be paused and the cycle it passes must be the refused cycle
|
|
@@ -413,8 +385,9 @@ function cycleOwnershipError(subscription, cycle) {
|
|
|
413
385
|
* sides. `cancelled` -> `active` is the owner's win-back.
|
|
414
386
|
*
|
|
415
387
|
* Which OPERATION may take an edge is narrower than the matrix: `active` ->
|
|
416
|
-
* `paused` is the owner's pause
|
|
417
|
-
* and `cancelled` -> `active` is only
|
|
388
|
+
* `paused` is now the owner's pause; historical non-payment pauses remain stored
|
|
389
|
+
* and use their separate recovery path. `cancelled` -> `active` is only
|
|
390
|
+
* {@link reactivateSubscription}.
|
|
418
391
|
*
|
|
419
392
|
* @beta
|
|
420
393
|
*/
|
|
@@ -137,9 +137,9 @@ there is none. `DEFAULT_CYCLE_RETRY_CADENCE` ships `backoffHours: [24, 72, 168]`
|
|
|
137
137
|
ladder in place, and injectable so a deployment can publish its own.
|
|
138
138
|
|
|
139
139
|
Asking for the next slot **is** the decision. `null` means only that no next
|
|
140
|
-
retry is scheduled;
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
retry is scheduled; neither exhaustion nor class termination pauses the
|
|
141
|
+
subscription. Class termination before exhaustion must not be treated as
|
|
142
|
+
exhaustion. The function never answers "the last
|
|
143
143
|
slot again". Callers must not add their own
|
|
144
144
|
`attempt <= maxRetryAttempts()` guard — a second copy of the budget check is
|
|
145
145
|
exactly how two rails drift apart.
|
|
@@ -158,7 +158,7 @@ listed, recognised class that the decision table already refuses can shorten
|
|
|
158
158
|
the ladder. The SQL retry rail publishes the same singleton, and the parity test
|
|
159
159
|
fails if those two spellings drift.
|
|
160
160
|
|
|
161
|
-
## The status matrix
|
|
161
|
+
## The status matrix and dunning continuity
|
|
162
162
|
|
|
163
163
|
`SUBSCRIPTION_STATUS_TRANSITIONS` is the whole status matrix, frozen:
|
|
164
164
|
|
|
@@ -182,26 +182,23 @@ the end instant and cancellation reason, requires the customer's confirmation of
|
|
|
182
182
|
the charge timing, refuses while a cycle is locked and bumps the template
|
|
183
183
|
version. Those durable duties stay with the host.
|
|
184
184
|
|
|
185
|
-
**The sanctioned non-payment rule.**
|
|
186
|
-
subscription
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
not active unchanged with no event, so a replay is harmless.
|
|
198
|
-
|
|
199
|
-
`resumeSubscriptionFromExpiredDunning` is the customer's recovery. It needs a
|
|
185
|
+
**The sanctioned non-payment rule.** Payment failure never pauses an active
|
|
186
|
+
subscription. `isDunningLadderExhausted(cycle, cadence?)` means the cycle is
|
|
187
|
+
`payment_failed`, has no `nextRetryAt`, and is past the last retry rung.
|
|
188
|
+
Pass the same cadence to payment failure and dunning operations.
|
|
189
|
+
`pauseSubscriptionForExpiredDunning` retains its compatibility name and inputs,
|
|
190
|
+
but now returns an exhausted subscription unchanged with no event. It refuses
|
|
191
|
+
non-exhausted cycles as `dunning_not_exhausted`. Exhaustion ends automatic
|
|
192
|
+
retries; authorized recovery may still pay the same cycle. Managed SQL still
|
|
193
|
+
hard-codes exhaustion at rung 4 in its dunning, recovery and due-list guards;
|
|
194
|
+
a custom core cadence does not configure those durable boundaries.
|
|
195
|
+
|
|
196
|
+
`resumeSubscriptionFromExpiredDunning` is the customer's historical-pause recovery. It needs a
|
|
200
197
|
paused subscription, the exhausted cycle and a payment method, stored or
|
|
201
198
|
supplied. The given cycle becomes `skipped` rather than being charged late.
|
|
202
199
|
Whether the method is chargeable unattended is the host's check, and so is
|
|
203
200
|
skipping any other uncollected cycle the host holds; the SQL boundary does both.
|
|
204
|
-
A subscription
|
|
201
|
+
A subscription the historical rule paused must not go through plain `resumeSubscription`,
|
|
205
202
|
which neither skips the cycle nor moves a stale `nextCycleAt`.
|
|
206
203
|
|
|
207
204
|
Both restarts put `nextCycleAt` `RESTART_LEAD_DAYS` (two) days out, but never
|
package/package.json
CHANGED
package/release-gates.json
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* - `ladderTerminatedByClass(failureClass, cadence?)` — whether the REASON for
|
|
12
12
|
* the refusal, rather than the rung reached, ends the ladder.
|
|
13
13
|
* - `isDunningLadderExhausted(cycle, cadence?)` — whether a refused cycle has
|
|
14
|
-
* run past the last rung
|
|
14
|
+
* run past the last rung; exhaustion leaves the subscription active.
|
|
15
15
|
*
|
|
16
16
|
* TERMINATING, not capping. An attempt past the end of the ladder answers
|
|
17
|
-
* `null`, which
|
|
17
|
+
* `null`, which ends automatic retries without pausing the subscription; it never
|
|
18
|
+
* answers "the last slot again".
|
|
18
19
|
* Callers therefore never need a `attempt <= maxRetryAttempts()` guard of their
|
|
19
20
|
* own — asking for the next slot IS the decision, and a second copy of the
|
|
20
21
|
* budget check is exactly how two rails drift apart.
|
|
@@ -146,15 +147,11 @@ export function maxRetryAttempts(
|
|
|
146
147
|
* Whether a refused cycle has EXHAUSTED the ladder: it failed, nothing is
|
|
147
148
|
* scheduled after it, and the attempt that failed sits past the last rung.
|
|
148
149
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* the same cadence here and to `recordPaymentFailure`. The managed SQL dunning
|
|
155
|
-
* boundary spells the shipped ladder's rung as a fixed constant
|
|
156
|
-
* (`v_ladder_exhausted_from`, 4); a deployment that publishes a different
|
|
157
|
-
* ladder must move that constant with it.
|
|
150
|
+
* Exhaustion stops automatic retries; it never pauses the subscription.
|
|
151
|
+
* An absent schedule alone is not exhaustion: a terminating refusal class can
|
|
152
|
+
* leave no retry on the first rung. Pass the same cadence here and to
|
|
153
|
+
* `recordPaymentFailure`. Legacy expired-dunning recovery also uses this
|
|
154
|
+
* predicate to distinguish an exhausted cycle from an early refusal.
|
|
158
155
|
*
|
|
159
156
|
* @beta
|
|
160
157
|
*/
|
|
@@ -193,7 +193,7 @@ export function resumeSubscription(input: {
|
|
|
193
193
|
}): SubscriptionEngineResult<SubscriptionMutationResult> {
|
|
194
194
|
// Resume leaves `paused` only. Leaving `cancelled` is a win-back and goes
|
|
195
195
|
// through reactivateSubscription, which re-arms the schedule. A subscription
|
|
196
|
-
//
|
|
196
|
+
// a legacy exhausted ladder paused must resume through
|
|
197
197
|
// resumeSubscriptionFromExpiredDunning, which skips the uncollected cycle.
|
|
198
198
|
return changeSubscriptionStatus(input.subscription, "active", input.now, "subscription.resumed", {}, ["paused"]);
|
|
199
199
|
}
|
|
@@ -263,19 +263,11 @@ export function reactivateSubscription(input: {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* {@link isDunningLadderExhausted} holds for the refused cycle. A ladder cut
|
|
272
|
-
* short by a terminating refusal class, or a failure on any rung that still has
|
|
273
|
-
* a retry after it, is refused as `dunning_not_exhausted` and leaves the
|
|
274
|
-
* subscription untouched. A subscription that is not active (already paused,
|
|
275
|
-
* cancelled or completed) is returned unchanged with no event, which is what
|
|
276
|
-
* the managed SQL dunning boundary does and what makes a replay harmless.
|
|
277
|
-
*
|
|
278
|
-
* The schedule is not touched: pausing never moves `nextCycleAt`.
|
|
266
|
+
* Compatibility entry point for an exhausted retry ladder. Exhaustion leaves
|
|
267
|
+
* the subscription unchanged and emits no pause event. The unpaid cycle stays
|
|
268
|
+
* available for authorized recovery; it never starts a new cycle implicitly.
|
|
269
|
+
* Below exhaustion the call still refuses as `dunning_not_exhausted`.
|
|
270
|
+
* Explicit owner pauses and legacy expired-dunning recovery remain separate.
|
|
279
271
|
*
|
|
280
272
|
* @beta
|
|
281
273
|
*/
|
|
@@ -292,35 +284,14 @@ export function pauseSubscriptionForExpiredDunning(input: {
|
|
|
292
284
|
if (!isDunningLadderExhausted(input.cycle, input.cadence ?? DEFAULT_CYCLE_RETRY_CADENCE)) {
|
|
293
285
|
return failure(
|
|
294
286
|
"dunning_not_exhausted",
|
|
295
|
-
"
|
|
287
|
+
"the cycle did not exhaust the retry ladder",
|
|
296
288
|
);
|
|
297
289
|
}
|
|
298
|
-
|
|
299
|
-
return success({ subscription: touchSubscription(input.subscription, input.subscription.updatedAt, {}), events: [] });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const subscription = touchSubscription(input.subscription, now.toISOString(), statusPatch("paused"));
|
|
303
|
-
return success({
|
|
304
|
-
subscription,
|
|
305
|
-
events: [
|
|
306
|
-
makeEvent({
|
|
307
|
-
eventType: "subscription.paused",
|
|
308
|
-
subscription,
|
|
309
|
-
cycleNumber: input.cycle.cycleNumber,
|
|
310
|
-
idempotencyKey: `${input.cycle.engineIdempotencyKey}:subscription_paused`,
|
|
311
|
-
occurredAt: now.toISOString(),
|
|
312
|
-
payload: {
|
|
313
|
-
reason: "payment_failed_expired",
|
|
314
|
-
retryAttempt: input.cycle.retryAttempt,
|
|
315
|
-
failureReason: input.cycle.failureReason,
|
|
316
|
-
},
|
|
317
|
-
}),
|
|
318
|
-
],
|
|
319
|
-
});
|
|
290
|
+
return success({ subscription: touchSubscription(input.subscription, input.subscription.updatedAt, {}), events: [] });
|
|
320
291
|
}
|
|
321
292
|
|
|
322
293
|
/**
|
|
323
|
-
* Customer-initiated recovery out of
|
|
294
|
+
* Customer-initiated recovery out of a historical non-payment pause.
|
|
324
295
|
*
|
|
325
296
|
* Mirrors the managed SQL `subscription_resume_after_expired_dunning`: the
|
|
326
297
|
* subscription must be paused and the cycle it passes must be the refused cycle
|
|
@@ -551,8 +522,9 @@ function cycleOwnershipError(
|
|
|
551
522
|
* sides. `cancelled` -> `active` is the owner's win-back.
|
|
552
523
|
*
|
|
553
524
|
* Which OPERATION may take an edge is narrower than the matrix: `active` ->
|
|
554
|
-
* `paused` is the owner's pause
|
|
555
|
-
* and `cancelled` -> `active` is only
|
|
525
|
+
* `paused` is now the owner's pause; historical non-payment pauses remain stored
|
|
526
|
+
* and use their separate recovery path. `cancelled` -> `active` is only
|
|
527
|
+
* {@link reactivateSubscription}.
|
|
556
528
|
*
|
|
557
529
|
* @beta
|
|
558
530
|
*/
|