Chuyển đến nội dung chính

Lesson 11: Cart, Checkout & Payment — Multi-gateway, Tax, Subscription & Fraud Detection

Shopping cart architecture, checkout flow, payment gateway integration (Stripe, PayPal), tax calculation, subscription/membership, fraud detection, PCI compliance, abandoned cart recovery, multi-currency checkout.

🏗️ Architecture — Lesson 11 Lesson 11: Cart, Checkout & Payment — Multi-gateway, Tax, Subscription & Fraud Detection

Fashion Design & Print-on-Demand System Architecture — From Domain Analysis to Production

Part 3: Product & E-Commerce Platform

xdev.asia

1. Checkout Architecture

┌──────────────────────────────────────────────────────┐
│                  Checkout Flow                       │
│                                                      │
│  ┌────────┐   ┌──────────┐   ┌─────────┐   ┌──────┐│
│  │  Cart  │──▶│ Shipping │──▶│ Payment │──▶│Review││
│  │        │   │ Address  │   │ Method  │   │      ││
│  └────────┘   └──────────┘   └─────────┘   └──┬───┘│
│                                                │     │
│                                          ┌─────▼───┐ │
│                                          │  Place  │ │
│                                          │  Order  │ │
│                                          └─────┬───┘ │
└────────────────────────────────────────────────│─────┘
                                                 │
         ┌───────────────────────────────────────┼───┐
         ▼                    ▼                  ▼   ▼
  ┌──────────────┐  ┌──────────────┐  ┌──────────┐ ┌─────┐
  │Payment Gateway│  │  Tax Engine  │  │  Fraud   │ │Order│
  │Stripe/PayPal │  │ TaxJar/Avalara│ │Detection │ │Svc  │
  └──────────────┘  └──────────────┘  └──────────┘ └─────┘

2. Shopping Cart Service

interface Cart {
  id: string;
  userId?: string;            // null for guest
  sessionId: string;          // For guest cart
  
  items: CartItem[];
  
  // Pricing
  subtotal: Money;
  discounts: AppliedDiscount[];
  shippingCost: Money;
  taxAmount: Money;
  total: Money;
  
  // Currency
  currency: Currency;
  
  // Metadata
  createdAt: Date;
  updatedAt: Date;
  expiresAt: Date;            // Cart expiry (30 days)
  
  // Checkout state
  checkoutState: CheckoutState;
}

interface CartItem {
  id: string;
  productId: string;
  variantSku: string;
  quantity: number;
  
  // Snapshot at time of add (prices can change)
  unitPrice: Money;
  
  // Custom design options
  customization?: {
    designId: string;
    printArea: string;
    mockupUrl: string;         // Preview image
  };
  
  // Availability check result
  availability: 'available' | 'low_stock' | 'unavailable';
  estimatedProductionDays: number;
}

class CartService {
  constructor(
    private cartRepo: CartRepository,    // Redis for active carts
    private pricingEngine: PricingEngine,
    private availabilityService: AvailabilityService,
  ) {}

  async addItem(cartId: string, item: AddItemRequest): Promise<Cart> {
    const cart = await this.cartRepo.get(cartId);
    
    // 1. Validate product exists and is available
    const availability = await this.availabilityService.check(item.variantSku);
    if (availability.status === 'unavailable') {
      throw new ProductUnavailableError(item.variantSku);
    }

    // 2. Get current price
    const price = await this.pricingEngine.getPrice(item.productId, item.variantSku);

    // 3. Add or update quantity
    const existingItem = cart.items.find(i => i.variantSku === item.variantSku);
    if (existingItem) {
      existingItem.quantity += item.quantity;
    } else {
      cart.items.push({
        id: generateId(),
        productId: item.productId,
        variantSku: item.variantSku,
        quantity: item.quantity,
        unitPrice: price,
        customization: item.customization,
        availability: availability.status,
        estimatedProductionDays: availability.productionDays,
      });
    }

    // 4. Recalculate totals
    await this.recalculate(cart);

    // 5. Save
    await this.cartRepo.save(cart);
    return cart;
  }

  private async recalculate(cart: Cart): Promise<void> {
    // Subtotal
    cart.subtotal = cart.items.reduce(
      (sum, item) => sum.add(item.unitPrice.multiply(item.quantity)),
      Money.zero(cart.currency),
    );

    // Apply discounts
    cart.discounts = await this.applyDiscounts(cart);
    const discountTotal = cart.discounts.reduce(
      (sum, d) => sum.add(d.amount),
      Money.zero(cart.currency),
    );

    // Shipping (if address known)
    if (cart.checkoutState.shippingAddress) {
      cart.shippingCost = await this.calculateShipping(cart);
      cart.taxAmount = await this.calculateTax(cart);
    }

    // Total
    cart.total = cart.subtotal
      .subtract(discountTotal)
      .add(cart.shippingCost)
      .add(cart.taxAmount);
  }
}

3. Payment Gateway Integration

// Payment service — abstract over multiple gateways
interface PaymentService {
  createPaymentIntent(order: CheckoutOrder): Promise<PaymentIntent>;
  confirmPayment(intentId: string, paymentMethod: string): Promise<PaymentResult>;
  refund(paymentId: string, amount: Money, reason: string): Promise<RefundResult>;
}

// Stripe implementation
class StripePaymentService implements PaymentService {
  constructor(private stripe: Stripe) {}

  async createPaymentIntent(order: CheckoutOrder): Promise<PaymentIntent> {
    const intent = await this.stripe.paymentIntents.create({
      amount: order.total.toSmallestUnit(),   // Cents
      currency: order.currency.toLowerCase(),
      
      // Metadata for reconciliation
      metadata: {
        orderId: order.id,
        customerId: order.customerId,
        source: order.source,
      },
      
      // Enable specific payment methods
      payment_method_types: ['card', 'klarna', 'afterpay_clearpay'],
      
      // Automatic payment methods based on customer's country
      automatic_payment_methods: { enabled: true },
      
      // Capture immediately (no hold)
      capture_method: 'automatic',
      
      // Statement descriptor
      statement_descriptor_suffix: 'POD ORDER',
    });

    return {
      id: intent.id,
      clientSecret: intent.client_secret!,
      status: intent.status,
      amount: Money.fromSmallestUnit(intent.amount, intent.currency),
    };
  }

  async confirmPayment(intentId: string, paymentMethodId: string): Promise<PaymentResult> {
    const intent = await this.stripe.paymentIntents.confirm(intentId, {
      payment_method: paymentMethodId,
    });

    return {
      success: intent.status === 'succeeded',
      paymentId: intent.id,
      status: intent.status,
      chargeId: intent.latest_charge as string,
    };
  }

  async refund(paymentId: string, amount: Money, reason: string): Promise<RefundResult> {
    const refund = await this.stripe.refunds.create({
      payment_intent: paymentId,
      amount: amount.toSmallestUnit(),
      reason: reason as Stripe.RefundCreateParams.Reason,
    });

    return {
      refundId: refund.id,
      status: refund.status!,
      amount: Money.fromSmallestUnit(refund.amount, refund.currency),
    };
  }
}

4. Fraud Detection

interface FraudDetectionService {
  assess(order: CheckoutOrder): Promise<FraudAssessment>;
}

interface FraudAssessment {
  riskScore: number;           // 0-100
  riskLevel: 'low' | 'medium' | 'high' | 'critical';
  signals: FraudSignal[];
  recommendation: 'approve' | 'review' | 'reject';
}

interface FraudSignal {
  type: string;
  severity: number;
  description: string;
}

async function assessFraud(order: CheckoutOrder): Promise<FraudAssessment> {
  const signals: FraudSignal[] = [];

  // 1. Velocity check — too many orders in short time
  const recentOrders = await getRecentOrders(order.email, '1h');
  if (recentOrders.length > 5) {
    signals.push({
      type: 'velocity',
      severity: 80,
      description: `${recentOrders.length} orders in last hour`,
    });
  }

  // 2. IP geolocation vs billing address mismatch
  const ipCountry = await geolocateIP(order.ipAddress);
  if (ipCountry !== order.billingAddress.country) {
    signals.push({
      type: 'geo_mismatch',
      severity: 40,
      description: `IP: ${ipCountry}, Billing: ${order.billingAddress.country}`,
    });
  }

  // 3. Email domain check
  if (isDisposableEmail(order.email)) {
    signals.push({
      type: 'disposable_email',
      severity: 60,
      description: 'Disposable email domain detected',
    });
  }

  // 4. Address verification (AVS)
  // 5. Card BIN country match
  // 6. Device fingerprint analysis
  // 7. Stripe Radar integration

  const riskScore = calculateRiskScore(signals);

  return {
    riskScore,
    riskLevel: getRiskLevel(riskScore),
    signals,
    recommendation: riskScore > 80 ? 'reject'
      : riskScore > 50 ? 'review'
      : 'approve',
  };
}

5. Abandoned Cart Recovery

// Abandoned cart detection & recovery automation
interface AbandonedCartService {
  detectAbandoned(): Promise<AbandonedCart[]>;
  sendRecoveryEmail(cart: AbandonedCart, sequence: number): Promise<void>;
}

// Recovery email sequence
const RECOVERY_SEQUENCE = [
  { delay: '1h',   subject: 'Bạn quên gì đó trong giỏ hàng!',      discount: null },
  { delay: '24h',  subject: 'Thiết kế của bạn đang chờ...',         discount: '10%' },
  { delay: '72h',  subject: 'Ưu đãi cuối cùng — Giảm 15%!',        discount: '15%' },
];

async function processAbandonedCarts(): Promise<void> {
  const abandonedCarts = await cartRepo.findAbandoned({
    minAge: Duration.hours(1),
    maxAge: Duration.days(7),
    hasEmail: true,
    hasItems: true,
    notRecovered: true,
  });

  for (const cart of abandonedCarts) {
    const sequenceIndex = getSequenceIndex(cart);
    if (sequenceIndex >= RECOVERY_SEQUENCE.length) continue;

    const sequence = RECOVERY_SEQUENCE[sequenceIndex];
    
    await emailService.send({
      to: cart.email,
      template: 'abandoned-cart-recovery',
      data: {
        items: cart.items.map(formatCartItem),
        total: cart.total,
        mockupImages: cart.items.map(i => i.customization?.mockupUrl),
        discountCode: sequence.discount
          ? await generateDiscountCode(cart.id, sequence.discount)
          : null,
        recoveryUrl: `${BASE_URL}/cart/recover/${cart.recoveryToken}`,
      },
    });

    await cartRepo.markRecoveryEmailSent(cart.id, sequenceIndex);
  }
}

6. Summary

ComponentTechnologyKey Features
Cart StorageRedis + PostgreSQLFast read/write, persistent after expiry
PaymentStripe + PayPalCards, BNPL, multi-currency
TaxTaxJar/AvalaraJurisdiction-aware, auto-filing
FraudStripe Radar + custom rulesML scoring, velocity check, AVS
Cart RecoveryEmail automation (3-step)Timed sequences with escalating discounts
PCI ComplianceStripe Elements (client-side)Card data never touches our servers