Skip to content

Latest commit

 

History

History
456 lines (337 loc) · 12.5 KB

File metadata and controls

456 lines (337 loc) · 12.5 KB

Proactive Cart Recovery Implementation

Date: March 7, 2026
Pattern: Azure Function Polling (NOT pg_notify)
Status: ✅ Complete


Executive Summary

Implemented proactive cart recovery system using polling pattern for Azure PostgreSQL Flexible Server free tier compatibility. The system triggers agent messages after 2 hours of cart abandonment without using pg_notify (which kills idle connections on Azure free tier).


Architecture

Polling Pattern (Azure Function Compatible)

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Cart Update    │────▶│  commerce_events │◀────│  Event Poller   │
│  (User Action)  │     │  (Database)      │     │  (60s interval) │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                               │                        │
                               │                        ▼
                               │              ┌──────────────────┐
                               │              │  Process Event   │
                               │              │  & Send Message  │
                               │              └──────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │  Mark Processed  │
                        └──────────────────┘

Key Design Decisions

Decision Rationale
Polling (60s) Azure PostgreSQL free tier kills idle connections
2-hour delay Industry standard for cart abandonment
5-minute window Only process recent events to avoid backlog
Max 10 events/poll Prevent system overload during backlog
In-memory timeout tracking Prevent duplicate abandonment triggers

Files Created

1. Database Schema

File: prisma/migrations/20260307120000_create_commerce_events/migration.sql

CREATE TABLE "commerce_events" (
    "id" UUID NOT NULL DEFAULT gen_random_uuid(),
    "event_type" TEXT NOT NULL,
    "user_id" TEXT,
    "payload" JSONB NOT NULL,
    "processed" BOOLEAN NOT NULL DEFAULT false,
    "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "processed_at" TIMESTAMPTZ,

    CONSTRAINT "commerce_events_pkey" PRIMARY KEY ("id")
);

-- Index for efficient polling
CREATE INDEX "idx_commerce_events_unprocessed" ON "commerce_events"("processed", "created_at") 
  WHERE (processed = false);

-- Index for user-specific events
CREATE INDEX "idx_commerce_events_user" ON "commerce_events"("user_id", "created_at");

Prisma Model: Added to prisma/schema.prisma

model CommerceEvent {
  id          String   @id @default(uuid())
  eventType   String   @map("event_type")
  userId      String?  @map("user_id")
  payload     Json     @default("{}")
  processed   Boolean  @default(false)
  createdAt   DateTime @default(now()) @map("created_at")
  processedAt DateTime? @map("processed_at")

  @@index([processed, createdAt])
  @@index([userId, createdAt])
  @@map("commerce_events")
}

2. Event Trigger System

File: apps/web/lib/events/trigger.ts

Supported Event Types:

  • cart_abandoned - User left cart without checkout
  • price_drop - Product price reduced
  • stock_low - Product running low on stock
  • order_delayed - Order delivery delayed
  • merchant_anomaly - Unusual merchant activity

Key Functions:

// Generic event trigger
await triggerEvent('cart_abandoned', payload, userId);

// Cart abandonment (populates product names)
await triggerCartAbandonment(
  'user-123',
  'cart-abc',
  299900,        // Total in cents
  3,             // Item count
  ['Product 1']  // Optional names
);

// Price drop notification
await triggerPriceDrop(123, 'Headphones', 4999, 3999);

// Low stock alert
await triggerStockLow(123, 'Headphones', 5, 10);

// Order delay notification
await triggerOrderDelayed(456, 'ORD-001', expectedDate, 'Weather delay');

// Merchant anomaly alert
await triggerMerchantAnomaly('inventory_spike', 'Description', 'high');

3. Event Poller

File: apps/web/lib/events/poller.ts

Configuration:

const POLL_INTERVAL_MS = 60 * 1000;        // 60 seconds
const MAX_EVENTS_PER_POLL = 10;            // Max 10 events per cycle
const EVENT_WINDOW_MS = 5 * 60 * 1000;     // 5 minute window

Lifecycle Functions:

// Start poller (call in server entry point)
startEventPoller();

// Stop poller (graceful shutdown)
stopEventPoller();

Event Handlers:

  • handleCartAbandonment - Generates recovery message with cart details
  • handlePriceDrop - Notifies about price reductions
  • handleStockLow - Creates urgency for low-stock items
  • handleOrderDelayed - Proactive delivery delay notification
  • handleMerchantAnomaly - Alerts merchant dashboard

Sample Output:

[Event Poller] Starting with interval: 60000 ms
[Event Poller] Found 2 unprocessed events
[Event Poller] Processing cart_abandoned event abc-123 for user user-456
[Proactive Message] Cart Recovery: Still thinking about your Wireless Headphones and 2 other items? Total: ₹29,999. Want me to hold them while you decide?
[Event Poller] Successfully processed event abc-123

4. Cart Service

File: apps/web/lib/cart/service.ts

Features:

  • Cart CRUD operations
  • Automatic abandonment scheduling (2 hours)
  • Duplicate timeout prevention
  • Product name personalization
  • Coupon code support

Key Functions:

// Get or create cart
const cart = await getOrCreateCart('user-123');

// Add to cart (auto-schedules abandonment check)
await addToCart('user-123', productId, quantity);

// Update item quantity
await updateCartItem('user-123', productId, newQuantity);

// Remove item
await removeFromCart('user-123', productId);

// Clear cart (cancels abandonment check)
await clearCart('user-123');

// Apply coupon
await applyCoupon('user-123', 'SAVE20');

// Complete checkout (cancels abandonment check)
await completeCheckout('user-123');

Abandonment Scheduling:

// Internal: Schedules check after 2 hours
scheduleAbandonmentCheck(customerId, cart);

// Internal: Cancels scheduled check
cancelAbandonmentCheck(customerId);

Timeout Tracking:

interface AbandonmentTimeout {
  timeoutId: NodeJS.Timeout;
  cartId: string;
  scheduledAt: Date;
}

const abandonmentTimeouts = new Map<string, AbandonmentTimeout>();

Integration Guide

Step 1: Run Database Migration

cd /home/aparna/Desktop/vercel-ai-sdk
pnpm prisma migrate dev --name create_commerce_events

Step 2: Start Event Poller

Add to your server entry point (e.g., apps/web/app/layout.tsx or API route):

import { startEventPoller } from '@/lib/events/poller';

// In production, start the poller
if (process.env.NODE_ENV === 'production') {
  startEventPoller();
}

Note: For serverless deployments (Vercel, Azure Functions), the poller should run as a separate scheduled function.

Step 3: Wire Cart Operations

Replace direct cart database calls with the cart service:

// Before
await db.cart.update({ ... });

// After
import { addToCart, updateCartItem } from '@/lib/cart/service';

await addToCart(userId, productId, quantity);

Step 4: Handle Proactive Messages

The poller logs proactive messages. To integrate with your chat system:

// In apps/web/lib/events/poller.ts - handleCartAbandonment
async function handleCartAbandonment(event: CommerceEvent): Promise<void> {
  // ... existing code ...
  
  // TODO: Integrate with your messaging system
  await createProactiveMessage({
    userId: event.userId!,
    type: 'cart_recovery',
    content: message,
    metadata: { cartId: payload.cartId },
  });
}

Testing

Manual Test Flow

  1. Add items to cart:

    await addToCart('user-test', productId, 2);
    // Logs: [Cart Abandonment] Scheduled for user user-test in 120 minutes
  2. Wait 2 hours (or reduce ABANDONMENT_DELAY_MS for testing)

  3. Poller triggers event:

    [Event Poller] Processing cart_abandoned event ...
    [Proactive Message] Cart Recovery: Still thinking about your ...
    
  4. Event marked processed:

    [Event Poller] Successfully processed event ...
    

Unit Test Example

// tests/cart-abandonment.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { triggerCartAbandonment } from '@/lib/events/trigger';
import { db } from '@/lib/prisma';

describe('Cart Abandonment', () => {
  beforeEach(async () => {
    await db.commerceEvent.deleteMany();
  });

  it('creates unprocessed event', async () => {
    await triggerCartAbandonment('user-123', 'cart-abc', 2999, 3);
    
    const event = await db.commerceEvent.findFirst({
      where: { eventType: 'cart_abandoned' },
    });
    
    expect(event).toBeTruthy();
    expect(event?.processed).toBe(false);
    expect(event?.userId).toBe('user-123');
  });
});

Performance Characteristics

Metric Value Notes
Poll Interval 60s Azure Function minimum
Event Window 5min Only recent events processed
Max Events/Poll 10 Prevents overload
Abandonment Delay 2 hours Industry standard
Memory Overhead ~100 bytes/user In-memory timeout tracking
Database Queries 2-3 per poll Find events + mark processed

Production Considerations

1. Serverless Deployment

For Vercel/Azure Functions, create a separate scheduled function:

// apps/api/functions/event-poller.ts
import { startEventPoller } from '@/lib/events/poller';

export default async function handler(req: Request) {
  // Run one poll cycle
  await pollAndProcessEvents();
  return { success: true };
}

// Configure cron trigger (every 60s)
export const config = {
  schedule: '* * * * *', // Every minute
};

2. Graceful Shutdown

// In server shutdown handler
import { stopEventPoller } from '@/lib/events/poller';

process.on('SIGTERM', async () => {
  stopEventPoller();
  await db.$disconnect();
  process.exit(0);
});

3. Monitoring

Add metrics to track:

  • Events created per hour
  • Average processing latency
  • Failed event rate
  • Cart recovery conversion rate

4. Error Handling

Events that fail processing are not marked as processed, so they'll be retried on the next poll cycle. Implement retry limits:

// Add to CommerceEvent model
retries Int @default(0)
maxRetries Int @default(3)

Security Considerations

  1. User ID Validation: Always validate user IDs before triggering events
  2. Payload Sanitization: JSON payloads are stored as-is - sanitize before storage
  3. Rate Limiting: Consider rate limiting cart updates to prevent abuse
  4. Audit Logging: Log all event triggers for compliance

Future Enhancements

  1. A/B Testing: Test different recovery message timings (1h, 2h, 4h)
  2. Multi-channel: Send email/SMS in addition to chat messages
  3. Dynamic Timing: Adjust abandonment delay based on user behavior
  4. ML Prediction: Predict abandonment likelihood before 2 hours
  5. Merchant Dashboard: Show real-time event processing metrics

Deliverables Checklist

  • commerce_events table created with proper indexes
  • triggerCartAbandonment function implemented
  • ✅ Event poller running every 60 seconds
  • ✅ Proactive message generation for cart recovery
  • ✅ TypeScript compiles without errors
  • pg_notify NOT used (polling pattern instead)
  • ✅ Cart service with automatic abandonment scheduling
  • ✅ Duplicate timeout prevention
  • ✅ Comprehensive documentation

Next Steps

  1. Run migration: pnpm prisma migrate dev
  2. Start poller: Add startEventPoller() to server entry point
  3. Wire cart operations: Replace direct DB calls with cart service
  4. Integrate messaging: Connect proactive messages to chat system
  5. Monitor: Set up alerts for failed event processing
  6. Test: Run E2E tests with reduced abandonment delay

Implementation Complete. Ready for integration testing.