@crvouga/mockingbird-service-stripe 1.3.1 → 1.4.1
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 +28 -0
- package/README.md +25 -6
- package/dist/{chunk-E54EAWQ7.js → chunk-4YHVIH3S.js} +185 -27
- package/dist/chunk-4YHVIH3S.js.map +7 -0
- package/dist/{chunk-VNST6CJX.js → chunk-RI3PMA5Y.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +18 -0
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-E54EAWQ7.js.map +0 -7
- /package/dist/{chunk-VNST6CJX.js.map → chunk-RI3PMA5Y.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-stripe
|
|
2
2
|
|
|
3
|
+
## 1.4.1 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
### Fixes and improvements
|
|
6
|
+
|
|
7
|
+
- report a missing invoice item under param id ([c2a5ede](https://github.com/crvouga/mockingbird/commit/c2a5ede5056a6f64d6df128b3f118759d452bc10))
|
|
8
|
+
- validate search limit before an empty page ([cc755f7](https://github.com/crvouga/mockingbird/commit/cc755f7672b4e652bd40c3b087c1fb504b636d85))
|
|
9
|
+
- refuse both price amounts before reading any other parameter ([e6a6735](https://github.com/crvouga/mockingbird/commit/e6a6735ae8bef0e7f9b40152a6e02056497739dd))
|
|
10
|
+
- leave the portal configuration list out of live parity walks ([f378c9d](https://github.com/crvouga/mockingbird/commit/f378c9d76e36f9fdc86343a4c3eeebf5694eb1a0))
|
|
11
|
+
- pick stripe listen's all-events flag from the installed CLI ([89ad2f4](https://github.com/crvouga/mockingbird/commit/89ad2f414745881f1c1f64445c12b8c70f80072d))
|
|
12
|
+
- pass --events to stripe listen so CLI 1.52 starts ([eeb20b2](https://github.com/crvouga/mockingbird/commit/eeb20b2cce57729d817b00d5f8bd9d0681db0ecb))
|
|
13
|
+
- report the stripe listen output when it exits before ready ([0155f0a](https://github.com/crvouga/mockingbird/commit/0155f0acfb8aed6f1677a29339dbeb4ef0bdaaad))
|
|
14
|
+
- reset the billing cycle anchor on a change to a different billing interval ([314910a](https://github.com/crvouga/mockingbird/commit/314910af7ff36ebd900445e97529740639ada4d2))
|
|
15
|
+
|
|
16
|
+
## 1.4.0 (2026-09-29)
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
- retry failed subscription renewals and apply the configured outcome ([c737a68](https://github.com/crvouga/mockingbird/commit/c737a6873a5e9b984b382b9f00c0b01f1712116c))
|
|
21
|
+
|
|
22
|
+
### Fixes and improvements
|
|
23
|
+
|
|
24
|
+
- validate list elements ahead of a schema issue in order ([5a09c88](https://github.com/crvouga/mockingbird/commit/5a09c880a432b4e13e2ef5b519a8a76962d372da))
|
|
25
|
+
- answer a missing SetupIntent with Stripe's 404 resource_missing ([9e8368f](https://github.com/crvouga/mockingbird/commit/9e8368ff2d01303cf5ff5410afc3387c75163ca6))
|
|
26
|
+
- leave the portal configuration list out of live parity walks ([9adfc89](https://github.com/crvouga/mockingbird/commit/9adfc89ed7dbe88d798724deaf9d9ccddea60800))
|
|
27
|
+
- use --all-snapshot with Stripe CLI 1.52 in the webhook oracle ([d95804c](https://github.com/crvouga/mockingbird/commit/d95804cbdf98bcbd2199865dcdd13157d44b1d12))
|
|
28
|
+
- pass --events to stripe listen so Stripe CLI 1.52 starts ([b31d3da](https://github.com/crvouga/mockingbird/commit/b31d3da44396f79630f21f732fd4fb2cc1d3fcc9))
|
|
29
|
+
- report why stripe listen exited in the webhook oracle ([19461e4](https://github.com/crvouga/mockingbird/commit/19461e478a3d0489aac646820ecaad12b2d6ce2a))
|
|
30
|
+
|
|
3
31
|
## 1.3.1 (2026-09-29)
|
|
4
32
|
|
|
5
33
|
### Fixes and improvements
|
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ the mock's hosted page.
|
|
|
86
86
|
|
|
87
87
|
State is partitioned by **account**, and the account is chosen by API key:
|
|
88
88
|
|
|
89
|
-
- `PUT /__admin/accounts {"accounts": [{id, keys, apiVersion?, webhookSecrets?, corpus?, displayName?}]}`
|
|
89
|
+
- `PUT /__admin/accounts {"accounts": [{id, keys, apiVersion?, webhookSecrets?, corpus?, displayName?, billing?}]}`
|
|
90
90
|
(also `createRuntime({accounts})` / `serve --accounts <json|file>`). Every key listed on an
|
|
91
91
|
account acts as it. Any other test key is an account of its own (`accountOfKey(key)`), so an MSO
|
|
92
92
|
key reading a PC object gets Stripe's exact `404 resource_missing` (`No such payment_intent: 'pi_…'`).
|
|
@@ -94,6 +94,8 @@ State is partitioned by **account**, and the account is chosen by API key:
|
|
|
94
94
|
its webhook payloads render at (default `2024-06-20`, what every backend receiver of ours pins).
|
|
95
95
|
- `webhookSecrets: {"<receiver url>": "whsec_…"}` delivers every event of the account there.
|
|
96
96
|
- `corpus: true` seeds the recorded catalog (below).
|
|
97
|
+
- `billing: {retries: {scheduleDays?, afterAllFail?}}` sets the account's failed-renewal retries
|
|
98
|
+
(see Lifecycles and the clock); unset fields use `createRuntime({lifecycle: {paymentRetries}})`.
|
|
97
99
|
|
|
98
100
|
**Namespaces** isolate parallel workers; each namespace has its own copy of every account. Carriers:
|
|
99
101
|
the `x-mockingbird-namespace` header, the `/ns/<namespace>/…` path prefix, or **by API key**:
|
|
@@ -171,6 +173,18 @@ clock-driven lifecycle, so their webhooks fire at once; the served mock also tic
|
|
|
171
173
|
voided (`void`), marked `uncollectible` (`mark_uncollectible`, `invoice.marked_uncollectible`) or
|
|
172
174
|
left as drafts (`keep_as_draft`), never charged; the status holds. `pause_collection[resumes_at]`
|
|
173
175
|
lifts the pause on its own before the renewal it precedes; `pause_collection=""` lifts it now.
|
|
176
|
+
- **Payment retries**: a declined renewal (`charge_automatically`) leaves the invoice `open` with
|
|
177
|
+
`attempt_count: 1` and `next_payment_attempt`, and the subscription `past_due`. Each retry
|
|
178
|
+
`scheduleDays` after the first failure (default `[3, 5, 7]`, Stripe's classic "3 retries over
|
|
179
|
+
1 week") charges again when the clock reaches it: a decline bumps `attempt_count`, emits
|
|
180
|
+
`invoice.payment_failed` and moves `next_payment_attempt`; a working default payment method pays
|
|
181
|
+
the invoice (`invoice.paid`) and the subscription is `active` again
|
|
182
|
+
(`customer.subscription.updated`). After the last retry `next_payment_attempt` is `null` and
|
|
183
|
+
`afterAllFail` applies: `cancel` (the default: `canceled`, `cancellation_details.reason:
|
|
184
|
+
payment_failed`, `customer.subscription.deleted`), `unpaid` (`customer.subscription.updated`) or
|
|
185
|
+
`past_due` (nothing changes). An empty schedule applies it at the first failure. The schedule is
|
|
186
|
+
fixed, not Stripe's ML-timed Smart Retries; set it per account (`billing.retries`, also
|
|
187
|
+
`PUT /__admin/accounts`) or for the runtime (`lifecycle.paymentRetries`). Dunning emails are not sent.
|
|
174
188
|
- `incomplete` subscriptions become `incomplete_expired` after 23 h (their invoice is voided).
|
|
175
189
|
- Checkout Sessions expire at `expires_at` (`checkout.session.expired`).
|
|
176
190
|
- Schedule phases advance; the last one releases or cancels per `end_behavior`.
|
|
@@ -185,8 +199,12 @@ now and returns `incomplete` on a decline; `error_if_incomplete` fails the call
|
|
|
185
199
|
customer, and paying it through Stripe.js activates the subscription. `trial_end`,
|
|
186
200
|
`backdate_start_date` + `billing_cycle_anchor` + `proration_behavior=none` (a $0 first invoice),
|
|
187
201
|
item updates with `always_invoice` (billed now) or `create_prorations` (next invoice), and
|
|
188
|
-
discounts with stable `di_` ids (`discounts=""` clears) are modelled.
|
|
189
|
-
|
|
202
|
+
discounts with stable `di_` ids (`discounts=""` clears) are modelled. An item update to a price with
|
|
203
|
+
a different billing period (say monthly → yearly, through the API or the portal's **Update plan**)
|
|
204
|
+
resets `billing_cycle_anchor` to now, starts a new period and invoices it at once (less a
|
|
205
|
+
proration credit for the unused time unless `proration_behavior=none`); a trialing subscription
|
|
206
|
+
keeps its trial end instead. Items must be active recurring prices sharing one currency and
|
|
207
|
+
interval. `DELETE /v1/subscriptions/:id` takes
|
|
190
208
|
`cancellation_details[comment|feedback]`, `prorate` (a credit for the unused time as a pending
|
|
191
209
|
proration) and `invoice_now` (a final invoice; a net credit lands on the customer balance), in the
|
|
192
210
|
query string (as stripe-node sends them) or the body. A $0 invoice is `paid` on
|
|
@@ -367,12 +385,13 @@ plus `port`, `host`; resolves `{url, port, runtime, close}`), `serveTarget` (the
|
|
|
367
385
|
PaymentSheet cannot be redirected (member-app native keeps its fake provider).
|
|
368
386
|
- **Connect** (`Stripe-Account`, application fees, transfers), tax, shipping, Radar, mandates,
|
|
369
387
|
meters, quotes, credit notes, payouts, and non-card payment methods (bank debits, wallets).
|
|
370
|
-
- **
|
|
371
|
-
`
|
|
388
|
+
- **Billing cycle anchor resets** other than a price change to a different billing period through
|
|
389
|
+
`POST /v1/subscriptions/:id`: an explicit `billing_cycle_anchor=now|unchanged` on update is ignored,
|
|
390
|
+
and the `/v1/subscription_items` endpoints change prices without resetting the period.
|
|
372
391
|
- **Proration arithmetic** is day-fraction approximate (Stripe prorates to the second);
|
|
373
392
|
`auto_advance` drafts are not finalized an hour later.
|
|
374
393
|
- **Customer portal extras**: the login page (`login_page.url` is not served), `schedule_at_period_end`
|
|
375
|
-
downgrades,
|
|
394
|
+
downgrades, multi-item subscription updates, locales,
|
|
376
395
|
and payment method configurations. Portal sessions do not expire.
|
|
377
396
|
- **Webhook endpoint `api_version`**: payloads render at the account's version, not per endpoint.
|
|
378
397
|
- **Live keys** (`sk_live_…`) are refused with Stripe's 401: the mock is test mode only.
|
|
@@ -1559,7 +1559,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1559
1559
|
};
|
|
1560
1560
|
|
|
1561
1561
|
// ../core/dist/version.js
|
|
1562
|
-
var PACKAGE_VERSION = true ? "1.
|
|
1562
|
+
var PACKAGE_VERSION = true ? "1.4.1" : UNRELEASED_VERSION;
|
|
1563
1563
|
|
|
1564
1564
|
// ../core/dist/signing.js
|
|
1565
1565
|
var encoder = new TextEncoder();
|
|
@@ -2583,6 +2583,27 @@ var eraOf = (version) => {
|
|
|
2583
2583
|
};
|
|
2584
2584
|
|
|
2585
2585
|
// src/accounts.ts
|
|
2586
|
+
var DEFAULT_PAYMENT_RETRIES = {
|
|
2587
|
+
scheduleDays: [3, 5, 7],
|
|
2588
|
+
afterAllFail: "cancel"
|
|
2589
|
+
};
|
|
2590
|
+
var RETRY_OUTCOMES = ["cancel", "unpaid", "past_due"];
|
|
2591
|
+
var validateRetries = (id, value) => {
|
|
2592
|
+
const raw = value?.retries;
|
|
2593
|
+
if (typeof value !== "object" || value === null || typeof raw !== "object" || raw === null)
|
|
2594
|
+
return `${id}: billing must be {"retries": {"scheduleDays": [3, 5, 7], "afterAllFail": "cancel"}}`;
|
|
2595
|
+
const { scheduleDays, afterAllFail } = raw;
|
|
2596
|
+
if (scheduleDays !== void 0 && (!Array.isArray(scheduleDays) || scheduleDays.some(
|
|
2597
|
+
(day, index) => typeof day !== "number" || !Number.isFinite(day) || day <= 0 || index > 0 && day <= scheduleDays[index - 1]
|
|
2598
|
+
)))
|
|
2599
|
+
return `${id}: billing.retries.scheduleDays must be increasing positive numbers of days`;
|
|
2600
|
+
if (afterAllFail !== void 0 && !RETRY_OUTCOMES.includes(afterAllFail))
|
|
2601
|
+
return `${id}: billing.retries.afterAllFail must be one of ${RETRY_OUTCOMES.join(", ")}`;
|
|
2602
|
+
return {
|
|
2603
|
+
...scheduleDays !== void 0 ? { scheduleDays } : {},
|
|
2604
|
+
...afterAllFail !== void 0 ? { afterAllFail } : {}
|
|
2605
|
+
};
|
|
2606
|
+
};
|
|
2586
2607
|
var DEFAULT_WEBHOOK_API_VERSION = "2024-06-20";
|
|
2587
2608
|
var KEY = /^(?:sk|rk|pk)_(?:test|live)_[A-Za-z0-9_]+$/;
|
|
2588
2609
|
var validateAccounts = (value) => {
|
|
@@ -2607,8 +2628,11 @@ var validateAccounts = (value) => {
|
|
|
2607
2628
|
const secrets = raw.webhookSecrets;
|
|
2608
2629
|
if (secrets !== void 0 && (typeof secrets !== "object" || secrets === null || Object.values(secrets).some((secret) => typeof secret !== "string")))
|
|
2609
2630
|
return `${raw.id}: webhookSecrets must map receiver URLs to whsec_ secrets`;
|
|
2631
|
+
const retries = raw.billing === void 0 ? void 0 : validateRetries(raw.id, raw.billing);
|
|
2632
|
+
if (typeof retries === "string") return retries;
|
|
2610
2633
|
accounts.push({
|
|
2611
2634
|
id: raw.id,
|
|
2635
|
+
...retries ? { billing: { retries } } : {},
|
|
2612
2636
|
keys: raw.keys,
|
|
2613
2637
|
...typeof raw.apiVersion === "string" ? { apiVersion: raw.apiVersion } : {},
|
|
2614
2638
|
...secrets ? { webhookSecrets: secrets } : {},
|
|
@@ -4875,11 +4899,19 @@ var activateAfterPayment = (scope, subscriptionId, intent) => {
|
|
|
4875
4899
|
};
|
|
4876
4900
|
var markPaymentFailed = (scope, invoice) => {
|
|
4877
4901
|
const now = customerNow(scope, invoice.customer);
|
|
4902
|
+
const attempts = invoice.attempt_count + 1;
|
|
4903
|
+
let next = now + 3 * DAY;
|
|
4904
|
+
if (invoice.subscription !== null) {
|
|
4905
|
+
const days = scope.services.paymentRetries(scope.account.account).scheduleDays;
|
|
4906
|
+
const first = attempts === 1 || invoice.next_payment_attempt === null ? now : invoice.next_payment_attempt - (days[attempts - 2] ?? 0) * DAY;
|
|
4907
|
+
const day = days[attempts - 1];
|
|
4908
|
+
next = day === void 0 ? null : first + day * DAY;
|
|
4909
|
+
}
|
|
4878
4910
|
const failed = {
|
|
4879
4911
|
...invoice,
|
|
4880
|
-
attempt_count:
|
|
4912
|
+
attempt_count: attempts,
|
|
4881
4913
|
attempted: true,
|
|
4882
|
-
next_payment_attempt:
|
|
4914
|
+
next_payment_attempt: next
|
|
4883
4915
|
};
|
|
4884
4916
|
scope.account.invoices.update(failed.id, failed);
|
|
4885
4917
|
scope.emit("invoice.payment_failed", renderInvoice(failed, scope.account));
|
|
@@ -5315,12 +5347,15 @@ var cycleSubscription = (scope, current) => {
|
|
|
5315
5347
|
});
|
|
5316
5348
|
claimInvoiceItems(scope, invoice);
|
|
5317
5349
|
let status = moved.status;
|
|
5350
|
+
let exhausted = false;
|
|
5318
5351
|
if (!keepDraft) {
|
|
5319
5352
|
const finalized = finalizeInvoice(scope, invoice);
|
|
5320
5353
|
const collected = collectCycleInvoice(scope, current, finalized);
|
|
5321
5354
|
invoice = collected.invoice;
|
|
5322
|
-
if (finalized.status === "open" && current.pause_collection === null && finalized.collection_method === "charge_automatically")
|
|
5355
|
+
if (finalized.status === "open" && current.pause_collection === null && finalized.collection_method === "charge_automatically") {
|
|
5323
5356
|
status = collected.failed ? "past_due" : "active";
|
|
5357
|
+
exhausted = collected.failed && invoice.attempt_count > 0 && invoice.next_payment_attempt === null;
|
|
5358
|
+
}
|
|
5324
5359
|
}
|
|
5325
5360
|
const latest = scope.account.subscriptions.get(current.id) ?? moved;
|
|
5326
5361
|
const next = {
|
|
@@ -5334,6 +5369,33 @@ var cycleSubscription = (scope, current) => {
|
|
|
5334
5369
|
const after = renderSubscription(next, scope.account);
|
|
5335
5370
|
const changed = changedFields(before, after);
|
|
5336
5371
|
scope.emit("customer.subscription.updated", after, changed);
|
|
5372
|
+
if (exhausted) applyFailedPaymentOutcome(scope, next);
|
|
5373
|
+
};
|
|
5374
|
+
var applyFailedPaymentOutcome = (scope, current) => {
|
|
5375
|
+
const outcome = scope.services.paymentRetries(scope.account.account).afterAllFail;
|
|
5376
|
+
if (outcome === "cancel") cancelSubscription(scope, current, "payment_failed");
|
|
5377
|
+
else if (outcome === "unpaid") saveSubscription(scope, current, { ...current, status: "unpaid" });
|
|
5378
|
+
};
|
|
5379
|
+
var retryDueInvoices = (scope, subscription, now) => {
|
|
5380
|
+
for (let guard2 = 0; guard2 < 50; guard2 += 1) {
|
|
5381
|
+
const due = scope.account.invoices.list({
|
|
5382
|
+
order: "oldest",
|
|
5383
|
+
where: (invoice) => invoice.subscription === subscription.id && invoice.status === "open" && invoice.collection_method === "charge_automatically" && invoice.next_payment_attempt !== null && invoice.next_payment_attempt <= now
|
|
5384
|
+
})[0]?.value;
|
|
5385
|
+
if (due === void 0) return;
|
|
5386
|
+
let settled;
|
|
5387
|
+
try {
|
|
5388
|
+
settled = payInvoice(scope, due, { offSession: true });
|
|
5389
|
+
} catch (error) {
|
|
5390
|
+
if (!(error instanceof StripeError)) throw error;
|
|
5391
|
+
settled = scope.account.invoices.get(due.id) ?? due;
|
|
5392
|
+
}
|
|
5393
|
+
const current = scope.account.subscriptions.get(subscription.id);
|
|
5394
|
+
if (settled.status !== "open" || current === void 0) return;
|
|
5395
|
+
if (settled.next_payment_attempt === due.next_payment_attempt) return;
|
|
5396
|
+
if (settled.next_payment_attempt === null) applyFailedPaymentOutcome(scope, current);
|
|
5397
|
+
if (settled.next_payment_attempt === null || current.status !== "past_due") return;
|
|
5398
|
+
}
|
|
5337
5399
|
};
|
|
5338
5400
|
var periodAmount = (scope, subscription, at) => {
|
|
5339
5401
|
const lines = subscriptionItems(scope, subscription).map((item) => {
|
|
@@ -5426,6 +5488,57 @@ var billProration = (scope, subscription, amount, behavior, options) => {
|
|
|
5426
5488
|
});
|
|
5427
5489
|
return void 0;
|
|
5428
5490
|
};
|
|
5491
|
+
var sameBillingPeriod = (a, b) => a.interval === b.interval && a.interval_count === b.interval_count;
|
|
5492
|
+
var invoiceBillingCycleReset = (scope, subscription, previous, prorate) => {
|
|
5493
|
+
const period = { start: subscription.current_period_start, end: subscription.current_period_end };
|
|
5494
|
+
const left = Math.max(0, previous.period.end - period.start) / (previous.period.end - previous.period.start || 1);
|
|
5495
|
+
const credits = prorate ? previous.items.map((item) => {
|
|
5496
|
+
const price = requirePrice(scope, item.price);
|
|
5497
|
+
return {
|
|
5498
|
+
...lineFromPrice(
|
|
5499
|
+
scope,
|
|
5500
|
+
price,
|
|
5501
|
+
item.quantity ?? 1,
|
|
5502
|
+
{ start: period.start, end: previous.period.end },
|
|
5503
|
+
{
|
|
5504
|
+
subscription: subscription.id,
|
|
5505
|
+
subscriptionItem: item.id,
|
|
5506
|
+
amount: -Math.round(priceAmount(price, item.quantity ?? 1) * left),
|
|
5507
|
+
description: "Unused time on the previous price (prorated)"
|
|
5508
|
+
}
|
|
5509
|
+
),
|
|
5510
|
+
proration: true
|
|
5511
|
+
};
|
|
5512
|
+
}) : [];
|
|
5513
|
+
const draft = createDraftInvoice(scope, {
|
|
5514
|
+
customer: subscription.customer,
|
|
5515
|
+
subscription: subscription.id,
|
|
5516
|
+
lines: [
|
|
5517
|
+
...credits.filter((line) => line.amount !== 0),
|
|
5518
|
+
...cycleLines(scope, subscription, period)
|
|
5519
|
+
],
|
|
5520
|
+
billingReason: "subscription_update",
|
|
5521
|
+
discountIds: liveDiscounts(scope, subscription.discount_ids, period.start),
|
|
5522
|
+
period,
|
|
5523
|
+
collectionMethod: subscription.collection_method,
|
|
5524
|
+
daysUntilDue: subscription.days_until_due,
|
|
5525
|
+
subscriptionMetadata: subscription.metadata
|
|
5526
|
+
});
|
|
5527
|
+
claimInvoiceItems(scope, draft);
|
|
5528
|
+
const finalized = finalizeInvoice(scope, draft);
|
|
5529
|
+
creditNegativeInvoice(scope, finalized);
|
|
5530
|
+
const collected = collectCycleInvoice(scope, subscription, finalized);
|
|
5531
|
+
const latest = scope.account.subscriptions.get(subscription.id) ?? subscription;
|
|
5532
|
+
const charged = finalized.status === "open" && !subscription.pause_collection && finalized.collection_method === "charge_automatically";
|
|
5533
|
+
const next = {
|
|
5534
|
+
...latest,
|
|
5535
|
+
status: charged ? collected.failed ? "past_due" : "active" : latest.status,
|
|
5536
|
+
latest_invoice: collected.invoice.id
|
|
5537
|
+
};
|
|
5538
|
+
saveSubscription(scope, latest, next);
|
|
5539
|
+
if (charged && collected.failed && collected.invoice.attempt_count > 0 && collected.invoice.next_payment_attempt === null)
|
|
5540
|
+
applyFailedPaymentOutcome(scope, next);
|
|
5541
|
+
};
|
|
5429
5542
|
var resumeSubscription = (scope, current, options) => {
|
|
5430
5543
|
const now = customerNow(scope, current.customer);
|
|
5431
5544
|
const recurring = recurringOf(scope, current);
|
|
@@ -5744,7 +5857,8 @@ var releaseSchedule = (scope, schedule, now) => {
|
|
|
5744
5857
|
var DEFAULT_LIFECYCLE = {
|
|
5745
5858
|
upcomingInvoiceDays: 3,
|
|
5746
5859
|
trialWillEndDays: 3,
|
|
5747
|
-
incompleteExpiryHours: 23
|
|
5860
|
+
incompleteExpiryHours: 23,
|
|
5861
|
+
paymentRetries: DEFAULT_PAYMENT_RETRIES
|
|
5748
5862
|
};
|
|
5749
5863
|
var runLifecycle = (scope, settings, onlyCustomers) => {
|
|
5750
5864
|
const inScope = (customer) => onlyCustomers === void 0 ? customer === null || !findCustomer(scope, customer)?.test_clock : customer !== null && onlyCustomers.has(customer);
|
|
@@ -5778,6 +5892,10 @@ var runLifecycle = (scope, settings, onlyCustomers) => {
|
|
|
5778
5892
|
);
|
|
5779
5893
|
}
|
|
5780
5894
|
for (let guard2 = 0; guard2 < 120; guard2 += 1) {
|
|
5895
|
+
if (subscription.status === "past_due") {
|
|
5896
|
+
retryDueInvoices(scope, subscription, now2);
|
|
5897
|
+
subscription = scope.account.subscriptions.get(subscription.id) ?? subscription;
|
|
5898
|
+
}
|
|
5781
5899
|
if (!["active", "trialing", "past_due"].includes(subscription.status)) break;
|
|
5782
5900
|
const cancelAt = subscription.cancel_at;
|
|
5783
5901
|
if (cancelAt !== null && cancelAt <= now2) {
|
|
@@ -6461,7 +6579,13 @@ var parseParams = (schema, raw, options = {}) => {
|
|
|
6461
6579
|
if (error) throw error;
|
|
6462
6580
|
}
|
|
6463
6581
|
const issue = issues[0];
|
|
6464
|
-
if (issue)
|
|
6582
|
+
if (issue) {
|
|
6583
|
+
const failing = /^[^[]+\[(\d+)\]/.exec(issue.path)?.[1];
|
|
6584
|
+
const list = params[key];
|
|
6585
|
+
if (failing !== void 0 && Number(failing) > 0 && Array.isArray(list))
|
|
6586
|
+
options.validate?.[key]?.({ ...params, [key]: list.slice(0, Number(failing)) });
|
|
6587
|
+
throw issueToError(issue);
|
|
6588
|
+
}
|
|
6465
6589
|
options.validate?.[key]?.(params);
|
|
6466
6590
|
}
|
|
6467
6591
|
const stray = parsed.issues[0];
|
|
@@ -6682,6 +6806,11 @@ var updateSubscription = (scope, current, params) => {
|
|
|
6682
6806
|
const previousItems = { items: renderSubscription(current, scope.account).items };
|
|
6683
6807
|
const proration = stringOf(params, "proration_behavior") ?? "create_prorations";
|
|
6684
6808
|
const itemRequests = requestedItems(params);
|
|
6809
|
+
const previousBilling = {
|
|
6810
|
+
items: subscriptionItems(scope, current),
|
|
6811
|
+
recurring: recurringOf(scope, current),
|
|
6812
|
+
period: { start: current.current_period_start, end: current.current_period_end }
|
|
6813
|
+
};
|
|
6685
6814
|
const changed = itemRequests.length === 0 ? { itemIds: current.item_ids, prorationAmount: 0 } : changeItems(scope, current, itemRequests, proration);
|
|
6686
6815
|
const itemIds = changed.itemIds;
|
|
6687
6816
|
const prorationAmount = current.status === "trialing" ? 0 : changed.prorationAmount;
|
|
@@ -6742,12 +6871,23 @@ var updateSubscription = (scope, current, params) => {
|
|
|
6742
6871
|
current_period_end: trialEndAt,
|
|
6743
6872
|
...next.status === "active" ? { status: "trialing" } : {}
|
|
6744
6873
|
};
|
|
6874
|
+
const nextRecurring = recurringOf(scope, next);
|
|
6875
|
+
const resetsAnchor = itemRequests.length > 0 && trialEndAt === void 0 && (current.status === "active" || current.status === "past_due") && !sameBillingPeriod(previousBilling.recurring, nextRecurring);
|
|
6876
|
+
if (resetsAnchor)
|
|
6877
|
+
next = {
|
|
6878
|
+
...next,
|
|
6879
|
+
billing_cycle_anchor: now,
|
|
6880
|
+
current_period_start: now,
|
|
6881
|
+
current_period_end: addInterval(now, nextRecurring.interval, nextRecurring.interval_count)
|
|
6882
|
+
};
|
|
6745
6883
|
saveSubscription(scope, current, next, previousItems);
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6884
|
+
if (resetsAnchor) invoiceBillingCycleReset(scope, next, previousBilling, proration !== "none");
|
|
6885
|
+
else
|
|
6886
|
+
billProration(scope, next, prorationAmount, proration, {
|
|
6887
|
+
period: { start: now, end: current.current_period_end },
|
|
6888
|
+
description: proration === "always_invoice" ? "Remaining time on the new price (prorated)" : "Proration for subscription change",
|
|
6889
|
+
failOnDecline: stringOf(params, "payment_behavior") === "error_if_incomplete"
|
|
6890
|
+
});
|
|
6751
6891
|
if (endTrialNow) {
|
|
6752
6892
|
const trialing = scope.account.subscriptions.get(current.id) ?? next;
|
|
6753
6893
|
const ended = { ...trialing, trial_end: now, current_period_end: now };
|
|
@@ -7478,8 +7618,16 @@ var portalChange = (scope, session, config, form) => {
|
|
|
7478
7618
|
throw invalidRequest("Choose a different plan or quantity.");
|
|
7479
7619
|
return { subscription, itemId: item.id, price, quantity };
|
|
7480
7620
|
};
|
|
7621
|
+
var resetsBillingPeriod = (scope, change) => {
|
|
7622
|
+
const { subscription } = change;
|
|
7623
|
+
if (subscription.status === "trialing") return false;
|
|
7624
|
+
const price = scope.account.prices.get(change.price)?.recurring;
|
|
7625
|
+
return price != null && !sameBillingPeriod(recurringOf(scope, subscription), price);
|
|
7626
|
+
};
|
|
7481
7627
|
var previewProration = (scope, config, change) => {
|
|
7482
|
-
|
|
7628
|
+
const prorate = config.features.subscription_update.proration_behavior !== "none";
|
|
7629
|
+
const resets = resetsBillingPeriod(scope, change);
|
|
7630
|
+
if (!prorate && !resets) return 0;
|
|
7483
7631
|
const { subscription } = change;
|
|
7484
7632
|
if (subscription.status === "trialing") return 0;
|
|
7485
7633
|
const item = scope.account.subscriptionItems.get(change.itemId);
|
|
@@ -7487,9 +7635,10 @@ var previewProration = (scope, config, change) => {
|
|
|
7487
7635
|
const now = customerNow(scope, subscription.customer);
|
|
7488
7636
|
const span = Math.max(1, subscription.current_period_end - subscription.current_period_start);
|
|
7489
7637
|
const left = Math.max(0, subscription.current_period_end - now) / span;
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
);
|
|
7638
|
+
const next = amount(change.price, change.quantity);
|
|
7639
|
+
const previous = amount(item?.price ?? change.price, item?.quantity ?? 1);
|
|
7640
|
+
if (resets) return next - (prorate ? Math.round(previous * left) : 0);
|
|
7641
|
+
return Math.round((next - previous) * left);
|
|
7493
7642
|
};
|
|
7494
7643
|
var runPortalAction = (scope, session, form, card2) => {
|
|
7495
7644
|
const config = requireConfiguration(scope, session.configuration);
|
|
@@ -8369,7 +8518,6 @@ var searchRecords = (records, params, options) => {
|
|
|
8369
8518
|
const { query } = params;
|
|
8370
8519
|
if (params.expand === "") throw parameterInvalidEmpty("expand");
|
|
8371
8520
|
if (query === "") throw parameterInvalidEmpty("query");
|
|
8372
|
-
if (params.page === "") throw parameterInvalidEmpty("page");
|
|
8373
8521
|
if (params.limit !== void 0) {
|
|
8374
8522
|
const limit2 = Number(params.limit);
|
|
8375
8523
|
if (limit2 < 1 || limit2 > 100)
|
|
@@ -8380,7 +8528,7 @@ var searchRecords = (records, params, options) => {
|
|
|
8380
8528
|
param: "limit"
|
|
8381
8529
|
});
|
|
8382
8530
|
}
|
|
8383
|
-
if (
|
|
8531
|
+
if (params.page === "") throw parameterInvalidEmpty("page");
|
|
8384
8532
|
if (typeof query !== "string" || query.trim() === "")
|
|
8385
8533
|
throw invalidRequest("Missing required param: query.", "query", "parameter_missing");
|
|
8386
8534
|
const groups = query.split(/\s+OR\s+/).map(parseSearchQuery);
|
|
@@ -8748,7 +8896,18 @@ var priceHandlers = (services) => {
|
|
|
8748
8896
|
return {
|
|
8749
8897
|
PostPrices: async (context) => {
|
|
8750
8898
|
const scope = requestScope(services, context);
|
|
8751
|
-
const params = bodyParams(context
|
|
8899
|
+
const params = bodyParams(context, {
|
|
8900
|
+
order: ["unit_amount", "unit_amount_decimal"],
|
|
8901
|
+
validate: {
|
|
8902
|
+
unit_amount_decimal: (given) => {
|
|
8903
|
+
if (given.unit_amount !== void 0 && given.unit_amount_decimal !== void 0)
|
|
8904
|
+
throw invalidRequest(
|
|
8905
|
+
"You may only specify one of these parameters: unit_amount, unit_amount_decimal.",
|
|
8906
|
+
"unit_amount"
|
|
8907
|
+
);
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
});
|
|
8752
8911
|
const hasProduct = params.product !== void 0;
|
|
8753
8912
|
const hasProductData = params.product_data !== void 0;
|
|
8754
8913
|
if (hasProduct && hasProductData)
|
|
@@ -8767,11 +8926,6 @@ var priceHandlers = (services) => {
|
|
|
8767
8926
|
const currency = normalizeCurrency(params.currency);
|
|
8768
8927
|
const hasAmount = params.unit_amount !== void 0;
|
|
8769
8928
|
const hasDecimal = params.unit_amount_decimal !== void 0;
|
|
8770
|
-
if (hasAmount && hasDecimal)
|
|
8771
|
-
throw invalidRequest(
|
|
8772
|
-
"You may only specify one of these parameters: unit_amount, unit_amount_decimal.",
|
|
8773
|
-
"unit_amount"
|
|
8774
|
-
);
|
|
8775
8929
|
if (!hasAmount && !hasDecimal)
|
|
8776
8930
|
throw invalidRequest(
|
|
8777
8931
|
"Prices require an `unit_amount` or `unit_amount_decimal` parameter to be set."
|
|
@@ -9516,7 +9670,7 @@ var confirmUpdatePage = (context, change) => {
|
|
|
9516
9670
|
const due = previewProration(scope, config, change);
|
|
9517
9671
|
const currency = after?.currency ?? change.subscription.currency;
|
|
9518
9672
|
const behavior = config.features.subscription_update.proration_behavior;
|
|
9519
|
-
const dueLine = behavior === "always_invoice" ? due < 0 ? `<div class="row total"><span>Credit to your balance</span><span data-testid="stripe-mock-portal-amount-due">\u2212${formatMoney(-due, currency)}</span></div>` : `<div class="row total"><span>Amount due today</span><span data-testid="stripe-mock-portal-amount-due">${formatMoney(due, currency)}</span></div>` : `<div class="row total"><span>Adjustment on your next invoice</span><span data-testid="stripe-mock-portal-amount-due">${due < 0 ? "\u2212" : ""}${formatMoney(Math.abs(due), currency)}</span></div>`;
|
|
9673
|
+
const dueLine = behavior === "always_invoice" || resetsBillingPeriod(scope, change) ? due < 0 ? `<div class="row total"><span>Credit to your balance</span><span data-testid="stripe-mock-portal-amount-due">\u2212${formatMoney(-due, currency)}</span></div>` : `<div class="row total"><span>Amount due today</span><span data-testid="stripe-mock-portal-amount-due">${formatMoney(due, currency)}</span></div>` : `<div class="row total"><span>Adjustment on your next invoice</span><span data-testid="stripe-mock-portal-amount-due">${due < 0 ? "\u2212" : ""}${formatMoney(Math.abs(due), currency)}</span></div>`;
|
|
9520
9674
|
return `<h1>Confirm your new plan</h1>
|
|
9521
9675
|
<div class="summary" data-testid="stripe-mock-portal-update-summary">
|
|
9522
9676
|
<div class="row"><span>Current plan</span><span>${escapeHtml2(productName(context, before))} \xB7 ${escapeHtml2(priceLabel(before, current?.quantity ?? 1))}</span></div>
|
|
@@ -9851,7 +10005,7 @@ var paymentIntentHandlers = (services) => {
|
|
|
9851
10005
|
// src/setup-intents.ts
|
|
9852
10006
|
var requireSetupIntent = (scope, id) => {
|
|
9853
10007
|
const intent = scope.account.setupIntents.get(id);
|
|
9854
|
-
if (!intent) throw
|
|
10008
|
+
if (!intent) throw resourceMissing("setupintent", id, "intent");
|
|
9855
10009
|
return intent;
|
|
9856
10010
|
};
|
|
9857
10011
|
var confirmSetup = (scope, intent, method, autoAuthenticate = false) => {
|
|
@@ -11270,7 +11424,7 @@ var requireItem = (scope, id) => {
|
|
|
11270
11424
|
status: 404,
|
|
11271
11425
|
code: "resource_missing",
|
|
11272
11426
|
message: `No such Invoice Item: '${id}'(livemode=false)`,
|
|
11273
|
-
param: "
|
|
11427
|
+
param: "id"
|
|
11274
11428
|
});
|
|
11275
11429
|
return item;
|
|
11276
11430
|
};
|
|
@@ -16942,6 +17096,10 @@ var StripeAPI = class {
|
|
|
16942
17096
|
publish: options.onWebhook,
|
|
16943
17097
|
accounts,
|
|
16944
17098
|
deliveryVersion: (account) => accounts.config(account)?.apiVersion ?? webhookVersion,
|
|
17099
|
+
paymentRetries: (account) => ({
|
|
17100
|
+
...this.settings.paymentRetries,
|
|
17101
|
+
...accounts.config(account)?.billing?.retries
|
|
17102
|
+
}),
|
|
16945
17103
|
pendingWebhooks: options.pendingWebhooks ?? (() => 0),
|
|
16946
17104
|
namespacePrefix: options.publicNamespace === void 0 || options.publicNamespace === "default" ? "" : `/ns/${encodeURIComponent(options.publicNamespace)}`,
|
|
16947
17105
|
publicUrl: options.publicUrl,
|
|
@@ -17244,4 +17402,4 @@ export {
|
|
|
17244
17402
|
createRuntime2 as createRuntime,
|
|
17245
17403
|
StripeAPI
|
|
17246
17404
|
};
|
|
17247
|
-
//# sourceMappingURL=chunk-
|
|
17405
|
+
//# sourceMappingURL=chunk-4YHVIH3S.js.map
|