Skip to content

Instantly share code, notes, and snippets.

@zacjones93
Created October 8, 2025 22:17
Show Gist options
  • Save zacjones93/6fa9f486c676d5b0650e14ce3c5f0636 to your computer and use it in GitHub Desktop.
Save zacjones93/6fa9f486c676d5b0650e14ce3c5f0636 to your computer and use it in GitHub Desktop.
Flat Discount Coupons Implementation Plan

Flat Discount Coupons - System Architecture Documentation

Overview

This document provides a comprehensive overview of the pricing and coupon system that will be modified to support flat-rate (fixed amount) discount coupons alongside the existing percentage-based discounts.

Current System Architecture

High-Level Data Flow

graph TB
    A[User Checkout Request] --> B[formatPricesForProduct]
    B --> C[determineCouponToApply]
    C --> D{Coupon Type?}
    D -->|PPP| E[getPPPDiscountPercent]
    D -->|Bulk| F[getBulkDiscountPercent]
    D -->|Special| G[Use Merchant Coupon]
    E --> H[Calculate Percentage Discount]
    F --> H
    G --> H
    B --> I[getFixedDiscountForIndividualUpgrade]
    I --> J{Is Upgrade?}
    J -->|Yes| K[Calculate Fixed Discount]
    J -->|No| L[Fixed Discount = 0]
    K --> M[getCalculatedPrice]
    L --> M
    H --> M
    M --> N[Return FormattedPrice]
    N --> O[stripeCheckout]
    O --> P{Create Stripe Coupon}
    P -->|Upgrade| Q[Create amount_off Coupon]
    P -->|Merchant| R[Create Promotion Code]
    Q --> S[Stripe Checkout Session]
    R --> S
Loading

Current Coupon Flow

sequenceDiagram
    participant User
    participant FormatPrices as formatPricesForProduct
    participant DetermineCoupon as determineCouponToApply
    participant DB as Database/Adapter
    participant Calc as getCalculatedPrice
    participant Stripe as stripeCheckout

    User->>FormatPrices: Request Price (productId, merchantCouponId, etc.)
    FormatPrices->>DB: Get Product, Price, Purchase
    FormatPrices->>DetermineCoupon: Determine which coupon to apply
    DetermineCoupon->>DB: Get MerchantCoupon
    DetermineCoupon->>DetermineCoupon: Check PPP conditions
    DetermineCoupon->>DetermineCoupon: Check Bulk conditions
    DetermineCoupon-->>FormatPrices: Return appliedMerchantCoupon (percentageDiscount)
    FormatPrices->>FormatPrices: getFixedDiscountForIndividualUpgrade
    FormatPrices->>Calc: Calculate price with percentOfDiscount & fixedDiscount
    Calc-->>FormatPrices: calculatedPrice
    FormatPrices-->>User: FormattedPrice
    User->>Stripe: Checkout with FormattedPrice
    Stripe->>Stripe: Create Stripe Coupon/Promotion Code
    Stripe->>Stripe: Create Checkout Session
    Stripe-->>User: Redirect to Stripe
Loading

Key System Components

1. Data Models & Schemas

Current MerchantCoupon Schema

Location: packages/core/src/schemas/merchant-coupon-schema.ts

{
  id: string
  identifier: string | null | undefined  // Stripe coupon ID
  status: number
  merchantAccountId: string
  percentageDiscount: number  // 0.0 to 1.0 (e.g., 0.25 = 25%)
  type: string  // 'ppp' | 'bulk' | 'special'
}

Key Issue: No field for fixed amount discounts (amount_off)

FormattedPrice Type

Location: packages/core/src/types.ts

{
  id: string
  quantity: number
  unitPrice: number
  fullPrice: number
  fixedDiscountForUpgrade: number  // Only for upgrades
  calculatedPrice: number
  availableCoupons: MinimalMerchantCoupon[]
  appliedMerchantCoupon?: MinimalMerchantCoupon
  bulk: boolean
  // ... upgrade fields
}

Key Issue: Fixed discounts only supported for upgrades, not merchant coupons

2. Core Pricing Functions

determineCouponToApply()

Location: packages/core/src/lib/pricing/determine-coupon-to-apply.ts:39

Purpose: Determines which coupon (PPP, Bulk, or Special) to apply based on priority

Current Logic:

  1. Check if merchant coupon is 'special' type
  2. Check PPP eligibility (country-based, single quantity, no prior full-price purchases)
  3. Check Bulk eligibility (quantity > 1 or existing bulk purchase)
  4. Return coupon with percentageDiscount only

Returns:

{
  appliedMerchantCoupon?: MinimalMerchantCoupon  // has percentageDiscount
  appliedCouponType: 'ppp' | 'bulk' | 'special' | 'none'
  availableCoupons: MinimalMerchantCoupon[]
  bulk: boolean
}

getCalculatedPrice()

Location: packages/core/src/lib/pricing/get-calculated-price.ts:17

Current Implementation:

function getCalculatedPrice({
  unitPrice,
  percentOfDiscount = 0,
  quantity = 1,
  fixedDiscount = 0,
}) {
  const fullPrice = unitPrice * quantity
  const discountMultiplier = 1 - percentOfDiscount
  const calculatedPrice = (
    (fullPrice - fixedDiscount) * discountMultiplier
  ).toFixed(2)

  return Number(calculatedPrice)
}

Order of operations:

  1. Calculate fullPrice = unitPrice × quantity
  2. Subtract fixedDiscount (upgrade only)
  3. Apply percentage discount multiplier
  4. Return final price

formatPricesForProduct()

Location: packages/core/src/lib/pricing/format-prices-for-product.ts:128

Current Flow:

  1. Get product, price, and purchase data
  2. Call determineCouponToApply() → get percentageDiscount
  3. Calculate fixedDiscountForUpgrade (upgrade scenarios only)
  4. Calculate fullPrice: unitPrice * quantity - fixedDiscountForUpgrade
  5. Call getCalculatedPrice() with percentOfDiscount
  6. Return FormattedPrice

Key Lines:

// Line 204: Extract percentage discount from merchant coupon
const percentOfDiscount = appliedMerchantCoupon?.percentageDiscount

// Line 202: Full price calculation
const fullPrice: number = unitPrice * quantity - fixedDiscountForUpgrade

// Line 221: Price calculation
calculatedPrice: getCalculatedPrice({
  unitPrice,
  percentOfDiscount,
  fixedDiscount: fixedDiscountForUpgrade,
  quantity,
})

3. Stripe Integration

stripeCheckout()

Location: packages/core/src/lib/pricing/stripe-checkout.ts:192

Current Discount Handling:

Scenario 1: Upgrades (Lines 323-376)

if (isUpgrade && upgradeFromPurchase && loadedProduct && customerId) {
  const fixedDiscountForIndividualUpgrade = await getFixedDiscountForIndividualUpgrade(...)

  if (fixedDiscountForIndividualUpgrade > 0) {
    const amount_off_in_cents = (fullPrice - calculatedPrice) * 100
    const couponId = await config.paymentsAdapter.createCoupon({
      amount_off: amount_off_in_cents,  // ← Creates fixed-amount coupon
      name: couponName,
      max_redemptions: 1,
      redeem_by: TWELVE_FOUR_HOURS_FROM_NOW,
      currency: 'USD',
      applies_to: { products: [merchantProductIdentifier] },
    })
    discounts.push({ coupon: couponId })
  }
}

Scenario 2: Merchant Coupons (Lines 377-395)

else if (merchantCoupon && merchantCoupon.identifier) {
  // Assumes pre-existing Stripe coupon with percentage_off
  const promotionCodeId = await config.paymentsAdapter.createPromotionCode({
    coupon: merchantCoupon.identifier,  // ← Uses existing Stripe coupon ID
    max_redemptions: 1,
    expires_at: TWELVE_FOUR_HOURS_FROM_NOW,
  })
  discounts.push({ promotion_code: promotionCodeId })
}

Key Issue: Merchant coupons currently only support percentage-based Stripe coupons via promotion codes

PaymentsAdapter Interface

Location: packages/core/src/types.ts:131

interface PaymentsAdapter {
  getCouponPercentOff(identifier: string): Promise<number>

  createCoupon(params: Stripe.CouponCreateParams): Promise<string>
  // Stripe.CouponCreateParams supports:
  // - percent_off: number
  // - amount_off: number (in cents)
  // - currency: string (required with amount_off)

  createPromotionCode(params: Stripe.PromotionCodeCreateParams): Promise<string>
  createCheckoutSession(params: Stripe.Checkout.SessionCreateParams): Promise<string | null>
  // ... other methods
}

Implementation: packages/core/src/providers/stripe.ts:95

Current Limitations

1. Schema Constraints

  • MerchantCoupon.percentageDiscount exists
  • ❌ No MerchantCoupon.amountDiscount field
  • ❌ No validation preventing both discount types

2. Business Logic Constraints

  • ✅ Fixed discounts work for upgrades only via getFixedDiscountForIndividualUpgrade
  • ❌ Merchant coupons limited to percentage-based discounts
  • ❌ No mechanism to choose between fixed vs percentage for merchant coupons

3. Stripe Integration Constraints

  • ✅ Adapter supports creating amount_off coupons (upgrade flow proves this)
  • ✅ Adapter can retrieve percent_off from existing coupons via getCouponPercentOff
  • ❌ No adapter method to retrieve amount_off from existing coupons
  • ❌ Merchant coupon flow assumes pre-existing Stripe coupon with percentage

4. Calculation Order Issues

  • Current: (fullPrice - fixedDiscount) * (1 - percentDiscount)
  • Fixed discount applied before percentage
  • Need to prevent double-discounting when merchant coupon is fixed-amount

Proposed Architecture Changes

1. Enhanced Data Model

classDiagram
    class MerchantCoupon {
        +string id
        +string? identifier
        +number status
        +string merchantAccountId
        +number? percentageDiscount
        +number? amountDiscount
        +string type
        +validate() boolean
    }

    class MinimalMerchantCoupon {
        +string id
        +number status
        +number? percentageDiscount
        +number? amountDiscount
        +string type
        +string? country
    }

    class FormattedPrice {
        +number calculatedPrice
        +number fullPrice
        +number fixedDiscountForUpgrade
        +number? appliedFixedDiscount
        +MinimalMerchantCoupon? appliedMerchantCoupon
        +DiscountType appliedDiscountType
    }

    class DiscountType {
        <<enumeration>>
        fixed
        percentage
        ppp
        bulk
        none
    }

    MerchantCoupon <|-- MinimalMerchantCoupon
    FormattedPrice --> MinimalMerchantCoupon
    FormattedPrice --> DiscountType
Loading

Schema Updates:

// merchant-coupon-schema.ts
export const merchantCouponSchema = z.object({
  id: z.string().max(191),
  identifier: z.string().max(191).optional().nullable(),
  status: z.number().int().default(0),
  merchantAccountId: z.string().max(191),
  percentageDiscount: z.coerce.number().optional().refine(...),
  amountDiscount: z.number().int().optional(),  // NEW: in cents
  type: z.string().max(191),
}).refine(
  (data) => {
    // Validation: Cannot have both percentageDiscount and amountDiscount
    const hasPercent = data.percentageDiscount !== undefined && data.percentageDiscount > 0
    const hasAmount = data.amountDiscount !== undefined && data.amountDiscount > 0
    return !(hasPercent && hasAmount)
  },
  { message: "Cannot have both percentage and amount discount" }
)

2. Enhanced Pricing Flow

graph TB
    A[User Checkout] --> B[formatPricesForProduct]
    B --> C[determineCouponToApply]
    C --> D{Merchant Coupon Type?}
    D -->|Has amountDiscount| E[Apply Fixed Discount Path]
    D -->|Has percentageDiscount| F[Apply Percentage Discount Path]
    D -->|PPP/Bulk| F
    E --> G[Set appliedDiscountType = 'fixed']
    F --> H[Set appliedDiscountType = 'percentage/ppp/bulk']
    G --> I[getCalculatedPrice with fixed only]
    H --> I[getCalculatedPrice with percent]
    I --> J[Return FormattedPrice]
    J --> K[stripeCheckout]
    K --> L{Discount Type?}
    L -->|fixed from merchant| M[Create transient amount_off coupon]
    L -->|percentage| N[Create promotion code]
    L -->|upgrade + fixed| O[Create upgrade amount_off coupon]
    M --> P[Stripe Checkout]
    N --> P
    O --> P
Loading

3. Updated Stripe Checkout Logic

sequenceDiagram
    participant FC as formatPricesForProduct
    participant DC as determineCouponToApply
    participant SC as stripeCheckout
    participant PA as PaymentsAdapter
    participant ST as Stripe

    FC->>DC: Get coupon to apply
    DC->>DC: Check amountDiscount field
    alt Has amountDiscount
        DC-->>FC: Return {amountDiscount, appliedDiscountType: 'fixed'}
    else Has percentageDiscount
        DC-->>FC: Return {percentageDiscount, appliedDiscountType: 'percentage'}
    end

    FC->>FC: Calculate price with appropriate discount
    FC-->>SC: Pass FormattedPrice + appliedDiscountType

    SC->>SC: Check appliedDiscountType
    alt Is 'fixed' merchant coupon
        SC->>PA: createCoupon({amount_off, currency: 'USD'})
        PA->>ST: Create transient amount_off coupon
        ST-->>PA: Return coupon ID
        PA-->>SC: Return coupon ID
        SC->>SC: Add to discounts[]
    else Is percentage/PPP/bulk
        SC->>PA: createPromotionCode({coupon: identifier})
        PA->>ST: Create promotion code
        ST-->>PA: Return promo code ID
        PA-->>SC: Return promo code ID
        SC->>SC: Add to discounts[]
    else Is upgrade with fixed discount
        SC->>PA: createCoupon({amount_off: calculated})
        PA->>ST: Create upgrade coupon
        ST-->>PA: Return coupon ID
        PA-->>SC: Return coupon ID
        SC->>SC: Add to discounts[]
    end

    SC->>PA: createCheckoutSession({discounts, metadata})
    PA->>ST: Create session
    ST-->>PA: Return session URL
    PA-->>SC: Return URL
Loading

Implementation Plan Summary

Phase 1: Data Layer

  1. Schema Updates

    • Add amountDiscount field to merchantCouponSchema
    • Add validation to prevent both discount types
    • Update MinimalMerchantCoupon type
  2. Database Migration

    • Add amountDiscount column (nullable int)
    • Add check constraint for mutual exclusivity
  3. Adapter Updates

    • Add getCouponAmountOff(identifier: string): Promise<number> to PaymentsAdapter
    • Implement in StripePaymentAdapter

Phase 2: Business Logic

  1. determineCouponToApply()

    • Check for amountDiscount on merchant coupons
    • Add appliedDiscountType to return value
    • Document precedence: amountDiscount > percentageDiscount > PPP > bulk
  2. formatPricesForProduct()

    • Extract appliedFixedDiscount from merchant coupon
    • Pass to getCalculatedPrice
    • Add to return payload
    • Handle conflict when upgrade + merchant fixed discount
  3. getCalculatedPrice()

    • Add optional merchantFixedDiscount parameter
    • Update calculation logic to handle merchant fixed discounts
    • Ensure non-negative prices

Phase 3: Stripe Integration

  1. stripeCheckout()

    • Check appliedDiscountType
    • For fixed merchant coupons: create transient amount_off coupon
    • Update metadata to include discount type and amount
    • Add guard to prevent stacking upgrade + merchant fixed discounts
    • Add logging for coupon decisions
  2. Metadata Updates

    • Add discountType: 'fixed' | 'percentage' | 'ppp' | 'bulk'
    • Add discountAmount: number (cents or percentage)

Phase 4: Observability

  1. Replace console.log with proper logging
  2. Add telemetry for discount type decisions
  3. Document in package README

Phase 5: Testing

  1. Unit tests for new discount type handling
  2. Integration tests for Stripe checkout with fixed discounts
  3. Test PPP + fixed discount interactions
  4. Test upgrade + fixed discount conflicts
  5. Test bulk + fixed discount scenarios

Key Files Reference

Core Pricing

  • packages/core/src/lib/pricing/format-prices-for-product.ts - Main pricing orchestrator
  • packages/core/src/lib/pricing/determine-coupon-to-apply.ts - Coupon selection logic
  • packages/core/src/lib/pricing/get-calculated-price.ts - Price calculation
  • packages/core/src/lib/pricing/stripe-checkout.ts - Stripe integration

Schemas & Types

  • packages/core/src/schemas/merchant-coupon-schema.ts - MerchantCoupon schema
  • packages/core/src/types.ts - Type definitions (FormattedPrice, PaymentsAdapter, etc.)

Adapters

  • packages/core/src/providers/stripe.ts - Stripe payment adapter implementation
  • packages/core/src/adapters.ts - CourseBuilderAdapter interface

Supporting Files

  • packages/core/src/lib/pricing/parity-coupon.ts - PPP discount calculation
  • packages/core/src/lib/pricing/bulk-coupon.ts - Bulk discount calculation

Risk Mitigation

Revenue Impact Risks

  1. Double Discounting Prevention

    • Validate merchant coupons have only one discount type
    • Add explicit guards in stripeCheckout to prevent stacking
    • Log all discount decisions for audit trail
  2. Negative Price Protection

    • Clamp calculated prices to minimum $0
    • Add validation in getCalculatedPrice
    • Alert/log when clamping occurs
  3. Coupon Metadata Integrity

    • Store complete discount information in checkout metadata
    • Validate amounts match between CourseBuilder and Stripe
    • Add reconciliation checks in webhook processing

Backward Compatibility

  1. Existing Percentage Coupons

    • Continue to work via existing flow
    • No changes to PPP/Bulk logic
    • Upgrade flow unchanged
  2. Database Schema

    • amountDiscount is nullable (backward compatible)
    • Existing coupons have percentageDiscount only
  3. API Compatibility

    • FormattedPrice type extended (not modified)
    • New fields are optional

Testing Strategy

Unit Tests

  • determineCouponToApply: fixed vs percentage precedence
  • getCalculatedPrice: fixed + percentage combinations
  • formatPricesForProduct: all discount scenarios
  • Schema validation: mutual exclusivity

Integration Tests

  • Stripe checkout with fixed merchant coupon
  • Stripe checkout with upgrade + fixed merchant coupon
  • PPP + fixed discount interaction
  • Bulk + fixed discount (should prefer better discount)

Edge Cases

  • Fixed discount > product price (clamp to 0)
  • Both amountDiscount and percentageDiscount set (validation error)
  • Fixed discount + upgrade conflict (precedence rules)
  • Currency mismatch (USD assumption)

Success Criteria

✅ Fixed-amount merchant coupons create valid Stripe checkout sessions ✅ Percentage-based coupons continue to work unchanged ✅ No revenue leaks from double-discounting ✅ All edge cases produce non-negative prices ✅ Logging provides audit trail for discount decisions ✅ Tests cover all discount type combinations ✅ Documentation updated for new coupon types ✅ Type safety maintained throughout

Flat Discount Coupons - Code Changes Quick Reference

Schema Changes

1. Update MerchantCoupon Schema

File: packages/core/src/schemas/merchant-coupon-schema.ts

// BEFORE
export const merchantCouponSchema = z.object({
  id: z.string().max(191),
  identifier: z.string().max(191).optional().nullable(),
  status: z.number().int().default(0),
  merchantAccountId: z.string().max(191),
  percentageDiscount: z.coerce.number().refine((value) => {
    const decimalPlaces = value.toString().split('.')[1]?.length || 0
    return decimalPlaces <= 2
  }),
  type: z.string().max(191),
})

// AFTER
export const merchantCouponSchema = z.object({
  id: z.string().max(191),
  identifier: z.string().max(191).optional().nullable(),
  status: z.number().int().default(0),
  merchantAccountId: z.string().max(191),
  percentageDiscount: z.coerce.number().optional().refine((value) => {
    if (value === undefined) return true
    const decimalPlaces = value.toString().split('.')[1]?.length || 0
    return decimalPlaces <= 2
  }),
  amountDiscount: z.number().int().optional(), // NEW: cents, integer
  type: z.string().max(191),
}).refine(
  (data) => {
    // Prevent both discount types
    const hasPercent = data.percentageDiscount !== undefined && data.percentageDiscount > 0
    const hasAmount = data.amountDiscount !== undefined && data.amountDiscount > 0
    return !(hasPercent && hasAmount)
  },
  { message: "Coupon cannot have both percentageDiscount and amountDiscount" }
)

2. Update Types

File: packages/core/src/types.ts

// Add to exports
export type DiscountType = 'fixed' | 'percentage' | 'ppp' | 'bulk' | 'none'

// Update MinimalMerchantCoupon type (around line 263)
export type MinimalMerchantCoupon = Omit<
  MerchantCouponWithCountry,
  'identifier' | 'merchantAccountId'
> & {
  amountDiscount?: number  // NEW
}

// Update FormattedPrice type (around line 270)
export type FormattedPrice = {
  id: string
  quantity: number
  unitPrice: number
  fullPrice: number
  fixedDiscountForUpgrade: number
  appliedFixedDiscount?: number  // NEW: from merchant coupon
  calculatedPrice: number
  availableCoupons: Array<Omit<MerchantCouponWithCountry, 'identifier'> | undefined>
  appliedMerchantCoupon?: MinimalMerchantCoupon
  appliedDiscountType?: DiscountType  // NEW
  upgradeFromPurchaseId?: string
  upgradeFromPurchase?: Purchase
  upgradedProduct?: ProductWithPrices | null
  bulk: boolean
  usedCouponId?: string
  usedCoupon?: Coupon | null
  defaultCoupon?: Coupon | null
}

// Update PaymentsAdapter interface (around line 131)
export interface PaymentsAdapter {
  getCouponPercentOff(identifier: string): Promise<number>
  getCouponAmountOff(identifier: string): Promise<number>  // NEW
  createCoupon(params: Stripe.CouponCreateParams): Promise<string>
  // ... rest unchanged
}

Business Logic Changes

3. Update determineCouponToApply

File: packages/core/src/lib/pricing/determine-coupon-to-apply.ts

// Add to return type (around line 127)
export const determineCouponToApply = async (
  params: DetermineCouponToApplyParams,
) => {
  // ... existing logic ...

  // Determine appliedDiscountType
  const appliedDiscountType: DiscountType = (() => {
    if (!couponToApply) return 'none'
    if (couponToApply.type === 'ppp') return 'ppp'
    if (couponToApply.type === 'bulk') return 'bulk'
    // NEW: Check for fixed amount discount
    if (couponToApply.amountDiscount && couponToApply.amountDiscount > 0) {
      return 'fixed'
    }
    if (couponToApply.percentageDiscount && couponToApply.percentageDiscount > 0) {
      return 'percentage'
    }
    return 'none'
  })()

  return {
    appliedMerchantCoupon: couponToApply || undefined,
    appliedCouponType,  // existing
    appliedDiscountType,  // NEW
    availableCoupons,
    bulk: consideredBulk,
  }
}

4. Update formatPricesForProduct

File: packages/core/src/lib/pricing/format-prices-for-product.ts

export async function formatPricesForProduct(
  options: FormatPricesForProductOptions,
): Promise<FormattedPrice> {
  // ... existing setup code ...

  const {
    appliedMerchantCoupon,
    appliedCouponType,
    appliedDiscountType,  // NEW
    ...result
  } = await determineCouponToApply({
    // ... existing params
  })

  // ... existing upgrade discount logic ...

  const unitPrice: number = price.unitAmount
  const fullPrice: number = unitPrice * quantity - fixedDiscountForUpgrade

  // NEW: Extract fixed discount from merchant coupon
  const appliedFixedDiscount = appliedDiscountType === 'fixed'
    ? (appliedMerchantCoupon?.amountDiscount || 0) / 100  // Convert cents to dollars
    : 0

  // Extract percentage discount (existing, but update for clarity)
  const percentOfDiscount = ['percentage', 'ppp', 'bulk'].includes(appliedDiscountType || '')
    ? appliedMerchantCoupon?.percentageDiscount
    : undefined

  // Handle conflicts: If upgrade has fixed discount AND merchant coupon has fixed discount
  const effectiveFixedDiscount = fixedDiscountForUpgrade > 0 && appliedFixedDiscount > 0
    ? Math.max(fixedDiscountForUpgrade, appliedFixedDiscount)  // Take better discount
    : fixedDiscountForUpgrade + appliedFixedDiscount

  const upgradeDetails = // ... existing ...

  return {
    ...product,
    quantity,
    unitPrice,
    fullPrice,
    fixedDiscountForUpgrade,
    appliedFixedDiscount,  // NEW
    appliedDiscountType,   // NEW
    calculatedPrice: getCalculatedPrice({
      unitPrice,
      percentOfDiscount,
      fixedDiscount: effectiveFixedDiscount,  // UPDATED
      quantity,
    }),
    availableCoupons: result.availableCoupons,
    appliedMerchantCoupon,
    ...(usedCoupon?.merchantCouponId === appliedMerchantCoupon?.id && {
      usedCouponId,
    }),
    bulk: result.bulk,
    ...upgradeDetails,
  }
}

5. Update getCalculatedPrice (if needed)

File: packages/core/src/lib/pricing/get-calculated-price.ts

// Current implementation already handles fixedDiscount
// Just ensure prices don't go negative:

export function getCalculatedPrice({
  unitPrice,
  percentOfDiscount = 0,
  quantity = 1,
  fixedDiscount = 0,
}: GetCalculatePriceOptions) {
  const fullPrice = unitPrice * quantity
  const discountMultiplier = 1 - percentOfDiscount
  const calculatedPrice = (
    (fullPrice - fixedDiscount) * discountMultiplier
  ).toFixed(2)

  // NEW: Ensure non-negative
  return Math.max(0, Number(calculatedPrice))
}

Stripe Integration Changes

6. Update stripeCheckout

File: packages/core/src/lib/pricing/stripe-checkout.ts

export async function stripeCheckout({
  params,
  config,
  adapter,
}: {
  params: CheckoutParams
  config: PaymentsProviderConsumerConfig
  adapter?: CourseBuilderAdapter
}): Promise<any> {
  // ... existing setup ...

  const merchantCoupon = couponId
    ? await adapter.getMerchantCoupon(couponId as string)
    : null

  // NEW: Determine if this is a fixed amount coupon
  const isMerchantCouponFixedAmount =
    merchantCoupon?.amountDiscount && merchantCoupon.amountDiscount > 0

  const stripeCouponPercentOff =
    merchantCoupon && merchantCoupon.identifier && !isMerchantCouponFixedAmount
      ? await config.paymentsAdapter.getCouponPercentOff(merchantCoupon.identifier)
      : 0

  let discounts = []
  let appliedPPPStripeCouponId: string | undefined | null = undefined
  let upgradedFromPurchaseId: string | undefined | null = undefined

  const isUpgrade = Boolean(
    (availableUpgrade || upgradeFromPurchase?.status === 'Restricted') &&
      upgradeFromPurchase,
  )

  const TWELVE_FOUR_HOURS_FROM_NOW = Math.floor(
    add(new Date(), { hours: 12 }).getTime() / 1000,
  )

  if (isUpgrade && upgradeFromPurchase && loadedProduct && customerId) {
    // ... existing upgrade logic ...

    if (fixedDiscountForIndividualUpgrade > 0) {
      // ... existing upgrade coupon creation ...
    }
  }
  // NEW: Handle fixed-amount merchant coupons
  else if (merchantCoupon && isMerchantCouponFixedAmount) {
    const amountOffInCents = merchantCoupon.amountDiscount!

    // Create transient amount_off coupon (similar to upgrade flow)
    const couponId = await config.paymentsAdapter.createCoupon({
      amount_off: amountOffInCents,
      name: `${merchantCoupon.type} discount`,
      max_redemptions: 1,
      redeem_by: TWELVE_FOUR_HOURS_FROM_NOW,
      currency: 'USD',
      applies_to: {
        products: [merchantProductIdentifier],
      },
    })

    discounts.push({
      coupon: couponId,
    })

    // Log for observability
    console.log('[Stripe Checkout] Applied fixed-amount merchant coupon', {
      merchantCouponId: merchantCoupon.id,
      amountOffInCents,
      transientStripeCouponId: couponId,
    })
  }
  // EXISTING: Percentage-based merchant coupons
  else if (merchantCoupon && merchantCoupon.identifier) {
    const isNotPPP = merchantCoupon.type !== 'ppp'
    if (isNotPPP || quantity === 1) {
      appliedPPPStripeCouponId =
        merchantCoupon.type === 'ppp' ? merchantCoupon?.identifier : undefined
      const promotionCodeId = await config.paymentsAdapter.createPromotionCode({
        coupon: merchantCoupon.identifier,
        max_redemptions: 1,
        expires_at: TWELVE_FOUR_HOURS_FROM_NOW,
      })
      discounts.push({
        promotion_code: promotionCodeId,
      })
    }
  }

  // ... rest of checkout session creation ...

  const metadata = CheckoutSessionMetadataSchema.parse({
    // ... existing metadata ...

    // NEW: Add discount type information
    ...(merchantCoupon && {
      discountType: isMerchantCouponFixedAmount ? 'fixed' : 'percentage',
      discountAmount: isMerchantCouponFixedAmount
        ? merchantCoupon.amountDiscount
        : Math.round((stripeCouponPercentOff || 0) * 100),
    }),
  })

  // ... rest unchanged
}

7. Add getCouponAmountOff to Stripe Adapter

File: packages/core/src/providers/stripe.ts

export class StripePaymentAdapter implements PaymentsAdapter {
  // ... existing methods ...

  async getCouponPercentOff(identifier: string) {
    const coupon = await this.stripe.coupons.retrieve(identifier)
    return coupon && coupon.percent_off ? coupon.percent_off / 100 : 0
  }

  // NEW
  async getCouponAmountOff(identifier: string) {
    const coupon = await this.stripe.coupons.retrieve(identifier)
    return coupon && coupon.amount_off ? coupon.amount_off : 0
  }

  // ... rest unchanged
}

// Update mock adapter
export const mockStripeAdapter: PaymentsAdapter = {
  getCouponPercentOff: async () => 0,
  getCouponAmountOff: async () => 0,  // NEW
  // ... rest unchanged
}

Database Migration

8. Add Database Column

Create migration file: packages/adapter-drizzle/drizzle/migrations/XXXX-add-merchant-coupon-amount-discount.sql

-- Add amountDiscount column to MerchantCoupon table
ALTER TABLE MerchantCoupon
ADD COLUMN amountDiscount INTEGER NULL;

-- Add check constraint to prevent both discount types
-- Note: Syntax varies by database (MySQL, PostgreSQL, etc.)
-- MySQL version:
ALTER TABLE MerchantCoupon
ADD CONSTRAINT chk_single_discount_type CHECK (
  (percentageDiscount IS NOT NULL AND percentageDiscount > 0 AND (amountDiscount IS NULL OR amountDiscount = 0)) OR
  (amountDiscount IS NOT NULL AND amountDiscount > 0 AND (percentageDiscount IS NULL OR percentageDiscount = 0)) OR
  (percentageDiscount IS NULL OR percentageDiscount = 0) AND (amountDiscount IS NULL OR amountDiscount = 0)
);

-- Add index for queries
CREATE INDEX idx_merchant_coupon_amount_discount ON MerchantCoupon(amountDiscount);

9. Update Drizzle Schema

File: packages/adapter-drizzle/src/schemas/merchant-coupon-schema.ts (or similar)

export const merchantCoupons = mysqlTable('MerchantCoupon', {
  id: varchar('id', { length: 191 }).primaryKey(),
  identifier: varchar('identifier', { length: 191 }),
  status: int('status').default(0),
  merchantAccountId: varchar('merchantAccountId', { length: 191 }),
  percentageDiscount: decimal('percentageDiscount', { precision: 3, scale: 2 }),
  amountDiscount: int('amountDiscount'),  // NEW
  type: varchar('type', { length: 191 }),
})

Testing Checklist

Unit Tests

File: packages/core/src/lib/pricing/format-prices-for-product.test.ts

describe('formatPricesForProduct with fixed-amount coupons', () => {
  it('applies fixed-amount merchant coupon correctly', async () => {
    // Test fixed amount discount
  })

  it('prefers fixed discount over percentage when specified', async () => {
    // Test precedence
  })

  it('handles upgrade + fixed merchant coupon conflict', async () => {
    // Test conflict resolution
  })

  it('ensures non-negative prices', async () => {
    // Test price clamping
  })
})

File: packages/core/src/lib/pricing/determine-coupon-to-apply.test.ts

describe('determineCouponToApply with fixed-amount coupons', () => {
  it('returns appliedDiscountType as "fixed" for amount-based coupons', async () => {
    // Test discount type detection
  })

  it('validates mutual exclusivity of discount types', async () => {
    // Test validation
  })
})

File: packages/core/src/lib/pricing/stripe-checkout.test.ts

describe('stripeCheckout with fixed-amount coupons', () => {
  it('creates transient amount_off coupon for fixed merchant coupons', async () => {
    // Test Stripe coupon creation
  })

  it('includes discount metadata in checkout session', async () => {
    // Test metadata
  })
})

Integration Tests

describe('Fixed Amount Coupon E2E', () => {
  it('completes checkout with fixed-amount coupon', async () => {
    // Full checkout flow test
  })

  it('handles PPP ineligibility with fixed-amount fallback', async () => {
    // Test PPP + fixed interaction
  })
})

Validation Checklist

  • Schema migration applied
  • TypeScript compilation passes
  • Unit tests pass
  • Integration tests pass
  • No console.log statements (use logger)
  • Documentation updated
  • Edge cases covered:
    • Fixed discount > product price
    • Both discount types set (should fail validation)
    • Upgrade + merchant fixed conflict
    • PPP + fixed interaction
    • Bulk + fixed interaction
  • Stripe checkout sessions created successfully
  • Metadata includes discount type and amount
  • Non-negative price enforcement
  • Backward compatibility verified

Rollout Considerations

Feature Flag (Optional)

// In config/environment
const FIXED_AMOUNT_COUPONS_ENABLED = process.env.ENABLE_FIXED_COUPONS === 'true'

// In determineCouponToApply
if (FIXED_AMOUNT_COUPONS_ENABLED && coupon.amountDiscount) {
  // New logic
} else {
  // Existing logic
}

Observability

// Replace console.log with proper logging
import { logger } from '@/server/logger'

logger.info('Applied fixed-amount coupon', {
  merchantCouponId: coupon.id,
  amountDiscount: coupon.amountDiscount,
  productId,
  userId,
})

Monitoring Queries

-- Find all fixed-amount coupons
SELECT * FROM MerchantCoupon WHERE amountDiscount IS NOT NULL AND amountDiscount > 0;

-- Find coupons with both discount types (should be empty)
SELECT * FROM MerchantCoupon
WHERE percentageDiscount > 0 AND amountDiscount > 0;

-- Usage analytics
SELECT
  type,
  COUNT(*) as usage_count,
  AVG(amountDiscount) as avg_discount
FROM Purchase p
JOIN MerchantCoupon mc ON p.merchantCouponId = mc.id
WHERE mc.amountDiscount IS NOT NULL
GROUP BY type;

Flat Discount Coupons - Decision Flow & Reference

Discount Type Decision Tree

graph TD
    Start([Checkout Request]) --> GetCoupon[Get Merchant Coupon]
    GetCoupon --> HasCoupon{Has Merchant<br/>Coupon?}

    HasCoupon -->|No| CheckPPP[Check PPP Eligibility]
    HasCoupon -->|Yes| CheckType{Coupon Has<br/>amountDiscount?}

    CheckType -->|Yes| FixedPath[FIXED DISCOUNT PATH]
    CheckType -->|No| HasPercent{Has<br/>percentageDiscount?}

    HasPercent -->|Yes| PercentPath[PERCENTAGE PATH]
    HasPercent -->|No| CheckPPP

    CheckPPP --> IsPPPEligible{PPP Eligible?<br/>country, quantity=1,<br/>no full-price purchases}

    IsPPPEligible -->|Yes| PPPPath[PPP PATH]
    IsPPPEligible -->|No| CheckBulk[Check Bulk Eligibility]

    CheckBulk --> IsBulk{quantity > 1 OR<br/>existing bulk purchase?}

    IsBulk -->|Yes| BulkPath[BULK PATH]
    IsBulk -->|No| NoDiscount[NO DISCOUNT]

    %% Fixed Path Details
    FixedPath --> IsUpgrade1{Is Upgrade?}
    IsUpgrade1 -->|Yes| ConflictCheck{Fixed Upgrade<br/>Discount > 0?}
    IsUpgrade1 -->|No| ApplyFixed[Apply Fixed Amount<br/>from merchantCoupon.amountDiscount]

    ConflictCheck -->|Yes| TakeBetter[Take Better Discount<br/>max(upgrade, merchant)]
    ConflictCheck -->|No| ApplyFixed

    TakeBetter --> CreateFixedCoupon[Create Transient<br/>amount_off Coupon]
    ApplyFixed --> CreateFixedCoupon

    CreateFixedCoupon --> StripeCheckout1[Stripe Checkout]

    %% Percentage Path Details
    PercentPath --> IsUpgrade2{Is Upgrade?}
    IsUpgrade2 -->|Yes| CalcUpgrade[Calculate Upgrade Discount<br/>+ Apply Percentage]
    IsUpgrade2 -->|No| UsePromoCode[Use Promotion Code<br/>with existing Stripe coupon]

    CalcUpgrade --> CreateAmountOff[Create amount_off Coupon<br/>for combined discount]
    CreateAmountOff --> StripeCheckout2[Stripe Checkout]
    UsePromoCode --> StripeCheckout2

    %% PPP Path
    PPPPath --> GetPPPPercent[Get PPP Discount %<br/>based on country]
    GetPPPPercent --> PPPUpgrade{Is Upgrade?}
    PPPUpgrade -->|Yes| PPPUpgradeCalc[Combine PPP %<br/>with upgrade discount]
    PPPUpgrade -->|No| PPPPromo[Create Promotion Code<br/>for PPP coupon]

    PPPUpgradeCalc --> StripeCheckout3[Stripe Checkout]
    PPPPromo --> StripeCheckout3

    %% Bulk Path
    BulkPath --> GetBulkPercent[Get Bulk Discount %<br/>based on quantity]
    GetBulkPercent --> BulkPromo[Create Promotion Code<br/>for bulk coupon]
    BulkPromo --> StripeCheckout4[Stripe Checkout]

    %% No Discount Path
    NoDiscount --> CheckUpgradeOnly{Is Upgrade?}
    CheckUpgradeOnly -->|Yes| UpgradeOnly[Apply Upgrade Discount Only]
    CheckUpgradeOnly -->|No| FullPrice[Full Price]
    UpgradeOnly --> StripeCheckout5[Stripe Checkout]
    FullPrice --> StripeCheckout5

    style FixedPath fill:#e1f5e1
    style PercentPath fill:#e3f2fd
    style PPPPath fill:#fff3e0
    style BulkPath fill:#f3e5f5
    style NoDiscount fill:#ffebee
Loading

Discount Type Priority Matrix

Scenario Priority 1 Priority 2 Priority 3 Priority 4 Result
Merchant coupon with amountDiscount ✅ Fixed Amount - - - Fixed discount applied
Merchant coupon with percentageDiscount ✅ Percentage - - - Percentage applied
PPP eligible + no merchant coupon ✅ PPP % - - - PPP applied
Bulk (qty>1) + no merchant coupon ✅ Bulk % - - - Bulk applied
PPP eligible + merchant percentage Compare ✅ Better discount wins - - Better of PPP vs merchant
Bulk + merchant percentage Compare ✅ Better discount wins - - Better of bulk vs merchant
Fixed merchant + upgrade ✅ Max(fixed, upgrade) - - - Better discount wins
Percentage merchant + upgrade ✅ Combine both - - - Combined discount
No coupons + upgrade ✅ Upgrade discount - - - Upgrade applied
No coupons + no upgrade ❌ No discount - - - Full price

Stripe Coupon Creation Decision

graph LR
    Start([stripeCheckout]) --> CheckScenario{Scenario?}

    CheckScenario -->|Upgrade + fixed| UpgradeFixed[Create amount_off coupon<br/>amount = fullPrice - calculatedPrice]
    CheckScenario -->|Merchant fixed| MerchantFixed[Create amount_off coupon<br/>amount = merchantCoupon.amountDiscount]
    CheckScenario -->|Merchant %| MerchantPercent[Create promotion_code<br/>for existing Stripe coupon]
    CheckScenario -->|PPP %| PPPPercent[Create promotion_code<br/>for PPP coupon]
    CheckScenario -->|Bulk %| BulkPercent[Create promotion_code<br/>for bulk coupon]

    UpgradeFixed --> AddDiscount1[Add to discounts array]
    MerchantFixed --> AddDiscount2[Add to discounts array]
    MerchantPercent --> AddDiscount3[Add to discounts array]
    PPPPercent --> AddDiscount4[Add to discounts array]
    BulkPercent --> AddDiscount5[Add to discounts array]

    AddDiscount1 --> CreateSession[Create Checkout Session]
    AddDiscount2 --> CreateSession
    AddDiscount3 --> CreateSession
    AddDiscount4 --> CreateSession
    AddDiscount5 --> CreateSession

    style UpgradeFixed fill:#ffd700
    style MerchantFixed fill:#90ee90
    style MerchantPercent fill:#87ceeb
    style PPPPercent fill:#ffb347
    style BulkPercent fill:#dda0dd
Loading

Price Calculation Formula

Current Formula

fullPrice = (unitPrice × quantity) - fixedDiscountForUpgrade
calculatedPrice = (fullPrice - 0) × (1 - percentOfDiscount)

Updated Formula (with merchant fixed discount)

fullPrice = (unitPrice × quantity) - fixedDiscountForUpgrade

If discountType === 'fixed':
  effectiveFixedDiscount = max(fixedDiscountForUpgrade, appliedFixedDiscount)
  calculatedPrice = fullPrice - effectiveFixedDiscount

Else if discountType in ['percentage', 'ppp', 'bulk']:
  calculatedPrice = fullPrice × (1 - percentOfDiscount)

Else:
  calculatedPrice = fullPrice

// Always ensure non-negative
calculatedPrice = max(0, calculatedPrice)

Comparison Table: Current vs Proposed

Aspect Current System Proposed System
Merchant Coupon Types Percentage only Percentage + Fixed Amount
Fixed Discount Support Upgrades only Upgrades + Merchant coupons
Schema Fields percentageDiscount percentageDiscount + amountDiscount
Stripe Coupon Creation • Promotion codes for merchant
• amount_off for upgrades
• Promotion codes for % merchant
• amount_off for fixed merchant
• amount_off for upgrades
Discount Type Tracking Implicit (via type field) Explicit (appliedDiscountType enum)
Validation None Mutual exclusivity check
Price Calculation (fullPrice - upgrade) × (1 - %) Conditional based on discount type
Conflict Resolution N/A Max of conflicting fixed discounts
Metadata Basic coupon info Includes discountType and discountAmount

Common Scenarios & Examples

Scenario 1: Simple Fixed-Amount Coupon

Product Price: $100
Merchant Coupon: $20 off (amountDiscount = 2000 cents)
Quantity: 1

Flow:
1. determineCouponToApply → returns amountDiscount=2000, discountType='fixed'
2. formatPricesForProduct → appliedFixedDiscount = $20
3. calculatedPrice = $100 - $20 = $80
4. stripeCheckout → creates amount_off=2000 coupon
5. Stripe session → customer pays $80

Scenario 2: Fixed Coupon + Upgrade Conflict

Product Price: $200
Upgrade Discount: $50 (from prior $50 purchase)
Merchant Coupon: $30 off (amountDiscount = 3000 cents)
Quantity: 1

Flow:
1. getFixedDiscountForIndividualUpgrade → returns $50
2. determineCouponToApply → returns amountDiscount=3000, discountType='fixed'
3. formatPricesForProduct:
   - fixedDiscountForUpgrade = $50
   - appliedFixedDiscount = $30
   - effectiveFixedDiscount = max($50, $30) = $50  // Better discount
4. calculatedPrice = $200 - $50 = $150
5. stripeCheckout → creates amount_off=5000 coupon
6. Stripe session → customer pays $150

Scenario 3: PPP vs Fixed Coupon

Product Price: $100
Country: India (PPP = 60% off)
Merchant Coupon: $25 off (amountDiscount = 2500 cents)
Quantity: 1

Decision:
- PPP would give: $100 × 0.40 = $40 (customer pays $40)
- Fixed would give: $100 - $25 = $75 (customer pays $75)
- PPP is better → PPP wins

But if merchant coupon is $70 off:
- Fixed would give: $100 - $70 = $30 (customer pays $30)
- Fixed is better → Fixed wins

Note: This requires explicit comparison logic in determineCouponToApply

Scenario 4: Bulk Purchase (No Fixed Discount)

Product Price: $100
Quantity: 5
No merchant coupon

Flow:
1. determineCouponToApply → detects quantity=5 → bulk discount 20%
2. calculatedPrice = ($100 × 5) × 0.80 = $400
3. No fixed discounts applied
4. Customer pays $400 total ($80 per seat)

Scenario 5: Percentage Merchant Coupon (Unchanged)

Product Price: $100
Merchant Coupon: 25% off (percentageDiscount = 0.25)
Quantity: 1

Flow:
1. determineCouponToApply → returns percentageDiscount=0.25, discountType='percentage'
2. calculatedPrice = $100 × 0.75 = $75
3. stripeCheckout → creates promotion_code for existing Stripe coupon
4. Stripe session → customer pays $75

This flow is UNCHANGED from current system

Edge Cases & Validation

Edge Case 1: Fixed Discount > Product Price

Product Price: $50
Merchant Coupon: $75 off

Result: calculatedPrice = max(0, $50 - $75) = $0
Customer pays: $0 (free)

Edge Case 2: Both Discount Types Set (Invalid)

Merchant Coupon:
  percentageDiscount: 0.25
  amountDiscount: 2000

Result: Schema validation fails
Error: "Coupon cannot have both percentageDiscount and amountDiscount"

Edge Case 3: PPP + Fixed Merchant + Upgrade

Product Price: $200
Upgrade: $60
PPP: 50% off
Fixed Merchant: $40 off
Quantity: 1

Decision Tree:
1. PPP ineligible (has upgrade = prior purchase)
2. Fixed merchant available
3. effectiveFixedDiscount = max($60 upgrade, $40 merchant) = $60
4. calculatedPrice = $200 - $60 = $140

Testing Scenarios Checklist

  • Fixed Amount Discounts

    • Simple fixed-amount merchant coupon
    • Fixed amount > product price (clamps to 0)
    • Fixed amount < product price (normal flow)
    • Fixed amount in cents converts correctly to dollars
  • Conflict Resolution

    • Fixed merchant + upgrade fixed (takes max)
    • Fixed merchant + percentage merchant (validation fails)
    • Fixed merchant + PPP (compare effectiveness)
    • Fixed merchant + bulk (fixed wins, no bulk on individual)
  • Backward Compatibility

    • Percentage merchant coupons work unchanged
    • PPP flow unchanged
    • Bulk flow unchanged
    • Upgrade-only flow unchanged
  • Stripe Integration

    • amount_off coupon created for fixed merchant
    • Promotion code created for percentage merchant
    • Transient coupons expire correctly (12 hours)
    • Metadata includes discount type and amount
  • Data Validation

    • Cannot set both percentageDiscount and amountDiscount
    • amountDiscount must be integer (cents)
    • percentageDiscount must be 0.0-1.0
    • Prices never go negative

Implementation Order

gantt
    title Flat Discount Coupons Implementation
    dateFormat YYYY-MM-DD
    section Schema
    Add amountDiscount field          :a1, 2024-01-01, 1d
    Add validation constraint          :a2, after a1, 1d
    Update types                       :a3, after a1, 1d
    section Business Logic
    Update determineCouponToApply      :b1, after a3, 2d
    Update formatPricesForProduct      :b2, after b1, 2d
    Update getCalculatedPrice          :b3, after b2, 1d
    section Stripe
    Add getCouponAmountOff             :c1, after a3, 1d
    Update stripeCheckout              :c2, after b3, 3d
    section Testing
    Unit tests                         :d1, after c2, 3d
    Integration tests                  :d2, after d1, 2d
    section Deployment
    Code review                        :e1, after d2, 2d
    Deploy to staging                  :e2, after e1, 1d
    Monitor & validate                 :e3, after e2, 2d
    Deploy to production               :e4, after e3, 1d
Loading

Key Takeaways

  1. Fixed amount coupons are similar to upgrade discounts - reuse existing amount_off coupon creation pattern

  2. Mutual exclusivity is critical - A coupon MUST have either percentage OR amount, never both

  3. Conflict resolution uses "max discount" - When upgrade and merchant both offer fixed discounts, take the better one

  4. Stripe supports both patterns - Promotion codes for percentage, direct coupons for amount_off

  5. Backward compatibility is maintained - All existing flows continue to work; this is purely additive

  6. Validation prevents revenue leaks - Non-negative prices and mutual exclusivity checks protect against errors

  7. Metadata enables debugging - Store discount type and amount in checkout session metadata for audit trail

Flat Discount Coupons - Comprehensive Testing Plan

Testing Overview

This testing plan covers all aspects of the flat discount coupon implementation, including unit tests, integration tests, and end-to-end scenarios. Tests are organized by component and include clear setup requirements, test data, and expected outcomes.

Test Environment Setup

Prerequisites

  • Database with test data (products, users, merchant accounts)
  • Stripe test mode API keys configured
  • Mock Stripe adapter for unit tests
  • Real Stripe adapter for integration tests

Test Data Requirements

Products

const testProducts = {
  basic: { id: 'prod_basic', name: 'Basic Course', price: 10000 }, // $100
  bundle: { id: 'prod_bundle', name: 'Bundle', price: 20000 }, // $200
  expensive: { id: 'prod_expensive', name: 'Premium', price: 50000 }, // $500
  cheap: { id: 'prod_cheap', name: 'Mini Course', price: 5000 }, // $50
}

Merchant Coupons

const testCoupons = {
  fixedAmount20: {
    id: 'coupon_fixed_20',
    amountDiscount: 2000, // $20 off
    type: 'special',
  },
  fixedAmount75: {
    id: 'coupon_fixed_75',
    amountDiscount: 7500, // $75 off
    type: 'special',
  },
  percentage25: {
    id: 'coupon_percent_25',
    percentageDiscount: 0.25, // 25% off
    type: 'special',
  },
  ppp60: {
    id: 'coupon_ppp_india',
    percentageDiscount: 0.60, // 60% off
    type: 'ppp',
    country: 'IN',
  },
  bulk20: {
    id: 'coupon_bulk_5seats',
    percentageDiscount: 0.20, // 20% off
    type: 'bulk',
  },
  invalid: {
    id: 'coupon_invalid',
    percentageDiscount: 0.25,
    amountDiscount: 2000, // Both set - should fail validation
  },
}

Purchases (for upgrade scenarios)

const testPurchases = {
  validPurchase: {
    id: 'purchase_valid',
    productId: 'prod_basic',
    status: 'Valid',
    totalAmount: 10000,
  },
  restrictedPurchase: {
    id: 'purchase_restricted',
    productId: 'prod_basic',
    status: 'Restricted',
    totalAmount: 4000, // Paid with PPP
  },
}

Unit Tests

1. Schema Validation Tests

File: packages/core/src/schemas/merchant-coupon-schema.test.ts

Test Case 1.1: Valid Fixed Amount Coupon

it('accepts coupon with amountDiscount only', () => {
  const coupon = {
    id: 'test_1',
    merchantAccountId: 'merchant_1',
    amountDiscount: 2000,
    type: 'special',
    status: 1,
  }

  expect(() => merchantCouponSchema.parse(coupon)).not.toThrow()
})

Test Case 1.2: Valid Percentage Coupon

it('accepts coupon with percentageDiscount only', () => {
  const coupon = {
    id: 'test_2',
    merchantAccountId: 'merchant_1',
    percentageDiscount: 0.25,
    type: 'special',
    status: 1,
  }

  expect(() => merchantCouponSchema.parse(coupon)).not.toThrow()
})

Test Case 1.3: Reject Both Discount Types

it('rejects coupon with both discount types', () => {
  const coupon = {
    id: 'test_3',
    merchantAccountId: 'merchant_1',
    percentageDiscount: 0.25,
    amountDiscount: 2000,
    type: 'special',
    status: 1,
  }

  expect(() => merchantCouponSchema.parse(coupon)).toThrow(
    'Cannot have both percentageDiscount and amountDiscount'
  )
})

Test Case 1.4: Accept Coupon with No Discount

it('accepts coupon with neither discount type (inactive)', () => {
  const coupon = {
    id: 'test_4',
    merchantAccountId: 'merchant_1',
    type: 'special',
    status: 0,
  }

  expect(() => merchantCouponSchema.parse(coupon)).not.toThrow()
})

Test Case 1.5: Amount Discount Must Be Integer

it('rejects amountDiscount with decimals', () => {
  const coupon = {
    id: 'test_5',
    merchantAccountId: 'merchant_1',
    amountDiscount: 20.5, // Invalid - must be integer cents
    type: 'special',
    status: 1,
  }

  expect(() => merchantCouponSchema.parse(coupon)).toThrow()
})

2. determineCouponToApply Tests

File: packages/core/src/lib/pricing/determine-coupon-to-apply.test.ts

Test Case 2.1: Fixed Amount Takes Priority

it('returns appliedDiscountType as "fixed" for amount-based coupons', async () => {
  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_fixed_20',
    country: 'US',
    quantity: 1,
    productId: 'prod_basic',
    purchaseToBeUpgraded: null,
    autoApplyPPP: true,
    usedCoupon: null,
  })

  expect(result.appliedDiscountType).toBe('fixed')
  expect(result.appliedMerchantCoupon?.amountDiscount).toBe(2000)
})

Test Case 2.2: Percentage Discount Detection

it('returns appliedDiscountType as "percentage" for percentage coupons', async () => {
  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_percent_25',
    country: 'US',
    quantity: 1,
    productId: 'prod_basic',
    purchaseToBeUpgraded: null,
    autoApplyPPP: true,
    usedCoupon: null,
  })

  expect(result.appliedDiscountType).toBe('percentage')
  expect(result.appliedMerchantCoupon?.percentageDiscount).toBe(0.25)
})

Test Case 2.3: PPP Takes Priority Over Fixed When Better

it('prefers PPP over fixed amount when PPP provides better discount', async () => {
  // PPP 60% off on $100 = $40 final price
  // Fixed $20 off on $100 = $80 final price
  // PPP is better

  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_fixed_20',
    country: 'IN', // India has 60% PPP discount
    quantity: 1,
    productId: 'prod_basic',
    purchaseToBeUpgraded: null,
    autoApplyPPP: true,
    usedCoupon: null,
  })

  expect(result.appliedDiscountType).toBe('ppp')
  expect(result.appliedMerchantCoupon?.percentageDiscount).toBe(0.60)
})

Test Case 2.4: Fixed Takes Priority Over PPP When Better

it('prefers fixed amount over PPP when fixed provides better discount', async () => {
  // Fixed $75 off on $100 = $25 final price
  // PPP 60% off on $100 = $40 final price
  // Fixed is better

  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_fixed_75',
    country: 'IN',
    quantity: 1,
    productId: 'prod_basic',
    purchaseToBeUpgraded: null,
    autoApplyPPP: true,
    usedCoupon: null,
  })

  expect(result.appliedDiscountType).toBe('fixed')
  expect(result.appliedMerchantCoupon?.amountDiscount).toBe(7500)
})

Test Case 2.5: Bulk Purchases Ignore Fixed Coupons

it('does not apply fixed coupon for bulk purchases', async () => {
  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_fixed_20',
    country: 'US',
    quantity: 5, // Bulk purchase
    productId: 'prod_basic',
    purchaseToBeUpgraded: null,
    autoApplyPPP: true,
    usedCoupon: null,
  })

  // Should fall back to bulk discount instead
  expect(result.appliedDiscountType).toBe('bulk')
})

3. formatPricesForProduct Tests

File: packages/core/src/lib/pricing/format-prices-for-product.test.ts

Test Case 3.1: Basic Fixed Amount Discount

it('applies fixed-amount merchant coupon correctly', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic',
    merchantCouponId: 'coupon_fixed_20',
    quantity: 1,
  })

  expect(result.unitPrice).toBe(100)
  expect(result.fullPrice).toBe(100)
  expect(result.appliedFixedDiscount).toBe(20)
  expect(result.appliedDiscountType).toBe('fixed')
  expect(result.calculatedPrice).toBe(80)
})

Test Case 3.2: Fixed Discount Greater Than Price

it('clamps price to $0 when fixed discount exceeds price', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_cheap', // $50
    merchantCouponId: 'coupon_fixed_75', // $75 off
    quantity: 1,
  })

  expect(result.calculatedPrice).toBe(0)
  expect(result.appliedFixedDiscount).toBe(75)
})

Test Case 3.3: Fixed Discount Equals Price

it('results in $0 when fixed discount equals price', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic', // $100
    merchantCouponId: 'coupon_fixed_100', // $100 off
    quantity: 1,
  })

  expect(result.calculatedPrice).toBe(0)
})

Test Case 3.4: Percentage Discount (Unchanged Behavior)

it('applies percentage discount correctly (backward compatibility)', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic', // $100
    merchantCouponId: 'coupon_percent_25', // 25% off
    quantity: 1,
  })

  expect(result.unitPrice).toBe(100)
  expect(result.fullPrice).toBe(100)
  expect(result.appliedDiscountType).toBe('percentage')
  expect(result.calculatedPrice).toBe(75)
  expect(result.appliedFixedDiscount).toBeUndefined()
})

Test Case 3.5: Upgrade + Fixed Merchant Conflict (Max Wins)

it('takes max discount when upgrade and fixed merchant both apply', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_bundle', // $200
    merchantCouponId: 'coupon_fixed_20', // $20 off
    upgradeFromPurchaseId: 'purchase_valid', // $100 credit
    quantity: 1,
  })

  expect(result.fixedDiscountForUpgrade).toBe(100)
  expect(result.appliedFixedDiscount).toBe(20)
  // Should use the better discount
  expect(result.calculatedPrice).toBe(100) // $200 - $100 upgrade discount
})

Test Case 3.6: Upgrade + Fixed Merchant (Merchant Wins)

it('uses fixed merchant when better than upgrade discount', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_expensive', // $500
    merchantCouponId: 'coupon_fixed_200', // $200 off
    upgradeFromPurchaseId: 'purchase_basic', // $100 credit
    quantity: 1,
  })

  expect(result.fixedDiscountForUpgrade).toBe(100)
  expect(result.appliedFixedDiscount).toBe(200)
  expect(result.calculatedPrice).toBe(300) // $500 - $200 merchant discount
})

Test Case 3.7: PPP Upgrade with Restricted Purchase

it('handles PPP upgrade from restricted purchase correctly', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic', // $100, same product
    upgradeFromPurchaseId: 'purchase_restricted', // Paid $40 with PPP
    quantity: 1,
  })

  // Upgrading from Restricted to Unrestricted on same product
  expect(result.fixedDiscountForUpgrade).toBe(40)
  expect(result.calculatedPrice).toBe(60) // $100 - $40
})

Test Case 3.8: Bulk Purchase (No Fixed Discount)

it('does not apply fixed discount for bulk purchases', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic',
    quantity: 5,
  })

  expect(result.bulk).toBe(true)
  expect(result.appliedDiscountType).toBe('bulk')
  expect(result.appliedFixedDiscount).toBeUndefined()
  // Should have bulk percentage discount instead
})

4. getCalculatedPrice Tests

File: packages/core/src/lib/pricing/get-calculated-price.test.ts

Test Case 4.1: Fixed Discount Only

it('applies fixed discount without percentage', () => {
  const result = getCalculatedPrice({
    unitPrice: 100,
    quantity: 1,
    fixedDiscount: 20,
    percentOfDiscount: 0,
  })

  expect(result).toBe(80)
})

Test Case 4.2: Percentage Discount Only

it('applies percentage discount without fixed', () => {
  const result = getCalculatedPrice({
    unitPrice: 100,
    quantity: 1,
    fixedDiscount: 0,
    percentOfDiscount: 0.25,
  })

  expect(result).toBe(75)
})

Test Case 4.3: Both Discounts (Order Matters)

it('applies fixed discount before percentage (upgrade scenario)', () => {
  // (100 - 20) * 0.75 = 60
  const result = getCalculatedPrice({
    unitPrice: 100,
    quantity: 1,
    fixedDiscount: 20,
    percentOfDiscount: 0.25,
  })

  expect(result).toBe(60)
})

Test Case 4.4: Non-Negative Enforcement

it('clamps negative results to 0', () => {
  const result = getCalculatedPrice({
    unitPrice: 50,
    quantity: 1,
    fixedDiscount: 100,
    percentOfDiscount: 0,
  })

  expect(result).toBe(0)
})

Test Case 4.5: Quantity Multiplier

it('applies quantity before discounts', () => {
  // (100 * 3 = 300) - 50 = 250
  const result = getCalculatedPrice({
    unitPrice: 100,
    quantity: 3,
    fixedDiscount: 50,
    percentOfDiscount: 0,
  })

  expect(result).toBe(250)
})

Integration Tests

5. Stripe Checkout Integration

File: packages/core/src/lib/pricing/stripe-checkout.test.ts

Test Case 5.1: Create Transient amount_off Coupon for Fixed Merchant

it('creates transient amount_off coupon for fixed merchant coupons', async () => {
  const mockCreateCoupon = vi.fn().mockResolvedValue('stripe_coupon_123')
  const mockAdapter = {
    paymentsAdapter: {
      createCoupon: mockCreateCoupon,
      createCheckoutSession: vi.fn().mockResolvedValue('session_url'),
    },
  }

  await stripeCheckout({
    params: {
      productId: 'prod_basic',
      couponId: 'coupon_fixed_20',
      quantity: 1,
      bulk: false,
      cancelUrl: 'http://test.com/cancel',
    },
    config: mockConfig,
    adapter: mockAdapter,
  })

  expect(mockCreateCoupon).toHaveBeenCalledWith({
    amount_off: 2000,
    name: expect.stringContaining('discount'),
    max_redemptions: 1,
    redeem_by: expect.any(Number),
    currency: 'USD',
    applies_to: {
      products: [expect.any(String)],
    },
  })
})

Test Case 5.2: Use Promotion Code for Percentage Merchant

it('creates promotion code for percentage merchant coupons', async () => {
  const mockCreatePromotionCode = vi.fn().mockResolvedValue('promo_123')
  const mockAdapter = {
    paymentsAdapter: {
      createPromotionCode: mockCreatePromotionCode,
      createCheckoutSession: vi.fn().mockResolvedValue('session_url'),
    },
  }

  await stripeCheckout({
    params: {
      productId: 'prod_basic',
      couponId: 'coupon_percent_25',
      quantity: 1,
      bulk: false,
      cancelUrl: 'http://test.com/cancel',
    },
    config: mockConfig,
    adapter: mockAdapter,
  })

  expect(mockCreatePromotionCode).toHaveBeenCalledWith({
    coupon: expect.any(String), // Stripe coupon identifier
    max_redemptions: 1,
    expires_at: expect.any(Number),
  })
})

Test Case 5.3: Metadata Includes Discount Type

it('includes discount type and amount in checkout metadata', async () => {
  const mockCreateSession = vi.fn().mockResolvedValue('session_url')
  const mockAdapter = {
    paymentsAdapter: {
      createCoupon: vi.fn().mockResolvedValue('coupon_123'),
      createCheckoutSession: mockCreateSession,
    },
  }

  await stripeCheckout({
    params: {
      productId: 'prod_basic',
      couponId: 'coupon_fixed_20',
      quantity: 1,
      bulk: false,
      cancelUrl: 'http://test.com/cancel',
    },
    config: mockConfig,
    adapter: mockAdapter,
  })

  const sessionCall = mockCreateSession.mock.calls[0][0]
  expect(sessionCall.metadata).toMatchObject({
    discountType: 'fixed',
    discountAmount: 2000,
  })
})

Test Case 5.4: Upgrade Coupon Created Correctly

it('creates upgrade amount_off coupon with correct amount', async () => {
  const mockCreateCoupon = vi.fn().mockResolvedValue('upgrade_coupon_123')

  await stripeCheckout({
    params: {
      productId: 'prod_bundle',
      upgradeFromPurchaseId: 'purchase_valid',
      quantity: 1,
      bulk: false,
      cancelUrl: 'http://test.com/cancel',
    },
    config: mockConfig,
    adapter: mockAdapterWithUpgrade,
  })

  expect(mockCreateCoupon).toHaveBeenCalledWith(
    expect.objectContaining({
      amount_off: expect.any(Number),
      name: expect.stringContaining('Upgrade'),
    })
  )
})

Test Case 5.5: No Double Discount on Upgrade + Fixed Merchant

it('does not stack upgrade and fixed merchant discounts', async () => {
  const mockCreateCoupon = vi.fn().mockResolvedValue('coupon_123')

  await stripeCheckout({
    params: {
      productId: 'prod_bundle',
      couponId: 'coupon_fixed_20',
      upgradeFromPurchaseId: 'purchase_valid',
      quantity: 1,
      bulk: false,
      cancelUrl: 'http://test.com/cancel',
    },
    config: mockConfig,
    adapter: mockAdapter,
  })

  // Should only create ONE coupon (whichever discount is better)
  expect(mockCreateCoupon).toHaveBeenCalledTimes(1)
})

End-to-End Test Scenarios

6. Complete Checkout Flows

File: apps/course-builder-web/tests/e2e/checkout-fixed-discount.spec.ts

E2E Test 6.1: Fixed Amount Coupon Checkout

test('completes checkout with fixed-amount coupon', async ({ page }) => {
  // 1. Navigate to product page
  await page.goto('/products/prod_basic')

  // 2. Apply fixed discount coupon
  await page.fill('[data-test="coupon-input"]', 'FIXED20')
  await page.click('[data-test="apply-coupon"]')

  // 3. Verify discount displayed
  await expect(page.locator('[data-test="original-price"]')).toHaveText('$100.00')
  await expect(page.locator('[data-test="discount-amount"]')).toHaveText('-$20.00')
  await expect(page.locator('[data-test="final-price"]')).toHaveText('$80.00')

  // 4. Proceed to checkout
  await page.click('[data-test="checkout-button"]')

  // 5. Verify Stripe session created with correct amount
  const stripeUrl = await page.url()
  expect(stripeUrl).toContain('stripe.com/checkout')

  // Verify via Stripe API (in test mode)
  const session = await getStripeSession(extractSessionId(stripeUrl))
  expect(session.amount_total).toBe(8000) // $80.00 in cents
})

E2E Test 6.2: PPP vs Fixed Discount Selection

test('selects better discount between PPP and fixed amount', async ({ page, context }) => {
  // Set geolocation to India
  await context.setGeolocation({ latitude: 28.6139, longitude: 77.2090 })

  // Navigate to product
  await page.goto('/products/prod_basic')

  // Apply fixed coupon
  await page.fill('[data-test="coupon-input"]', 'FIXED20')
  await page.click('[data-test="apply-coupon"]')

  // Should show PPP discount instead (60% > $20)
  await expect(page.locator('[data-test="discount-type"]')).toHaveText('PPP Discount')
  await expect(page.locator('[data-test="final-price"]')).toHaveText('$40.00')
})

E2E Test 6.3: Upgrade with Fixed Discount

test('applies correct discount for upgrade scenario', async ({ page }) => {
  // Assume user has purchased basic product for $100
  await loginUser(page, 'user_with_basic_purchase')

  // Navigate to bundle upgrade
  await page.goto('/products/prod_bundle/upgrade')

  // Original: $200, Upgrade credit: $100, Expected: $100
  await expect(page.locator('[data-test="upgrade-credit"]')).toHaveText('-$100.00')
  await expect(page.locator('[data-test="final-price"]')).toHaveText('$100.00')

  // Apply additional fixed coupon
  await page.fill('[data-test="coupon-input"]', 'FIXED20')
  await page.click('[data-test="apply-coupon"]')

  // Should still be $100 (upgrade credit is better than $20 coupon)
  await expect(page.locator('[data-test="final-price"]')).toHaveText('$100.00')
})

Database Integration Tests

7. Database Schema and Queries

File: packages/adapter-drizzle/src/schemas/merchant-coupon-schema.test.ts

Test Case 7.1: Insert Fixed Amount Coupon

it('inserts coupon with amountDiscount', async () => {
  const result = await db.insert(merchantCoupons).values({
    id: 'test_coupon_1',
    merchantAccountId: 'merchant_1',
    amountDiscount: 2000,
    type: 'special',
    status: 1,
  })

  expect(result).toBeTruthy()

  const inserted = await db.query.merchantCoupons.findFirst({
    where: eq(merchantCoupons.id, 'test_coupon_1'),
  })

  expect(inserted?.amountDiscount).toBe(2000)
})

Test Case 7.2: Query Coupons by Discount Type

it('queries fixed amount coupons', async () => {
  const fixedCoupons = await db.query.merchantCoupons.findMany({
    where: and(
      isNotNull(merchantCoupons.amountDiscount),
      gt(merchantCoupons.amountDiscount, 0)
    ),
  })

  expect(fixedCoupons.length).toBeGreaterThan(0)
  fixedCoupons.forEach(coupon => {
    expect(coupon.amountDiscount).toBeGreaterThan(0)
    expect(coupon.percentageDiscount).toBeFalsy()
  })
})

Test Case 7.3: Database Constraint Prevents Both Discounts

it('rejects insert with both discount types via DB constraint', async () => {
  await expect(
    db.insert(merchantCoupons).values({
      id: 'test_invalid',
      merchantAccountId: 'merchant_1',
      percentageDiscount: '0.25',
      amountDiscount: 2000,
      type: 'special',
      status: 1,
    })
  ).rejects.toThrow()
})

Payment Adapter Tests

8. Stripe Adapter Tests

File: packages/core/src/providers/stripe.test.ts

Test Case 8.1: getCouponAmountOff Implementation

it('retrieves amount_off from Stripe coupon', async () => {
  const stripeAdapter = new StripePaymentAdapter(mockStripeClient)

  mockStripeClient.coupons.retrieve.mockResolvedValue({
    id: 'stripe_coupon_123',
    amount_off: 2000,
    currency: 'usd',
  })

  const amountOff = await stripeAdapter.getCouponAmountOff('stripe_coupon_123')

  expect(amountOff).toBe(2000)
})

Test Case 8.2: getCouponAmountOff Returns 0 for Percentage Coupon

it('returns 0 for percentage-based coupons', async () => {
  const stripeAdapter = new StripePaymentAdapter(mockStripeClient)

  mockStripeClient.coupons.retrieve.mockResolvedValue({
    id: 'stripe_coupon_percent',
    percent_off: 25,
  })

  const amountOff = await stripeAdapter.getCouponAmountOff('stripe_coupon_percent')

  expect(amountOff).toBe(0)
})

Edge Case Tests

9. Edge Cases and Error Handling

Test Case 9.1: Extremely Large Fixed Discount

it('handles very large fixed discounts', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic', // $100
    merchantCouponId: 'coupon_fixed_10000', // $10,000 off
    quantity: 1,
  })

  expect(result.calculatedPrice).toBe(0)
})

Test Case 9.2: Negative Amount Discount

it('rejects negative amountDiscount', () => {
  const coupon = {
    id: 'test_negative',
    merchantAccountId: 'merchant_1',
    amountDiscount: -1000,
    type: 'special',
    status: 1,
  }

  expect(() => merchantCouponSchema.parse(coupon)).toThrow()
})

Test Case 9.3: Zero Amount Discount

it('treats zero amountDiscount as no discount', async () => {
  const result = await determineCouponToApply({
    prismaCtx: mockAdapter,
    merchantCouponId: 'coupon_zero',
    quantity: 1,
    productId: 'prod_basic',
  })

  expect(result.appliedDiscountType).toBe('none')
})

Test Case 9.4: Currency Mismatch (Future Consideration)

it('assumes USD for all amount discounts', async () => {
  // Current implementation assumes USD
  // This test documents the assumption for future multi-currency support
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic',
    merchantCouponId: 'coupon_fixed_20',
    quantity: 1,
  })

  // Amount discount applied directly without currency conversion
  expect(result.appliedFixedDiscount).toBe(20)
})

Test Case 9.5: Bulk Purchase with Fixed Coupon

it('ignores fixed coupon for bulk purchases', async () => {
  const result = await formatPricesForProduct({
    ctx: mockAdapter,
    productId: 'prod_basic',
    merchantCouponId: 'coupon_fixed_20',
    quantity: 5, // Bulk
  })

  expect(result.bulk).toBe(true)
  expect(result.appliedDiscountType).toBe('bulk')
  expect(result.appliedFixedDiscount).toBeUndefined()
})

Performance Tests

10. Performance Benchmarks

Test Case 10.1: Price Calculation Performance

it('calculates prices within acceptable time', async () => {
  const start = performance.now()

  for (let i = 0; i < 1000; i++) {
    await formatPricesForProduct({
      ctx: mockAdapter,
      productId: 'prod_basic',
      merchantCouponId: 'coupon_fixed_20',
      quantity: 1,
    })
  }

  const end = performance.now()
  const avgTime = (end - start) / 1000

  expect(avgTime).toBeLessThan(1) // < 1ms per calculation
})

Test Execution Order

Recommended Execution Sequence

  1. Schema Validation (fastest, catches basic issues)

    • Run first to validate data model changes
  2. Unit Tests - Business Logic

    • determineCouponToApply
    • getCalculatedPrice
    • formatPricesForProduct
  3. Database Integration

    • Schema tests
    • Query tests
  4. Payment Adapter Tests

    • Stripe adapter methods
  5. Stripe Checkout Integration

    • Mock-based integration tests
  6. End-to-End Tests

    • Full checkout flows
    • Real Stripe test mode (slowest)

Acceptance Criteria

Feature Complete When:

  • All Unit Tests Pass (100% coverage)

    • Schema validation
    • Business logic functions
    • Edge cases
  • Integration Tests Pass

    • Database operations
    • Stripe checkout creation
    • Payment adapter methods
  • End-to-End Tests Pass

    • Fixed amount checkout flow
    • PPP comparison logic
    • Upgrade scenarios
  • Backward Compatibility Verified

    • Percentage coupons work unchanged
    • PPP flow unaffected
    • Bulk pricing unaffected
    • Upgrade flow unaffected
  • Edge Cases Handled

    • Discount > price (clamps to $0)
    • Both discount types (validation fails)
    • Negative prices prevented
    • Large discounts handled
  • Performance Acceptable

    • Price calculation < 1ms
    • Stripe checkout creation < 500ms
    • No N+1 queries
  • Observability Implemented

    • Discount type logged
    • Discount amount logged
    • Conflict resolution logged
    • No console.log statements

Test Commands

# Run all tests
pnpm test

# Run specific package tests
pnpm test --filter="@coursebuilder/core"

# Run specific test file
cd packages/core && pnpm test src/lib/pricing/format-prices-for-product.test.ts

# Watch mode for development
cd packages/core && pnpm test:watch

# Coverage report
pnpm test --coverage

# E2E tests only
pnpm test:e2e

# Integration tests
pnpm test:integration

Regression Testing Checklist

Before deployment, verify these existing flows still work:

  • Percentage-only merchant coupon checkout
  • PPP discount application
  • Bulk purchase pricing
  • Upgrade from valid purchase
  • Upgrade from restricted (PPP) purchase
  • Unrestricted upgrade from PPP
  • Bundle upgrade with multiple prior purchases
  • Subscription checkout (if applicable)
  • Free product (price = $0)
  • Applied coupon displayed in UI
  • Purchase confirmation email

Test Data Cleanup

After test runs:

-- Clean up test coupons
DELETE FROM MerchantCoupon WHERE id LIKE 'test_%';

-- Clean up test purchases
DELETE FROM Purchase WHERE id LIKE 'test_%';

-- Clean up test Stripe resources
-- (Use Stripe CLI or API to clean test mode data)

Monitoring Post-Deployment

Metrics to Track

  1. Discount Type Distribution

    SELECT
      CASE
        WHEN mc.amountDiscount > 0 THEN 'fixed'
        WHEN mc.percentageDiscount > 0 THEN 'percentage'
        ELSE 'none'
      END as discount_type,
      COUNT(*) as usage_count
    FROM Purchase p
    LEFT JOIN MerchantCoupon mc ON p.merchantCouponId = mc.id
    GROUP BY discount_type;
  2. Average Discount Amount

    SELECT
      AVG(mc.amountDiscount / 100) as avg_fixed_discount,
      AVG(p.totalAmount * mc.percentageDiscount) as avg_percentage_discount
    FROM Purchase p
    JOIN MerchantCoupon mc ON p.merchantCouponId = mc.id
    WHERE p.createdAt > NOW() - INTERVAL 7 DAY;
  3. Checkout Success Rate by Discount Type

    • Monitor failed checkouts
    • Compare success rates across discount types

Conclusion

This testing plan ensures comprehensive coverage of the fixed discount coupon feature across all layers of the application. Execute tests in the recommended order, verify all acceptance criteria, and monitor metrics post-deployment to ensure successful implementation.

Flat Discount Coupon Support

Summary

  • Add first-class support for fixed-amount coupons alongside existing percentage-based flow.
  • Unify price calculation and checkout metadata so either discount type produces consistent Stripe sessions and UI pricing.
  • Preserve upgrade behaviour and PPP edge cases while adding validation and logging for the new path.

Difficulty & Risk

  • Difficulty: High — spans schema updates, pricing logic, adapters, and Stripe integration with careful backward-compat handling.
  • Risk: Medium-High — miscalculations or coupon stacking bugs can impact revenue; mitigated via tests and logging.

Current Behaviour

  • formatPricesForProduct only exposes a percent-based discount from determineCouponToApply; flat discounts are limited to upgrade flows via getFixedDiscountForIndividualUpgrade.
const percentOfDiscount = appliedMerchantCoupon?.percentageDiscount
// ... existing code ...
return {
    ...product,
    quantity,
    unitPrice,
    fullPrice,
    fixedDiscountForUpgrade,
    calculatedPrice: getCalculatedPrice({
        unitPrice,
        percentOfDiscount,
        fixedDiscount: fixedDiscountForUpgrade,
        quantity,
    }),
    availableCoupons: result.availableCoupons,
    appliedMerchantCoupon,
    // ... existing code ...
}
  • stripeCheckout already fabricates single-use amount_off coupons for upgrade scenarios but defaults to existing Stripe coupons (percentage) for merchant coupons.
if (isUpgrade && upgradeFromPurchase && loadedProduct && customerId) {
    const fixedDiscountForIndividualUpgrade = await getFixedDiscountForIndividualUpgrade({
        // ... existing code ...
    })
    if (fixedDiscountForIndividualUpgrade > 0) {
        const amount_off_in_cents = (fullPrice - calculatedPrice) * 100
        const couponId = await config.paymentsAdapter.createCoupon({
            amount_off: amount_off_in_cents,
            name: couponName,
            max_redemptions: 1,
            redeem_by: TWELVE_FOUR_HOURS_FROM_NOW,
            currency: 'USD',
            applies_to: { products: [merchantProductIdentifier] },
        })
        discounts.push({ coupon: couponId })
    }
} else if (merchantCoupon && merchantCoupon.identifier) {
    // percentage-only flow relying on pre-existing Stripe coupon ids
    const promotionCodeId = await config.paymentsAdapter.createPromotionCode({
        coupon: merchantCoupon.identifier,
        max_redemptions: 1,
        expires_at: TWELVE_FOUR_HOURS_FROM_NOW,
    })
    discounts.push({ promotion_code: promotionCodeId })
}
  • Merchant coupon metadata (types, value) is assumed to live in adapters; current schema lacks an explicit fixed-amount field and downstream code expects percentages.

Goals

  • Model amountOff (in cents) for merchant coupons and expose it through adapters and pricing utilities.
  • Ensure formatPricesForProduct and stripeCheckout understand when to prioritise fixed versus percent discounts, avoiding double-discounts.
  • Maintain current behaviour for PPP, upgrades, and bulk pricing.
  • Provide logs/telemetry for discount type decisions.

Proposed Changes

1. Data & Adapter Surface

  • Extend coupon schema/types (MerchantCoupon, determineCouponToApply return payload) to carry mutually exclusive percentageDiscount and amountDiscount fields (cents, integer).
  • Update adapter contracts (PaymentsAdapter, CourseBuilderAdapter) to return Stripe coupon metadata with amount information. Ensure tsup build passes.
  • Add migration or seed updates if coupon records live in persistence layer.

2. Coupon Selection Logic

  • Update determineCouponToApply to:
    • Validate and normalise fixed-amount coupons (include currency assumptions, e.g. USD).
    • Resolve conflicts: prefer amountDiscount when defined, otherwise fall back to percentage.
    • Surface an explicit appliedDiscountType enum ('fixed' | 'percentage' | 'ppp' | 'bulk').
  • Add unit tests covering precedence rules, PPP interactions, and invalid configurations.

3. Price Formatting Pipeline

  • Modify formatPricesForProduct to propagate fixed discount amounts:
    • Inject appliedFixedDiscount = appliedMerchantCoupon?.amountDiscount ?? 0.
    • Feed appliedFixedDiscount into getCalculatedPrice (requires ensuring helper supports arbitrary fixed deductions beyond upgrades).
    • Update return payload to include appliedDiscountType, appliedFixedDiscount, and to reuse existing fixedDiscountForUpgrade without collision.
  • Ensure fullPrice and calculatedPrice stay non-negative; clamp where necessary.
  • Write unit tests for formatPricesForProduct covering: no coupon, percentage coupon, fixed coupon, upgrade + fixed coupon conflict, bulk.

4. Stripe Checkout Integration

  • When merchantCoupon carries amountDiscount:
    • Use Stripe promotion codes only if the underlying coupon already has amount_off; otherwise create a transient coupon mirroring amountDiscount (similar to upgrade flow) and store identifier for reconciliation.
    • Update checkout metadata to capture discountType, discountAmount, and existing fields.
    • Prevent stacking upgrade discount coupon creation when a fixed merchant coupon applies; decide precedence and enforce via guard clauses.
  • Add logging via server logger (@/server/logger equivalent in packages) for coupon application decisions; remove stray console.log.
  • Extend existing integration tests/mocks for PaymentsAdapter to validate amount_off path.

5. Client & Admin Surfaces (if applicable)

  • Audit UI surfaces that display coupon details to ensure they can show fixed amounts (currency formatting) as well as percentages.
  • Update documentation/help text to clarify supported discount types.

6. Observability & Cleanup

  • Replace remaining console.* usage in pricing code with central logger.
  • Document new coupon fields in package README or docs/pricing.md if available.

Testing & Definition of Done

  • Unit tests: determineCouponToApply, getCalculatedPrice, formatPricesForProduct covering both discount types, PPP, upgrade interactions.
  • Integration tests: mocked Stripe checkout flow verifying discount payloads, metadata, and adapter interactions for fixed vs percentage.
  • Type checks + pnpm test --filter core (or package-specific script) pass.
  • No new eslint errors; tsup build for affected packages succeeds.
  • Logging verified in local/dev environment (ensure correct context IDs).
  • Documentation or admin UI updates merged.
  • Feature flag (if needed) toggled or rollout plan documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment