@86d-app/abandoned-carts
v0.0.25
Published
Abandoned cart tracking and recovery for 86d commerce platform
Downloads
854
Maintainers
Readme
[!WARNING] This project is under active development and is not ready for production use. Please proceed with caution. Use at your own risk.
Abandoned Carts Module
Tracks shopping carts that have been inactive beyond a configurable threshold and provides multi-channel recovery workflows (email, SMS, push notifications) to convert abandoned carts into orders.
Installation
npm install @86d-app/abandoned-cartsUsage
import abandonedCarts from "@86d-app/abandoned-carts";
const module = abandonedCarts({
abandonmentThresholdMinutes: 60,
maxRecoveryAttempts: 3,
expirationDays: 30,
});Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| abandonmentThresholdMinutes | number | 60 | Minutes of inactivity before a cart is considered abandoned |
| maxRecoveryAttempts | number | 3 | Maximum recovery attempts per cart (enforced at controller and endpoint level) |
| expirationDays | number | 30 | Default days for bulkExpire() when called without arguments |
All options are enforced at runtime:
maxRecoveryAttempts:recordAttempt()throws an error and the send-recovery endpoint returns HTTP 400 when the limit is reached.expirationDays:bulkExpire()uses this value when no explicitolderThanDaysargument is provided.abandonmentThresholdMinutes: Exposed viagetOptions()for consumers to query the configured threshold.
Store Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /abandoned-carts/track | Report a cart as abandoned (emits cart.abandoned) |
| GET | /abandoned-carts/recover/:token | Recover a cart using a unique recovery token |
Admin Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /admin/abandoned-carts | List abandoned carts (filterable by status, email) |
| GET | /admin/abandoned-carts/stats | Get recovery statistics |
| POST | /admin/abandoned-carts/bulk-expire | Bulk-expire carts older than N days (defaults to expirationDays) |
| GET | /admin/abandoned-carts/:id | Get a single abandoned cart with recovery attempts |
| POST | /admin/abandoned-carts/:id/recover | Send a recovery message (emits cart.recoveryAttempted) |
| POST | /admin/abandoned-carts/:id/dismiss | Dismiss an abandoned cart |
| DELETE | /admin/abandoned-carts/:id/delete | Delete an abandoned cart and its attempts |
Controller API
The AbandonedCartController interface is exported for inter-module use.
interface AbandonedCartController {
create(params: CreateAbandonedCartParams): Promise<AbandonedCart>;
get(id: string): Promise<AbandonedCart | null>;
getByToken(token: string): Promise<AbandonedCart | null>;
getByCartId(cartId: string): Promise<AbandonedCart | null>;
list(params?: { status?: string; email?: string; take?: number; skip?: number }): Promise<AbandonedCart[]>;
markRecovered(id: string, orderId: string): Promise<AbandonedCart | null>;
markExpired(id: string): Promise<AbandonedCart | null>;
dismiss(id: string): Promise<AbandonedCart | null>;
delete(id: string): Promise<boolean>;
recordAttempt(params: RecordAttemptParams): Promise<RecoveryAttempt>;
updateAttemptStatus(attemptId: string, status: "delivered" | "opened" | "clicked" | "failed"): Promise<RecoveryAttempt | null>;
listAttempts(abandonedCartId: string): Promise<RecoveryAttempt[]>;
getWithAttempts(id: string): Promise<AbandonedCartWithAttempts | null>;
getStats(): Promise<AbandonedCartStats>;
countAll(): Promise<number>;
bulkExpire(olderThanDays?: number): Promise<number>;
getOptions(): AbandonedCartControllerOptions;
}Events
The module emits the following events:
| Event | Source | Payload |
|---|---|---|
| cart.abandoned | Track endpoint | { cartId, email, cartTotal, currency, itemCount } |
| cart.recoveryAttempted | Send-recovery endpoint | { cartId, channel, recipient, attemptId } |
| cart.recovered | markRecovered() controller | { cartId, orderId, email, cartTotal, currency } |
| cart.expired | markExpired() / bulkExpire() | { cartId, email, cartTotal } |
| cart.dismissed | dismiss() controller | { cartId, email, cartTotal } |
Types
interface AbandonedCart {
id: string;
cartId: string;
customerId?: string;
email?: string;
items: CartItemSnapshot[];
cartTotal: number;
currency: string;
status: "active" | "recovered" | "expired" | "dismissed";
recoveryToken: string;
attemptCount: number;
lastActivityAt: Date;
abandonedAt: Date;
recoveredAt?: Date;
recoveredOrderId?: string;
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
interface CartItemSnapshot {
productId: string;
variantId?: string;
name: string;
sku?: string;
price: number;
quantity: number;
imageUrl?: string;
}
interface RecoveryAttempt {
id: string;
abandonedCartId: string;
channel: "email" | "sms" | "push";
recipient: string;
status: "sent" | "delivered" | "opened" | "clicked" | "failed";
subject?: string;
openedAt?: Date;
clickedAt?: Date;
sentAt: Date;
createdAt: Date;
}
interface AbandonedCartStats {
totalAbandoned: number;
totalRecovered: number;
totalExpired: number;
totalDismissed: number;
recoveryRate: number;
totalRecoveredValue: number;
}
interface AbandonedCartControllerOptions {
maxRecoveryAttempts: number;
expirationDays: number;
abandonmentThresholdMinutes: number;
}Store Components
CartRecovery
Recovers an abandoned cart by its recovery token, displaying the saved cart items or an expiration notice if the token is no longer valid.
Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| token | string | Yes | Recovery token used to look up the abandoned cart |
Usage in MDX
<CartRecovery token="abc123" />Use this component on a dedicated cart recovery landing page linked from recovery emails.
Notes
- Requires the
cartmodule (reads cartItems, cartTotal) andcustomersmodule (reads customerEmail). - Each abandoned cart gets a unique
recoveryToken(UUID) used in recovery links. - Recovery attempts track engagement: sent, delivered, opened, clicked, or failed.
maxRecoveryAttemptsis enforced both in the controller (recordAttemptthrows) and in the send-recovery admin endpoint (returns 400).bulkExpire()called without arguments uses the configuredexpirationDaysoption (default 30).- Cart items are stored as a JSON snapshot at the time of abandonment, decoupled from live cart data.
- Event emitter is injected at module init; controller uses a no-op fallback when no emitter is available.
