Include Loyalty/Communication/Sports Center backends and registry updates alongside production nginx and compose wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
4.1 KiB
Phase 7.1 — Membership Engine
| Field | Value |
|---|---|
| Identifier | loyalty-7.1 |
| Status | Complete |
| Module | loyalty |
| Service | loyalty-service |
| Version | 0.7.1.0 |
| Database | loyalty_db |
| Depends On | Phase 7.0 |
| ADR(s) | ADR-001, ADR-003, ADR-006, ADR-011 |
| Manifest | phase-manifest.yaml |
Objective
Deliver the Membership Engine: lifecycle state machine, eligibility transitions, append-only lifecycle history, program cascade policy, and lifecycle APIs/events — without points ledger, rewards redemption, or campaign engines.
Scope
In Scope
- Member lifecycle: activate, renew, freeze, resume, cancel, expire, transfer
- Enroll path hardened with term / expiry and lifecycle events
MembershipLifecycleEventappend-only history- Cascade policy: reject program soft-delete while blocking members exist
- Permissions, events, validators, migration
0002_phase_71_membership - Tests + documentation + handover
Out of Scope
- Point ledger / balances (7.2)
- Rewards redemption (7.3)
- Campaign engine (7.4+)
- Wallet / gift cards / partner network
Cascade Policy
Soft-deleting a LoyaltyProgram is rejected (409 program_has_active_members) when any non-deleted member exists in statuses: pending, active, frozen, suspended, expired. Cancelled / transferred / closed members do not block.
State Machine
| Action | Allowed from | To |
|---|---|---|
| enroll / activate | pending (activate also expired) | active |
| renew | active, expired, frozen, suspended | active |
| freeze | active | frozen |
| resume | frozen, suspended | active |
| cancel | pending, active, frozen, suspended, expired | cancelled |
| expire | active, frozen, suspended | expired |
| transfer | active, frozen, suspended | transferred (+ new active member in target program) |
Terminal: cancelled, transferred, closed.
Models
| Entity | Soft delete | Audit | Tenant |
|---|---|---|---|
| Member (extended fields) | Yes | Yes | Yes |
| MembershipLifecycleEvent | No (append-only) | Via fields | Yes |
New member fields: activated_at, membership_started_at, membership_expires_at, frozen_at, freeze_reason, cancelled_at, cancel_reason, expired_at, transferred_to_member_id.
APIs
| Method | Path | Permission |
|---|---|---|
| POST | /api/v1/members/{id}/activate |
loyalty.members.activate |
| POST | /api/v1/members/{id}/renew |
loyalty.members.renew |
| POST | /api/v1/members/{id}/freeze |
loyalty.members.freeze |
| POST | /api/v1/members/{id}/resume |
loyalty.members.resume |
| POST | /api/v1/members/{id}/cancel |
loyalty.members.cancel |
| POST | /api/v1/members/{id}/expire |
loyalty.members.expire |
| POST | /api/v1/members/{id}/transfer |
loyalty.members.transfer |
| GET | /api/v1/members/{id}/lifecycle |
loyalty.members.lifecycle.view |
Existing create / enroll / update / delete remain compatible (additive fields).
Events
| Event | When |
|---|---|
loyalty.member.activated |
Activate / enroll activation |
loyalty.member.renewed |
Renew |
loyalty.member.frozen |
Freeze |
loyalty.member.resumed |
Resume |
loyalty.member.cancelled |
Cancel |
loyalty.member.expired |
Expire |
loyalty.member.transferred |
Transfer (source) |
Migration
| Item | Detail |
|---|---|
| Alembic | 0002_phase_71_membership (down_revision 0001_initial) |
| Breaking | None — additive columns + new table |
| Backfill | None required |
Tests
Command: cd backend/services/loyalty && pytest -q → 59 passed
Known Limitations
- No automatic expire scheduler (API-driven expire only)
- Transfer copies profile/contact; does not move point accounts (7.2)
- JWT↔tenant binding remains platform-wide
Next Phase
Phase 7.2 — Point Engine (immutable ledger)