silvershop-suite / abandoned-cart
Abandoned-cart recovery for SilverShop: detect carts left behind, send a timed recovery-email sequence, and restore the cart from a tokenised link — with a self-contained snapshot so recovery survives cart cleanup.
Package info
github.com/silvershop-suite/silvershop-abandoned-cart
Type:silverstripe-vendormodule
pkg:composer/silvershop-suite/abandoned-cart
Requires
- php: ^8.3
- silvershop/core: ^6.2
- silverstripe/framework: ^6.0
- symbiote/silverstripe-queuedjobs: ^6.0
Requires (Dev)
Suggests
- silvershop-suite/newsletter: Gates recovery mail on the canonical NewsletterConsent opt-in (require_consent mode) and is the seam for the v2 ESP-export delivery.
- silvershop-suite/notification-log: Captures each recovery email as an auditable, GDPR-categorised SentNotification row and provides the data-erasure seam.
- silvershop-suite/order-admin: Richer CMS order surfaces alongside the recovery admin.
- silvershop-suite/order-confirmation: Shares the tokenised-order-access idiom; its GuestToken gives placed-order links from the recovery landing.
- silvershop-suite/store-profile: Shared shop identity used as the sender name/address for the recovery emails.
- silvershop/discounts: v2 incentive coupons injected into the later recovery emails.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-25 11:48:04 UTC
README
Abandoned-cart recovery for SilverShop. It detects carts left behind, sends a short, timed recovery-email sequence, and restores the cart from a one-click tokenised link — with a self-contained snapshot so recovery works even after a cart is cleaned up.
Native and out-of-the-box: no ESP account required. Every scheduled send is visible and auditable in
QueuedJobAdmin, every email can be logged by silvershop-suite/notification-log, and recovered revenue is reported in the
CMS.
Requirements
silverstripe/framework ^6.0silvershop/core ^6.2— provides the shared email shell (OrderEmailHeader/OrderEmailFooter) the recovery email wraps itself in.symbiote/silverstripe-queuedjobs ^6.0— all scheduling is queued jobs, not opaque OS cron.
PHP 8.3 / 8.4.
Install
composer require silvershop-suite/abandoned-cart
Then vendor/bin/sake db:build --flush (or dev/build?flush=1), and arm the recurring scan once:
vendor/bin/sake tasks:SilverShop-AbandonedCart-Task-SendAbandonedCartRecoveryTask --schedule
How it works
-
Detection. A cart in SilverShop is an
OrderwithStatus = 'Cart'. The recurringScanAbandonedCartsJobfinds carts whoseLastEditedis older than the first step delay (and newer than amax_age_minsfloor), that have at least one line item and a known customer email (Order::getLatestEmail()— a member's address, or a guest's once they entered it at checkout). -
Snapshot. For each newly-eligible cart a
CartRecoveryrow is written with a unique token, the recipient email, and a JSON snapshot of the cart's reconstructable identity (per line: buyable class/ID, quantity, and title/price for display). Recovery never depends on the live cart surviving. -
Sequence. Each
CartRecoveryis sent up to three recovery emails on a configurable cadence (default 1 h / 24 h / 72 h), one per due step, via a per-cartSendCartRecoveryJob. Sends are claimed before sending (the step is written before the email goes out), so a crashed or retried job never double-sends. The sequence stops the moment the cart is placed, emptied, unsubscribed, or fully sent. -
Restore. The email's "Return to your cart" button hits
cart-recovery/resume/<token>, which:- live-order fast path — if the original cart still exists and is still a cart, it is set as the visitor's current cart exactly (core reprices and prunes anything no longer purchasable); otherwise
- snapshot rebuild — a fresh cart is rebuilt from the snapshot at current price and stock, skipping any product that is gone or unavailable (surfaced as "no longer available"). The snapshot price is display-only and is never used to sell.
-
Attribution. The resume click is stamped; when the order is subsequently placed, an
onPlaceOrderhook marks theCartRecoveryRecovered with the order value. TheRecoveredCartsReportshows recovered count + value by period — the attribution core's count-onlyAbandonedCartReportlacks.
Consent / lawful basis (GDPR, verify with your DPO)
Recovery email is marketing-adjacent, so the lawful basis is configurable. This is a legal posture shipped with a safe default — verify it with legal/DPO for your jurisdiction.
SilverShop\AbandonedCart\Service\CartRecoveryService: consent_mode: 'legitimate_interest' # or 'require_consent'
legitimate_interest(default) — send to recipients who gave their email in the checkout flow (a pre-contractual step) or who are members, always with one-click unsubscribe in every email and our own suppression list (an unsubscribed row is never mailed again). A defensible legitimate-interest / soft-opt-in posture.require_consent— only send when the address has an explicitNewsletterConsentopt-in (requiressilvershop-suite/newsletter). With a strict counsel, flip this on.
Every email carries a one-click unsubscribe regardless of mode.
Retention / GDPR
The CartData snapshot and the recipient email are personal data. When a row goes terminal (Recovered / Unsubscribed
/ Stopped / Completed) its token is nulled immediately (killing the live resume link), and the row is deleted
retention_days (default 30) after it ends. The recovery email is logged by silvershop-suite/notification-log under the
Marketing category (key cart.recovery), so it participates in that module's split-retention and data-erasure
flows.
The CartCleanupTask window (now optional)
Core's CartCleanupTask hard-deletes Cart orders older than delete_after_mins (default 2 hours). Because
recovery is snapshot-based, this is no longer a blocker — a deleted cart is rebuilt from the snapshot. Raising
CartCleanupTask.delete_after_mins above your sequence only keeps the exact live-order fast path available longer; it
is a nice-to-have, not a prerequisite.
Configuration
SilverShop\AbandonedCart\Service\CartRecoveryService: steps: [60, 1440, 4320] # minutes after abandonment per recovery step consent_mode: 'legitimate_interest' max_age_mins: 20160 # ignore carts older than this (14 days); 0 disables the floor max_per_run: 100 # cap per inline CLI pass retention_days: 30 # delete terminal rows this long after they end attribute_on_place_without_click: false from_email: '' # falls back to store-profile / SiteConfig / admin email SilverShop\AbandonedCart\Job\ScanAbandonedCartsJob: scan_interval_mins: 30 # the recurring scan re-queues itself this far ahead
Run the sequence from cron without a worker (dry-run by default):
vendor/bin/sake tasks:SilverShop-AbandonedCart-Task-SendAbandonedCartRecoveryTask --run
Soft dependencies
All guarded — the module works standalone and lights these up when present:
silvershop-suite/newsletter— therequire_consentgate (and the v2 ESP-export seam).silvershop-suite/notification-log— logs + categorises each recovery email and provides the erasure seam.silvershop-suite/store-profile— shop identity for the sender name/address.silvershop-suite/order-confirmation/silvershop-suite/order-admin— shared token idiom + admin surfaces.silvershop/discounts— v2 incentive coupons.
Roadmap (v2)
ESP-export delivery seam (push cart-abandoned / cart-recovered events to the newsletter provider registry); a
discount incentive injected into later emails; A/B timing; open-tracking pixel; a core updateCartCleanupQuery
extension hook so carts with an active recovery are spared; product thumbnails in the email.
License
BSD-3-Clause.
Credits
Built and maintained by Remy Vaartjes (remy@xd.nl) — part of the SilverShop-Suite.