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
| Concept | Description |
|---|---|
| Account | One wallet per customer per business. Holds a denormalized balance and status (active/suspended). |
| Transaction | Immutable ledger entry. Never updated or deleted. Types: issue, use, adjust, reverse. |
| Config | Per-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
- Lazy account creation — Accounts are created on first issuance, not upfront.
- Immutable ledger — Transactions are never updated/deleted. Reversals create new
reverseentries. - Pessimistic locking —
SELECT FOR UPDATEon debit operations prevents race conditions. - Denormalized balance — Stored on the account for fast reads; ledger is the audit source of truth.
- Fail-safe event handlers — Store credit events never rethrow; they must never break core flows (returns, exchanges, layaways, sale voids).
- Feature flag gated — All endpoints require
STORE_CREDIT_ENABLEDbusiness parameter.
API Endpoints
All endpoints require authentication and the STORE_CREDIT_ENABLED feature flag.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /store-credit/balance?customerId | Read | Get account balance (returns 0.00 if no account) |
GET | /store-credit/history/:accountId | Read | Paginated transaction history |
POST | /store-credit/issue | Create | Issue credit (manual, return, exchange, layaway) |
POST | /store-credit/redeem | Create | Redeem credit during a sale |
POST | /store-credit/reverse/:transactionId?locationId | Update | Reverse a redemption |
POST | /store-credit/accounts/:accountId/adjust | Update | Manager balance adjustment |
PUT | /store-credit/accounts/:accountId/suspend | Update | Suspend account |
PUT | /store-credit/accounts/:accountId/reactivate | Update | Reactivate account |
GET | /store-credit/reports/liability | Read | Liability report (JSON or CSV) |
GET | /store-credit/config | Read | Get business configuration |
PUT | /store-credit/config | Update | Upsert business configuration |
Event Integration
The module listens to events from other modules:
| Event | Source | Action |
|---|---|---|
store-credit.issue-from-return | Returns module | Auto-issue credit from return |
store-credit.issue-from-exchange | Exchanges module | Auto-issue credit from exchange |
store-credit.issue-from-layaway-cancellation | Layaway module | Auto-issue credit from layaway cancel |
sale.updated | Sales module | Auto-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 constraintsstore_credit_config— One per business (upsert pattern)