Skip to main content

Store Credit Module

Overview

The store credit module implements a wallet + immutable ledger system for customer store credit. Each customer has at most one store credit account per business. All balance mutations are recorded as immutable ledger transactions for full auditability.

Store credit is a financial liability — the business owes the customer the credit amount until redeemed.

Domain Concepts

ConceptDescription
AccountOne wallet per customer per business. Holds a denormalized balance and status (active/suspended).
TransactionImmutable ledger entry. Never updated or deleted. Types: issue, use, adjust, reverse.
ConfigPer-business policy settings (manual issuance cap, layaway refund policy).

Architecture

store-credit/
├── domain/
│ └── store-credit-repository.domain.ts # IStoreCreditRepository port + input interfaces
├── application/
│ ├── store-credit.service.ts # Business logic orchestration
│ └── events/
│ ├── on-exchange-completed.handler.ts
│ ├── on-layaway-cancelled.handler.ts
│ ├── on-return-completed.handler.ts
│ └── on-sale-voided.handler.ts
├── infrastructure/
│ └── store-credit.repository.ts # Kysely adapter (implements IStoreCreditRepository)
└── interfaces/
├── store-credit.controller.ts # REST endpoints
├── dtos/ # Request validation
└── query/ # Query parameter validation

Dependency flow: Controller → Service → IStoreCreditRepository (port) ← Repository (adapter)

Key Design Decisions

  1. Lazy account creation — Accounts are created on first issuance, not upfront.
  2. Immutable ledger — Transactions are never updated/deleted. Reversals create new reverse entries.
  3. Pessimistic lockingSELECT FOR UPDATE on debit operations prevents race conditions.
  4. Denormalized balance — Stored on the account for fast reads; ledger is the audit source of truth.
  5. Fail-safe event handlers — Store credit events never rethrow; they must never break core flows (returns, exchanges, layaways, sale voids).
  6. Feature flag gated — All endpoints require STORE_CREDIT_ENABLED business parameter.

API Endpoints

All endpoints require authentication and the STORE_CREDIT_ENABLED feature flag.

MethodPathPermissionDescription
GET/store-credit/balance?customerIdReadGet account balance (returns 0.00 if no account)
GET/store-credit/history/:accountIdReadPaginated transaction history
POST/store-credit/issueCreateIssue credit (manual, return, exchange, layaway)
POST/store-credit/redeemCreateRedeem credit during a sale
POST/store-credit/reverse/:transactionId?locationIdUpdateReverse a redemption
POST/store-credit/accounts/:accountId/adjustUpdateManager balance adjustment
PUT/store-credit/accounts/:accountId/suspendUpdateSuspend account
PUT/store-credit/accounts/:accountId/reactivateUpdateReactivate account
GET/store-credit/reports/liabilityReadLiability report (JSON or CSV)
GET/store-credit/configReadGet business configuration
PUT/store-credit/configUpdateUpsert business configuration

Event Integration

The module listens to events from other modules:

EventSourceAction
store-credit.issue-from-returnReturns moduleAuto-issue credit from return
store-credit.issue-from-exchangeExchanges moduleAuto-issue credit from exchange
store-credit.issue-from-layaway-cancellationLayaway moduleAuto-issue credit from layaway cancel
sale.updatedSales moduleAuto-reverse redemptions when sale is voided

Example Requests

Issue Store Credit

POST /store-credit/issue
{
"customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"amount": 25.00,
"sourceType": "manual",
"referenceNote": "Customer loyalty reward"
}

Redeem Store Credit

POST /store-credit/redeem
{
"customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"amount": 10.00,
"sourceId": "sale-uuid-here",
"referenceNote": "Applied to sale"
}

Adjust Balance

POST /store-credit/accounts/:accountId/adjust
{
"locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"delta": -5.00,
"referenceNote": "Manager correction for pricing error"
}

Liability Report

GET /store-credit/reports/liability?from=2026-01-01&to=2026-12-31&format=json

Response:

{
"totalOutstandingBalance": "1500.00",
"totalIssued": "2000.00",
"totalUsed": "500.00",
"netChange": "1500.00",
"bySourceType": [
{ "sourceType": "manual", "totalIssued": "1200.00", "count": 15 },
{ "sourceType": "return", "totalIssued": "800.00", "count": 8 }
],
"byEmployee": [
{ "employeeId": "uuid", "employeeName": "John Doe", "totalIssued": "1000.00", "count": 10 }
]
}

Database Tables

  • store_credit_account — One per customer per business (unique constraint)
  • store_credit_transaction — Immutable ledger with CHECK constraints
  • store_credit_config — One per business (upsert pattern)