102 lines
3.3 KiB
Markdown
102 lines
3.3 KiB
Markdown
# Phase 7.2 — Point Engine (Immutable Ledger)
|
||
|
||
| Field | Value |
|
||
| --- | --- |
|
||
| Identifier | `loyalty-7.2` |
|
||
| Status | Complete |
|
||
| Module | loyalty |
|
||
| Service | `loyalty-service` |
|
||
| Version | `0.7.2.0` |
|
||
| Database | `loyalty_db` |
|
||
| Depends On | Phase 7.1 |
|
||
| ADR(s) | ADR-001, ADR-003, ADR-006, ADR-011 |
|
||
| Manifest | [phase-manifest.yaml](ai-framework/phase-manifest.yaml) |
|
||
|
||
## Objective
|
||
|
||
Deliver the Point Engine: append-only immutable ledger as the sole source of point balances, with earn / redeem / adjust / expire operations, idempotency, and balance APIs — without rewards redemption, campaigns, wallet, or gift cards.
|
||
|
||
## Scope
|
||
|
||
### In Scope
|
||
|
||
- `PointLedgerEntry` append-only model (signed amounts; `balance_after`)
|
||
- Balance = `SUM(amount)` — never stored on `PointAccount`
|
||
- Earn, redeem, adjust, expire operations
|
||
- Idempotency via optional `idempotency_key` (tenant-unique)
|
||
- Insufficient-balance rejection on redeem/expire
|
||
- Permissions, events, validators, migration `0003_phase_72_points`
|
||
- Tests + documentation + handover + service snapshot
|
||
|
||
### Out of Scope
|
||
|
||
- Reward catalog redemption flows (7.3)
|
||
- Campaign rule engine (7.4)
|
||
- Referral / wallet / gift cards / analytics / partner APIs (7.5–7.9)
|
||
|
||
## Ledger Rules
|
||
|
||
| Rule | Detail |
|
||
| --- | --- |
|
||
| Append-only | No UPDATE/DELETE of ledger rows |
|
||
| Signed amounts | Earn/adjust+ positive; redeem/expire/adjust− negative |
|
||
| Open accounts only | Frozen/closed accounts reject mutations |
|
||
| Idempotency | Same `(tenant_id, idempotency_key)` returns existing entry |
|
||
| Balance | Computed from ledger; never a mutable column on `PointAccount` |
|
||
|
||
## Models
|
||
|
||
| Entity | Soft delete | Audit | Tenant |
|
||
| --- | --- | --- | --- |
|
||
| PointLedgerEntry | No (append-only) | Via fields + audit log | Yes |
|
||
|
||
## APIs
|
||
|
||
| Method | Path | Permission |
|
||
| --- | --- | --- |
|
||
| GET | `/api/v1/point-accounts/{id}/balance` | `loyalty.points.view` |
|
||
| GET | `/api/v1/point-accounts/{id}/ledger` | `loyalty.points.view` |
|
||
| POST | `/api/v1/point-accounts/{id}/earn` | `loyalty.points.earn` |
|
||
| POST | `/api/v1/point-accounts/{id}/redeem` | `loyalty.points.redeem` |
|
||
| POST | `/api/v1/point-accounts/{id}/adjust` | `loyalty.points.adjust` |
|
||
| POST | `/api/v1/point-accounts/{id}/expire-points` | `loyalty.points.expire` |
|
||
|
||
## Events
|
||
|
||
| Event | When |
|
||
| --- | --- |
|
||
| `loyalty.points.earned` | Earn |
|
||
| `loyalty.points.redeemed` | Redeem |
|
||
| `loyalty.points.adjusted` | Adjust |
|
||
| `loyalty.points.expired` | Expire points |
|
||
|
||
## Migration
|
||
|
||
| Item | Detail |
|
||
| --- | --- |
|
||
| Alembic | `0003_phase_72_points` (down_revision `0002_phase_71_membership`) |
|
||
| Breaking | None — additive table |
|
||
| Backfill | None required |
|
||
|
||
## Tests
|
||
|
||
Command: `cd backend/services/loyalty && pytest -q` → **61 passed**
|
||
|
||
## Known Limitations
|
||
|
||
- No automatic expire scheduler (API-driven expire only)
|
||
- No FIFO lot tracking for expiration (bulk expire by amount)
|
||
- Transfer still does not migrate ledger entries (explicit follow-up if needed)
|
||
|
||
## Next Phase
|
||
|
||
Phase 7.3 — Rewards Engine
|
||
|
||
## Related Documents
|
||
|
||
- [Handover](phase-handover/phase-7-2.md)
|
||
- [Phase 7.1](loyalty-phase-7-1.md)
|
||
- [ADR-011](architecture/adr/ADR-011.md)
|
||
- [Service Snapshot](service-snapshots/loyalty.yaml)
|
||
- [Module Registry](module-registry.md#loyalty)
|