🏗️ Technical Architecture Overview
The CPL Group Real Rewards Loyalty system is built upon Odoo 17.0 Community Edition running containerized microservices on Ubuntu Linux (GCP VM odoo-production-vm).
1. Modular Architecture
All custom loyalty logic, ingestion pipelines, and security mechanisms are isolated in modular addons located in odoo/addons/:
odoo/addons/
├── cpl_real_rewards_ui/ # UI Overrides, Staff Dashboards, Card Wizards, /docs Auth Controller
├── real_rewards_loyalty/ # Demographics Validations, Points Moderation, Ledger Engine
├── real_rewards_importer/ # POS CSV Batch Ingestion & Order Deduplication
├── real_rewards_security/ # Microsoft Entra ID (Azure AD) SSO, Passwordless OTP Login, Session Security
└── real_rewards_api_mailer/ # Transactional Email Server Dispatcher
2. Data Model Hierarchy & Anchoring
To avoid split accounts and broken loyalty histories across physical card replacements, the data model anchors member profiles to an immutable UUID:
erDiagram
RES_PARTNER ||--o{ REAL_REWARDS_LOYALTY_LEDGER : "has points entries"
RES_PARTNER ||--o{ REAL_REWARDS_CARD_REPLACEMENT_LOG : "has card changes"
RES_PARTNER ||--o{ POS_ORDER : "participates in"
RES_PARTNER {
uuid rrp_member_uuid PK "Permanent Immutable Anchor"
string rrp_card_number "Active Card Number (Index)"
string name "Full Name"
date birth_date "Validated DOB (1900 to Today)"
string phone "Normalized Mobile"
integer loyalty_points "Calculated Whole Points"
}
REAL_REWARDS_LOYALTY_LEDGER {
id id PK
many2one partner_id FK
integer points_delta "Truncated Whole Points"
string source_type "pos_sale | manual_adjustment | redemption"
datetime timestamp
}
REAL_REWARDS_CARD_REPLACEMENT_LOG {
id id PK
many2one partner_id FK
string old_card_number
string new_card_number
string replacement_reason
datetime timestamp
}
3. Strict Point Truncation Rule
In compliance with the project constitution.md:
* Point accruals are integer truncated (no floating point or rounding up).
* A POS spend of K45.95 generates strictly 45 points.
* Decimal precision errors are eliminated at the database level by storing whole integer values in real.rewards.loyalty.ledger.