@crvouga/mockingbird-service-stripe 1.4.0 → 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 +13 -0
- package/README.md +10 -3
- package/dist/{chunk-P77GCNSW.js → chunk-4YHVIH3S.js} +102 -21
- package/dist/chunk-4YHVIH3S.js.map +7 -0
- package/dist/{chunk-ZSCTOAWQ.js → chunk-RI3PMA5Y.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-P77GCNSW.js.map +0 -7
- /package/dist/{chunk-ZSCTOAWQ.js.map → chunk-RI3PMA5Y.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 1.4.0 (2026-09-29)
|
|
4
17
|
|
|
5
18
|
### Features
|
package/README.md
CHANGED
|
@@ -199,8 +199,12 @@ now and returns `incomplete` on a decline; `error_if_incomplete` fails the call
|
|
|
199
199
|
customer, and paying it through Stripe.js activates the subscription. `trial_end`,
|
|
200
200
|
`backdate_start_date` + `billing_cycle_anchor` + `proration_behavior=none` (a $0 first invoice),
|
|
201
201
|
item updates with `always_invoice` (billed now) or `create_prorations` (next invoice), and
|
|
202
|
-
discounts with stable `di_` ids (`discounts=""` clears) are modelled.
|
|
203
|
-
|
|
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
|
|
204
208
|
`cancellation_details[comment|feedback]`, `prorate` (a credit for the unused time as a pending
|
|
205
209
|
proration) and `invoice_now` (a final invoice; a net credit lands on the customer balance), in the
|
|
206
210
|
query string (as stripe-node sends them) or the body. A $0 invoice is `paid` on
|
|
@@ -381,10 +385,13 @@ plus `port`, `host`; resolves `{url, port, runtime, close}`), `serveTarget` (the
|
|
|
381
385
|
PaymentSheet cannot be redirected (member-app native keeps its fake provider).
|
|
382
386
|
- **Connect** (`Stripe-Account`, application fees, transfers), tax, shipping, Radar, mandates,
|
|
383
387
|
meters, quotes, credit notes, payouts, and non-card payment methods (bank debits, wallets).
|
|
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.
|
|
384
391
|
- **Proration arithmetic** is day-fraction approximate (Stripe prorates to the second);
|
|
385
392
|
`auto_advance` drafts are not finalized an hour later.
|
|
386
393
|
- **Customer portal extras**: the login page (`login_page.url` is not served), `schedule_at_period_end`
|
|
387
|
-
downgrades,
|
|
394
|
+
downgrades, multi-item subscription updates, locales,
|
|
388
395
|
and payment method configurations. Portal sessions do not expire.
|
|
389
396
|
- **Webhook endpoint `api_version`**: payloads render at the account's version, not per endpoint.
|
|
390
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.4.
|
|
1562
|
+
var PACKAGE_VERSION = true ? "1.4.1" : UNRELEASED_VERSION;
|
|
1563
1563
|
|
|
1564
1564
|
// ../core/dist/signing.js
|
|
1565
1565
|
var encoder = new TextEncoder();
|
|
@@ -5488,6 +5488,57 @@ var billProration = (scope, subscription, amount, behavior, options) => {
|
|
|
5488
5488
|
});
|
|
5489
5489
|
return void 0;
|
|
5490
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
|
+
};
|
|
5491
5542
|
var resumeSubscription = (scope, current, options) => {
|
|
5492
5543
|
const now = customerNow(scope, current.customer);
|
|
5493
5544
|
const recurring = recurringOf(scope, current);
|
|
@@ -6755,6 +6806,11 @@ var updateSubscription = (scope, current, params) => {
|
|
|
6755
6806
|
const previousItems = { items: renderSubscription(current, scope.account).items };
|
|
6756
6807
|
const proration = stringOf(params, "proration_behavior") ?? "create_prorations";
|
|
6757
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
|
+
};
|
|
6758
6814
|
const changed = itemRequests.length === 0 ? { itemIds: current.item_ids, prorationAmount: 0 } : changeItems(scope, current, itemRequests, proration);
|
|
6759
6815
|
const itemIds = changed.itemIds;
|
|
6760
6816
|
const prorationAmount = current.status === "trialing" ? 0 : changed.prorationAmount;
|
|
@@ -6815,12 +6871,23 @@ var updateSubscription = (scope, current, params) => {
|
|
|
6815
6871
|
current_period_end: trialEndAt,
|
|
6816
6872
|
...next.status === "active" ? { status: "trialing" } : {}
|
|
6817
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
|
+
};
|
|
6818
6883
|
saveSubscription(scope, current, next, previousItems);
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
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
|
+
});
|
|
6824
6891
|
if (endTrialNow) {
|
|
6825
6892
|
const trialing = scope.account.subscriptions.get(current.id) ?? next;
|
|
6826
6893
|
const ended = { ...trialing, trial_end: now, current_period_end: now };
|
|
@@ -7551,8 +7618,16 @@ var portalChange = (scope, session, config, form) => {
|
|
|
7551
7618
|
throw invalidRequest("Choose a different plan or quantity.");
|
|
7552
7619
|
return { subscription, itemId: item.id, price, quantity };
|
|
7553
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
|
+
};
|
|
7554
7627
|
var previewProration = (scope, config, change) => {
|
|
7555
|
-
|
|
7628
|
+
const prorate = config.features.subscription_update.proration_behavior !== "none";
|
|
7629
|
+
const resets = resetsBillingPeriod(scope, change);
|
|
7630
|
+
if (!prorate && !resets) return 0;
|
|
7556
7631
|
const { subscription } = change;
|
|
7557
7632
|
if (subscription.status === "trialing") return 0;
|
|
7558
7633
|
const item = scope.account.subscriptionItems.get(change.itemId);
|
|
@@ -7560,9 +7635,10 @@ var previewProration = (scope, config, change) => {
|
|
|
7560
7635
|
const now = customerNow(scope, subscription.customer);
|
|
7561
7636
|
const span = Math.max(1, subscription.current_period_end - subscription.current_period_start);
|
|
7562
7637
|
const left = Math.max(0, subscription.current_period_end - now) / span;
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
);
|
|
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);
|
|
7566
7642
|
};
|
|
7567
7643
|
var runPortalAction = (scope, session, form, card2) => {
|
|
7568
7644
|
const config = requireConfiguration(scope, session.configuration);
|
|
@@ -8442,7 +8518,6 @@ var searchRecords = (records, params, options) => {
|
|
|
8442
8518
|
const { query } = params;
|
|
8443
8519
|
if (params.expand === "") throw parameterInvalidEmpty("expand");
|
|
8444
8520
|
if (query === "") throw parameterInvalidEmpty("query");
|
|
8445
|
-
if (params.page === "") throw parameterInvalidEmpty("page");
|
|
8446
8521
|
if (params.limit !== void 0) {
|
|
8447
8522
|
const limit2 = Number(params.limit);
|
|
8448
8523
|
if (limit2 < 1 || limit2 > 100)
|
|
@@ -8453,7 +8528,7 @@ var searchRecords = (records, params, options) => {
|
|
|
8453
8528
|
param: "limit"
|
|
8454
8529
|
});
|
|
8455
8530
|
}
|
|
8456
|
-
if (
|
|
8531
|
+
if (params.page === "") throw parameterInvalidEmpty("page");
|
|
8457
8532
|
if (typeof query !== "string" || query.trim() === "")
|
|
8458
8533
|
throw invalidRequest("Missing required param: query.", "query", "parameter_missing");
|
|
8459
8534
|
const groups = query.split(/\s+OR\s+/).map(parseSearchQuery);
|
|
@@ -8821,7 +8896,18 @@ var priceHandlers = (services) => {
|
|
|
8821
8896
|
return {
|
|
8822
8897
|
PostPrices: async (context) => {
|
|
8823
8898
|
const scope = requestScope(services, context);
|
|
8824
|
-
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
|
+
});
|
|
8825
8911
|
const hasProduct = params.product !== void 0;
|
|
8826
8912
|
const hasProductData = params.product_data !== void 0;
|
|
8827
8913
|
if (hasProduct && hasProductData)
|
|
@@ -8840,11 +8926,6 @@ var priceHandlers = (services) => {
|
|
|
8840
8926
|
const currency = normalizeCurrency(params.currency);
|
|
8841
8927
|
const hasAmount = params.unit_amount !== void 0;
|
|
8842
8928
|
const hasDecimal = params.unit_amount_decimal !== void 0;
|
|
8843
|
-
if (hasAmount && hasDecimal)
|
|
8844
|
-
throw invalidRequest(
|
|
8845
|
-
"You may only specify one of these parameters: unit_amount, unit_amount_decimal.",
|
|
8846
|
-
"unit_amount"
|
|
8847
|
-
);
|
|
8848
8929
|
if (!hasAmount && !hasDecimal)
|
|
8849
8930
|
throw invalidRequest(
|
|
8850
8931
|
"Prices require an `unit_amount` or `unit_amount_decimal` parameter to be set."
|
|
@@ -9589,7 +9670,7 @@ var confirmUpdatePage = (context, change) => {
|
|
|
9589
9670
|
const due = previewProration(scope, config, change);
|
|
9590
9671
|
const currency = after?.currency ?? change.subscription.currency;
|
|
9591
9672
|
const behavior = config.features.subscription_update.proration_behavior;
|
|
9592
|
-
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>`;
|
|
9593
9674
|
return `<h1>Confirm your new plan</h1>
|
|
9594
9675
|
<div class="summary" data-testid="stripe-mock-portal-update-summary">
|
|
9595
9676
|
<div class="row"><span>Current plan</span><span>${escapeHtml2(productName(context, before))} \xB7 ${escapeHtml2(priceLabel(before, current?.quantity ?? 1))}</span></div>
|
|
@@ -11343,7 +11424,7 @@ var requireItem = (scope, id) => {
|
|
|
11343
11424
|
status: 404,
|
|
11344
11425
|
code: "resource_missing",
|
|
11345
11426
|
message: `No such Invoice Item: '${id}'(livemode=false)`,
|
|
11346
|
-
param: "
|
|
11427
|
+
param: "id"
|
|
11347
11428
|
});
|
|
11348
11429
|
return item;
|
|
11349
11430
|
};
|
|
@@ -17321,4 +17402,4 @@ export {
|
|
|
17321
17402
|
createRuntime2 as createRuntime,
|
|
17322
17403
|
StripeAPI
|
|
17323
17404
|
};
|
|
17324
|
-
//# sourceMappingURL=chunk-
|
|
17405
|
+
//# sourceMappingURL=chunk-4YHVIH3S.js.map
|