@blamejs/blamejs-shop 0.4.40 → 0.4.41
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/metered-usage.js +13 -5
- package/lib/plan-changes.js +13 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.41 (2026-06-13) — **Subscription billing no longer double-charges on a retried period close or a concurrent immediate plan change.** Two billing paths could invoice the same charge twice. A metered-usage period close is cron-driven and at-least-once, but it enqueued its roll-up invoice with no idempotency key, so a retried tick — a transient error mid-run, an at-least-once redelivery, or an operator re-running the close — wrote a second invoice for the same period and billed the customer twice. And an immediate plan change recorded its proration adjustment after a guard that only blocked a queued (pending) change, so two concurrent immediate executions both transitioned the subscription and both recorded the proration. The period close now stamps a deterministic per-period idempotency key so a replay returns the existing invoice, and the immediate plan change now transitions the subscription with a conditional update so only the one call that actually moves the plan records the proration. No migration to apply. **Fixed:** *A retried metered-usage period close no longer bills the period twice* — Rolling a usage period into an invoice now carries a deterministic idempotency key derived from the subscription, period window, and currency. A period close that runs more than once — a cron retry, an at-least-once redelivery, or a manual re-run — now returns the invoice already recorded for that period instead of enqueuing a duplicate, so the customer is billed for the period exactly once. · *Concurrent immediate plan changes charge proration once* — Executing an immediate plan change now transitions the subscription with a conditional update keyed on the current plan, and records the proration adjustment only when that update actually moves the plan. The prior guard only refused a queued change, so two immediate executions racing the same change — a double-submit or a retry — could both apply the transition and both record the proration. Now only the one call that wins the transition charges it.
|
|
12
|
+
|
|
11
13
|
- v0.4.40 (2026-06-13) — **A payment webhook whose first delivery fails is reprocessed on redelivery instead of dropping the refund or capture.** The Stripe and PayPal webhook handlers claim an event id for replay suppression the moment the signature verifies — before processing — so a duplicate delivery can never apply a refund or a state change twice. But the claim was kept even when processing then FAILED: a transient database error, a garbled or unparseable refund amount, or an illegal state transition would make the handler return a 5xx so the provider redelivered — and the redelivery found the already-claimed id and was dropped as a replay, permanently losing the refund or the capture-to-paid transition. The claim is now released whenever processing throws, so the provider's redelivery (carrying a recovered payload, or arriving once a transient blip clears) is reprocessed and the refund or capture finally lands. A delivery that SUCCEEDS still keeps its claim, so genuine duplicate deliveries remain suppressed and a refund is never applied twice. No migration to apply. **Fixed:** *Webhook redelivery recovers a refund or capture a failed first delivery would have lost* — Replay suppression on the Stripe and PayPal webhook handlers now reserves the event id, commits the claim only when processing succeeds, and releases it when processing throws. Previously the id was claimed eagerly and kept regardless of outcome, so any first delivery that failed downstream — a transient store error, a refund amount that couldn't be parsed, an out-of-order event that hit an illegal transition — turned the provider's at-least-once redelivery into a silently dropped replay, and the refund or the move to paid never reached the order ledger. The provider's redelivery is now reprocessed, while a delivery that already succeeded still suppresses true duplicates so a refund is never double-applied.
|
|
12
14
|
|
|
13
15
|
- v0.4.39 (2026-06-13) — **Account erasure severs sign-in first and no longer strands a half-erased account when one data domain fails.** Hardening the right-to-erasure flow, which fans a deletion out across every customer-keyed table. Two gaps are closed. The sign-in revocation — passkeys, OAuth links, the email-hash lookup key, and live portal sessions — ran last in the fan-out, so a failure partway through left the supposedly-erased account still able to authenticate. And any single domain handler that threw aborted the entire erasure, leaving every remaining domain untouched with no record of what failed. Now the access-cut runs first, so the account can no longer sign in even if a later step fails; each domain is isolated, so one failure no longer strands the rest; the run reports which domains failed and stays open for retry instead of reporting completion; and re-running converges once the transient clears, since every domain handler is idempotent. No migration to apply. **Fixed:** *Erasure cuts off account sign-in before deleting data* — An erasure request now revokes every sign-in path — passkeys, OAuth identities, the email-hash lookup key, and live portal sessions — as the first step of the fan-out, ahead of the per-table deletions. Previously this revocation ran last, so an erasure that failed partway through left the account's credentials intact and the customer still able to sign in to the anonymized profile. Severing access is erasure's first obligation; data removal follows. · *A single failing domain no longer aborts the whole erasure* — Each customer-keyed domain is now deleted in isolation: a handler that throws is recorded as a failure and the erasure continues to the remaining domains, instead of aborting and stranding everything after it half-erased. The request is marked fulfilled only when every domain succeeds — a partial run stays open and reports exactly which domains failed, so re-running retries just the failures and converges to a clean, complete erasure. The erasure console reflects an incomplete run as needing a re-run rather than reporting success, and every domain handler is idempotent, so the retry removes nothing twice.
|
package/lib/asset-manifest.json
CHANGED
package/lib/metered-usage.js
CHANGED
|
@@ -624,12 +624,20 @@ function create(opts) {
|
|
|
624
624
|
// Skip zero-charge currencies — a period that consumed only
|
|
625
625
|
// included-floor units shouldn't enqueue a $0 invoice line.
|
|
626
626
|
if (amountMinor <= 0) continue;
|
|
627
|
+
// Idempotency key. A period close is cron-driven and at-least-once: a
|
|
628
|
+
// tick that retries (transient error mid-run, redelivery, or an operator
|
|
629
|
+
// re-running the close) MUST NOT enqueue a second invoice for the same
|
|
630
|
+
// [period_start, period_end, currency] — that double-bills the customer.
|
|
631
|
+
// recordInvoice dedupes on processor_invoice_id, so stamp a deterministic
|
|
632
|
+
// id derived from the period identity; a re-run returns the existing row.
|
|
633
|
+
var dedupeId = "metered:" + subscriptionId + ":" + periodStart + ":" + periodEnd + ":" + currency;
|
|
627
634
|
var invoice = await billingHandle.recordInvoice({
|
|
628
|
-
subscription_id:
|
|
629
|
-
period_start:
|
|
630
|
-
period_end:
|
|
631
|
-
amount_minor:
|
|
632
|
-
currency:
|
|
635
|
+
subscription_id: subscriptionId,
|
|
636
|
+
period_start: periodStart,
|
|
637
|
+
period_end: periodEnd,
|
|
638
|
+
amount_minor: amountMinor,
|
|
639
|
+
currency: currency,
|
|
640
|
+
processor_invoice_id: dedupeId,
|
|
633
641
|
});
|
|
634
642
|
invoices.push(invoice);
|
|
635
643
|
}
|
package/lib/plan-changes.js
CHANGED
|
@@ -348,11 +348,20 @@ function create(opts) {
|
|
|
348
348
|
);
|
|
349
349
|
|
|
350
350
|
if (status === "executed") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
// Atomic transition claim. The plan update is conditional on the
|
|
352
|
+
// CURRENT plan id, so two concurrent executeChange calls for the same
|
|
353
|
+
// immediate change can't both apply it — only the call that observes
|
|
354
|
+
// the pre-change plan id transitions the row (rowCount 1); a racing
|
|
355
|
+
// second call sees rowCount 0. The _pendingFor guard above only blocks
|
|
356
|
+
// PENDING changes, so without this an immediate change executed twice
|
|
357
|
+
// (a double-submit or retry) would record the proration invoice twice
|
|
358
|
+
// and double-charge. The proration below is gated on winning the claim.
|
|
359
|
+
var planUpd = await query(
|
|
360
|
+
"UPDATE subscriptions SET plan_id = ?1, updated_at = ?2 WHERE id = ?3 AND plan_id = ?4",
|
|
361
|
+
[newPlanId, ts, subscriptionId, sub.plan_id],
|
|
354
362
|
);
|
|
355
|
-
|
|
363
|
+
var wonTransition = Number((planUpd && planUpd.rowCount) || 0) > 0;
|
|
364
|
+
if (wonTransition && billingHandle && typeof billingHandle.recordInvoice === "function") {
|
|
356
365
|
// Queue the proration adjustment as a single invoice row.
|
|
357
366
|
// The amount is the net (first_charge - credit); negative
|
|
358
367
|
// nets clamp to zero (the credit covers the partial-period
|
package/package.json
CHANGED