@blamejs/blamejs-shop 0.4.20 → 0.4.21
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/collections.js +282 -14
- package/lib/gift-card-ledger.js +37 -8
- package/lib/gift-registry.js +43 -5
- package/lib/loyalty-earn-rules.js +106 -0
- package/lib/loyalty.js +63 -30
- package/lib/order.js +24 -0
- package/lib/search-ranking.js +58 -2
- package/lib/store-credit.js +31 -19
- package/lib/storefront.js +110 -19
- package/lib/subscription-controls.js +113 -0
- package/package.json +1 -1
|
@@ -242,6 +242,77 @@ function create(opts) {
|
|
|
242
242
|
if (!subscriptionsHandle || typeof subscriptionsHandle.get !== "function") {
|
|
243
243
|
throw new TypeError("subscriptionControls.create: opts.subscriptions handle required");
|
|
244
244
|
}
|
|
245
|
+
// Optional payment handle (the shared Stripe adapter). When wired,
|
|
246
|
+
// quantity changes on a Stripe-backed subscription are pushed to
|
|
247
|
+
// Stripe BEFORE the local row is touched, so the row the shop shows
|
|
248
|
+
// and the quantity Stripe actually bills never diverge. Without it
|
|
249
|
+
// (a deploy with no payment processor, or a non-Stripe row), the
|
|
250
|
+
// controls stay local-only. The handle must expose
|
|
251
|
+
// `subscriptions.retrieve(id)` + `subscriptions.update(id, body)`.
|
|
252
|
+
var payment = opts.payment || null;
|
|
253
|
+
var hasStripe = !!(payment && payment.subscriptions &&
|
|
254
|
+
typeof payment.subscriptions.update === "function" &&
|
|
255
|
+
typeof payment.subscriptions.retrieve === "function");
|
|
256
|
+
|
|
257
|
+
// A row is Stripe-backed when the processor adapter is wired AND the
|
|
258
|
+
// row carries the upstream subscription id the webhook + billing
|
|
259
|
+
// mirror key on. Rows without one are shop-local (e.g. a manually
|
|
260
|
+
// seeded subscription on a deploy that never reached Stripe) and the
|
|
261
|
+
// controls mutate only local columns for them.
|
|
262
|
+
function _isStripeBacked(row) {
|
|
263
|
+
return hasStripe && row && typeof row.stripe_subscription_id === "string" && row.stripe_subscription_id.length > 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Push a new line quantity to Stripe for a Stripe-backed subscription.
|
|
267
|
+
// Stripe models quantity on the subscription ITEM, not the
|
|
268
|
+
// subscription, so we retrieve the live subscription to find its
|
|
269
|
+
// (single) item id, then update that item's quantity. Every
|
|
270
|
+
// subscription this shop creates binds exactly one price
|
|
271
|
+
// (`items: [{ price }]`), so the first item is authoritative; if a
|
|
272
|
+
// subscription somehow carries no item we surface a structured error
|
|
273
|
+
// rather than silently writing a local-only change that diverges from
|
|
274
|
+
// Stripe. The idempotency key folds in the subscription id + target
|
|
275
|
+
// quantity so a retried call is a safe no-op at Stripe.
|
|
276
|
+
async function _pushQuantityToStripe(stripeSubscriptionId, newQuantity) {
|
|
277
|
+
var live;
|
|
278
|
+
try {
|
|
279
|
+
live = await payment.subscriptions.retrieve(stripeSubscriptionId);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
var rErr = new Error(
|
|
282
|
+
"subscriptionControls.changeQuantity: could not reach Stripe to update the subscription — " + (e && e.message || e),
|
|
283
|
+
);
|
|
284
|
+
rErr.code = "SUBSCRIPTION_STRIPE_PUSH_FAILED";
|
|
285
|
+
rErr.cause = e;
|
|
286
|
+
throw rErr;
|
|
287
|
+
}
|
|
288
|
+
var items = live && live.items && Array.isArray(live.items.data) ? live.items.data : [];
|
|
289
|
+
if (!items.length || !items[0] || !items[0].id) {
|
|
290
|
+
var noItem = new Error(
|
|
291
|
+
"subscriptionControls.changeQuantity: Stripe subscription " + stripeSubscriptionId + " has no billable item to update",
|
|
292
|
+
);
|
|
293
|
+
noItem.code = "SUBSCRIPTION_STRIPE_NO_ITEM";
|
|
294
|
+
throw noItem;
|
|
295
|
+
}
|
|
296
|
+
var idemKey = "subctl:qty:" + stripeSubscriptionId + ":" + newQuantity;
|
|
297
|
+
try {
|
|
298
|
+
return await payment.subscriptions.update(
|
|
299
|
+
stripeSubscriptionId,
|
|
300
|
+
{ items: [{ id: items[0].id, quantity: newQuantity }] },
|
|
301
|
+
idemKey,
|
|
302
|
+
);
|
|
303
|
+
} catch (e2) {
|
|
304
|
+
// The processor rejected or failed the quantity update. Wrap it in
|
|
305
|
+
// a stable code so the route can surface a "nothing changed, retry"
|
|
306
|
+
// notice; the local row is still untouched (this runs before the
|
|
307
|
+
// local write).
|
|
308
|
+
var uErr = new Error(
|
|
309
|
+
"subscriptionControls.changeQuantity: Stripe rejected the quantity update — " + (e2 && e2.message || e2),
|
|
310
|
+
);
|
|
311
|
+
uErr.code = "SUBSCRIPTION_STRIPE_PUSH_FAILED";
|
|
312
|
+
uErr.cause = e2;
|
|
313
|
+
throw uErr;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
245
316
|
|
|
246
317
|
// Fetch the raw subscription row + bound plan in one round-trip,
|
|
247
318
|
// returning `null` if either is absent. The plan lookup feeds the
|
|
@@ -317,6 +388,14 @@ function create(opts) {
|
|
|
317
388
|
MAX_SKIP_COUNT: MAX_SKIP_COUNT,
|
|
318
389
|
MAX_QUANTITY: MAX_QUANTITY,
|
|
319
390
|
REACTIVATE_GRACE_MS: REACTIVATE_GRACE_MS,
|
|
391
|
+
// True when this instance can push changes to Stripe (a payment
|
|
392
|
+
// handle with retrieve + update was wired). Callers use it to decide
|
|
393
|
+
// whether a row with a stripe_subscription_id is genuinely Stripe-
|
|
394
|
+
// backed — a frequency change is refused (immutable interval) and a
|
|
395
|
+
// quantity change round-trips Stripe. Without it, the controls are
|
|
396
|
+
// local-only even for a Stripe-shaped row, so the storefront keeps
|
|
397
|
+
// offering the frequency control.
|
|
398
|
+
stripeBacked: hasStripe,
|
|
320
399
|
|
|
321
400
|
pause: async function (input) {
|
|
322
401
|
if (!input || typeof input !== "object") {
|
|
@@ -462,6 +541,18 @@ function create(opts) {
|
|
|
462
541
|
throw cErr;
|
|
463
542
|
}
|
|
464
543
|
|
|
544
|
+
// Stripe-backed: push the new quantity to Stripe BEFORE the local
|
|
545
|
+
// write. If the processor rejects the update, we throw and leave
|
|
546
|
+
// the local row untouched — the customer sees an honest error and
|
|
547
|
+
// the shop's `quantity` column never diverges from what Stripe
|
|
548
|
+
// actually bills (the divergence this control surface previously
|
|
549
|
+
// produced, writing a local "Quantity updated." while Stripe kept
|
|
550
|
+
// charging the old quantity). The local write only lands after the
|
|
551
|
+
// Stripe update succeeds.
|
|
552
|
+
if (_isStripeBacked(row)) {
|
|
553
|
+
await _pushQuantityToStripe(row.stripe_subscription_id, newQuantity);
|
|
554
|
+
}
|
|
555
|
+
|
|
465
556
|
var before = _snapshot(row);
|
|
466
557
|
var ts = _now();
|
|
467
558
|
await query(
|
|
@@ -495,6 +586,28 @@ function create(opts) {
|
|
|
495
586
|
throw cErr;
|
|
496
587
|
}
|
|
497
588
|
|
|
589
|
+
// Stripe-backed: refuse. A Stripe Price's `recurring.interval` is
|
|
590
|
+
// IMMUTABLE — you cannot re-cadence an existing price, only swap
|
|
591
|
+
// the subscription item to a DIFFERENT price with the desired
|
|
592
|
+
// interval. This shop binds each subscription to a single
|
|
593
|
+
// `stripe_price_id` from its plan and carries no catalog of
|
|
594
|
+
// per-frequency prices to swap between, so the billing interval
|
|
595
|
+
// simply isn't expressible here. Writing the local `frequency`
|
|
596
|
+
// column anyway would tell the customer "Delivery frequency
|
|
597
|
+
// updated" while Stripe kept invoicing on the original cadence —
|
|
598
|
+
// exactly the divergence this surface must not create. Refuse with
|
|
599
|
+
// honest copy; the customer cancels + re-subscribes on a plan with
|
|
600
|
+
// the cadence they want. (A non-Stripe row falls through to the
|
|
601
|
+
// local-only recompute below — the shop's own cadence view.)
|
|
602
|
+
if (_isStripeBacked(row)) {
|
|
603
|
+
var fErr = new Error(
|
|
604
|
+
"subscriptionControls.changeFrequency: delivery frequency can't be changed on this subscription — " +
|
|
605
|
+
"cancel and start a new subscription on a plan with the cadence you want",
|
|
606
|
+
);
|
|
607
|
+
fErr.code = "SUBSCRIPTION_FREQUENCY_IMMUTABLE";
|
|
608
|
+
throw fErr;
|
|
609
|
+
}
|
|
610
|
+
|
|
498
611
|
// next_billing_at is recomputed off the row's current cycle
|
|
499
612
|
// anchor — `current_period_start` (Stripe-mirrored) if present,
|
|
500
613
|
// otherwise the current next_billing_at, otherwise now. One
|
package/package.json
CHANGED