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 |
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