@bethinkpl/reorder
v1.8.0
Published
Customized version of @reorderjs/reorder.
Maintainers
Keywords
Readme
@bethinkpl/reorder
Customized fork of @reorder/reorderjs.
Changes
New features
- added day granularity for frequency intervals
Improvements / Fixes
- prevents executing
POST /store/carts/{cartId}/completefor carts containing subscription items - behaviour of
POST /store/carts/{cartId}/subscribematches it's non-subscription counterpart more closely (droppedsubscriptionfield, supports query params and uses same defaults) - shipping address is optional when item(s) in subscription cart don't require shipping (i.e.:
item.requires_shipping = false) - exposed
setPaymentSessionDatahook inprocessRenewalCycleWorkflowfor provider-agnostic payment handling during renewals - exposed the same
setPaymentSessionDatahook inrunDunningRetryWorkflow(input:{ subscription: { id, payment_context } }) so dunning retries use the host app's payment session payload, falling back to the stock Stripe payload when no handler is registered - dunning now emits five lifecycle events (names exported as
DunningEventsfrom@bethinkpl/reorder/modules/dunning) for the host app to notify customers on:subscription.dunning_started({ subscription_id, dunning_case_id, renewal_cycle_id, attempt_count, next_retry_at }, fired only when a case is created — a repeated failure on the same renewal cycle re-enters the existing case and stays silent),subscription.dunning_attempt_failed({ subscription_id, dunning_case_id, attempt_no, error_code, next_retry_at }, fired when a counted retry failed and another one is scheduled; no-budget reschedules stay silent - a tick that charged nothing and handed its attempt slot back never tells the customer their payment failed),subscription.payment_failed({ subscription_id, dunning_case_id, recovery_reason }, fired only by the run that actually moves the subscription topayment_failed, from either the exhausted retry loop or the admin "mark unrecovered" action — a later settlement of an already-payment_failedsubscription stays silent),subscription.dunning_parked({ subscription_id, dunning_case_id, attempt_no, park_reason, error_code }, fired when a retry is handed to an operator instead of being settled or rescheduled;park_reasonis one ofsetup_failure,requires_action,customer_payment_stalled,unreached_providerorindeterminate_provider_response) andsubscription.dunning_recovered({ subscription_id, dunning_case_id, renewal_order_id, recovery_reason }, fired when a case closes as recovered —recovery_reasonispayment_recoveredwhen a scheduled retry collected it andcustomer_paymentwhen the customer paid the renewal order themselves through the host app, which also settles the renewal cycle, advances the billing dates and hands the subscription back toactive). Delivery is at-most-once and immediate: each event is dispatched as soon as its emitting step runs, once the module writes that triggered it have committed, so a later step failing in the same workflow (e.g. the lock release) cannot retract an event that already went out, and a rejecting bus is never retried. Emitting never fails the workflow either — an event bus that rejects is logged as analertabledunning error and the case keeps its state - renewals and dunning retries capture through the payment module directly, which emits nothing:
payment.captured(and any host invoicing, tax or email hanging off it) arrives only when the provider's own webhook for that charge is delivered and the host's provider maps it to a successful action. With a missing or mis-signed webhook endpoint the subscription still renews and a dunning case still readsrecovered, and nothing errors - a scheduled dunning retry now steps aside instead of destroying a payment the customer is in the middle of: before it touches the renewal order it reads every payment session on every payment collection of that order, and when one is still live (status
pending,pending_authorizationorrequires_more, created less than 60 minutes ago and not past its owndata.expiresAt) or a sibling collection holds somebody else's authorization (authorizedorpartially_authorized), the tick charges nothing - it aborts its attempt row, hands the attempt slot back to the budget and reschedules itself 60 minutes later without notifying the customer, never touching the case setup-failure streak and only parking the case (park_reason: "customer_payment_stalled") once 24 ticks in a row have stepped aside (an adminretry nowis skipped the same way; the skips are counted inmetadata.session_conflict_count, which is informational and cleared as soon as a retry reaches the provider). Sessions a retry creates mark themselves withcontext: { dunning_case_id, dunning_attempt_id }and sessions a renewal run creates withcontext: { renewal_cycle_id }, so neither ever reads the other's session - or its own leftovers, including an authorization a failed capture left behind - as a customer's. A host app that creates its own payment sessions on a renewal order is protected as long as it sets neither marker on them, and it can mark a sessioncontext: { initiated_by: "customer" }to be treated as the customer's whatever else it carries - the daily renewal job applies the same guard before it reuses an order a previous run already created: if the customer is mid-checkout on it, the run is skipped before it records anything - no attempt row, no new failure, no dunning case - so the cycle keeps the
failedstatus it already had and the next window picks it up again. The job counts the skip asblockedrather thanfailed(failure_kind: "customer_payment_in_progress", never alertable) - completing subscription cart no longer requires payment method to be already saved at that point of time, instead it's info is backfilled after payment is captured
- renewal orders recompute the subscription plan discount instead of billing full catalog price (frozen
pricing_snapshotfirst, live plan config on plan change), with a combined-discount clamp applied on both checkout and renewals - exposed
resolveRenewalAdjustmentshook inprocessRenewalCycleWorkflowso the host app can add its own (code-less) discounts to renewal orders, andsubscriptionCreatedhook increateSubscriptionFromCartWorkflowfor reacting to genuine subscription creation source_snapshotnow truthfully captures the initial order's adjustments and tax lines (audit-only — renewals deliberately no longer replay them)- renewal cycles stranded in PROCESSING by an unhandled failure between steps are now marked FAILED via compensation, keeping them retryable
QoL
- addition of eslint lint rules
- improved test setup
Content of README.md WIP
