diff --git a/.env.example b/.env.example index da27f40f..4ec8b68f 100644 --- a/.env.example +++ b/.env.example @@ -203,6 +203,34 @@ R2_PUBLIC_URL= # Cloudflare R2 backup bucket (used by scripts/backup.sh --upload) R2_BACKUP_BUCKET=orion-backups +# ============================================================================= +# LOYALTY MODULE +# ============================================================================= +# Anti-fraud defaults (all optional, shown values are defaults) +# LOYALTY_DEFAULT_COOLDOWN_MINUTES=15 +# LOYALTY_MAX_DAILY_STAMPS=5 +# LOYALTY_PIN_MAX_FAILED_ATTEMPTS=5 +# LOYALTY_PIN_LOCKOUT_MINUTES=30 + +# Points configuration +# LOYALTY_DEFAULT_POINTS_PER_EURO=10 + +# Google Wallet integration +# See docs/deployment/hetzner-server-setup.md Step 25 for setup guide +# Get Issuer ID from https://pay.google.com/business/console +# LOYALTY_GOOGLE_ISSUER_ID=3388000000012345678 +# LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/path/to/service-account.json + +# Apple Wallet integration (requires Apple Developer account) +# LOYALTY_APPLE_PASS_TYPE_ID=pass.com.example.loyalty +# LOYALTY_APPLE_TEAM_ID=ABCD1234 +# LOYALTY_APPLE_WWDR_CERT_PATH=/path/to/wwdr.pem +# LOYALTY_APPLE_SIGNER_CERT_PATH=/path/to/signer.pem +# LOYALTY_APPLE_SIGNER_KEY_PATH=/path/to/signer.key + +# QR code size in pixels (default: 300) +# LOYALTY_QR_CODE_SIZE=300 + # ============================================================================= # CLOUDFLARE CDN / PROXY # ============================================================================= diff --git a/docs/deployment/environment.md b/docs/deployment/environment.md index c930bc63..228f16fb 100644 --- a/docs/deployment/environment.md +++ b/docs/deployment/environment.md @@ -300,6 +300,59 @@ the server filesystem. --- +## Loyalty Module + +Configuration for the loyalty module (stamp/points programs, wallet integration). +All variables use the `LOYALTY_` prefix and are managed by `app/modules/loyalty/config.py`. + +### Anti-Fraud Defaults + +| Variable | Description | Default | Required | +|---|---|---|---| +| `LOYALTY_DEFAULT_COOLDOWN_MINUTES` | Minimum minutes between stamps for the same card | `15` | No | +| `LOYALTY_MAX_DAILY_STAMPS` | Maximum stamps per card per day | `5` | No | +| `LOYALTY_PIN_MAX_FAILED_ATTEMPTS` | Failed PIN attempts before lockout | `5` | No | +| `LOYALTY_PIN_LOCKOUT_MINUTES` | Duration of PIN lockout in minutes | `30` | No | + +### Points + +| Variable | Description | Default | Required | +|---|---|---|---| +| `LOYALTY_DEFAULT_POINTS_PER_EURO` | Points earned per euro spent | `10` | No | + +### Google Wallet + +!!! info "Required for Google Wallet passes" + Both variables must be set for loyalty cards to appear in Google Wallet. + See [Hetzner Step 25](hetzner-server-setup.md#step-25-google-wallet-integration) for setup guide. + +| Variable | Description | Default | Required | +|---|---|---|---| +| `LOYALTY_GOOGLE_ISSUER_ID` | Google Wallet Issuer ID (numeric string from [Pay & Wallet Console](https://pay.google.com/business/console)) | `None` | Yes (for Google Wallet) | +| `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` | Path to the Google service account JSON key file | `None` | Yes (for Google Wallet) | + +### Apple Wallet + +!!! info "Required for Apple Wallet passes" + All five variables must be set for `.pkpass` generation. + Requires an Apple Developer account ($99/year). + +| Variable | Description | Default | Required | +|---|---|---|---| +| `LOYALTY_APPLE_PASS_TYPE_ID` | Pass type identifier (e.g., `pass.com.example.loyalty`) | `None` | Yes (for Apple Wallet) | +| `LOYALTY_APPLE_TEAM_ID` | Apple Developer Team ID | `None` | Yes (for Apple Wallet) | +| `LOYALTY_APPLE_WWDR_CERT_PATH` | Path to Apple WWDR intermediate certificate | `None` | Yes (for Apple Wallet) | +| `LOYALTY_APPLE_SIGNER_CERT_PATH` | Path to pass signing certificate (`.pem`) | `None` | Yes (for Apple Wallet) | +| `LOYALTY_APPLE_SIGNER_KEY_PATH` | Path to pass signing private key (`.pem`) | `None` | Yes (for Apple Wallet) | + +### QR Code + +| Variable | Description | Default | Required | +|---|---|---|---| +| `LOYALTY_QR_CODE_SIZE` | QR code image size in pixels | `300` | No | + +--- + ## Cloudflare CDN / Proxy | Variable | Description | Default | Required | @@ -335,6 +388,8 @@ marked **critical** will trigger a startup warning if left at their default valu - [x] **Email (Mailgun):** `MAILGUN_API_KEY`, `MAILGUN_DOMAIN` — transactional only, no marketing features - [x] **Email (SES):** `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` — cheapest at scale - [x] **R2 Storage:** `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` + - [x] **Google Wallet:** `LOYALTY_GOOGLE_ISSUER_ID`, `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` + - [ ] **Apple Wallet:** `LOYALTY_APPLE_PASS_TYPE_ID`, `LOYALTY_APPLE_TEAM_ID`, `LOYALTY_APPLE_WWDR_CERT_PATH`, `LOYALTY_APPLE_SIGNER_CERT_PATH`, `LOYALTY_APPLE_SIGNER_KEY_PATH` ### Example `.env` file (production) @@ -374,4 +429,8 @@ ENABLE_METRICS=True SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 SENTRY_ENVIRONMENT=production CLOUDFLARE_ENABLED=True + +# Google Wallet (Loyalty) +LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598 +LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/app/google-wallet-sa.json ``` diff --git a/docs/deployment/hetzner-server-setup.md b/docs/deployment/hetzner-server-setup.md index b44b97c9..04357c96 100644 --- a/docs/deployment/hetzner-server-setup.md +++ b/docs/deployment/hetzner-server-setup.md @@ -102,6 +102,26 @@ Complete step-by-step guide for deploying Orion on a Hetzner Cloud VPS. **Steps 1–24 fully complete.** Enterprise infrastructure hardening done. +!!! success "Progress — 2026-02-24" + **Completed:** + + - **Step 25: Google Wallet Integration** — Google Cloud project "Orion" created, Wallet API enabled, service account configured + - Google Pay Merchant ID: `BCR2DN5TW2CNXDAG` + - Google Wallet Issuer ID: `3388000000023089598` + - Service account: `wallet-service@orion-488322.iam.gserviceaccount.com` (admin role in Pay & Wallet Console) + - Service account JSON key generated + - Dependencies added to `requirements.txt`: `google-auth>=2.0.0`, `PyJWT>=2.0.0` (commit `d36783a`) + - Loyalty env vars added to `.env.example` and `docs/deployment/environment.md` + + **Next steps:** + + - [ ] Upload service account JSON to Hetzner server + - [ ] Set `LOYALTY_GOOGLE_ISSUER_ID` and `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` in production `.env` + - [ ] Restart app and test end-to-end: enroll → add pass → stamp → verify pass updates + - [ ] Wire "Add to Google Wallet" button into storefront enrollment success page + - [ ] Submit for Google production approval when ready + - [ ] Apple Wallet setup (APNs push, certificates, pass images) + !!! success "Progress — 2026-02-16" **Completed:** @@ -1753,6 +1773,196 @@ This document has been updated with Steps 19–24. Additional documentation chan --- +## Step 25: Google Wallet Integration + +Enable loyalty card passes in Google Wallet so customers can add their loyalty card to their Android phone. + +### Prerequisites + +- Google account (personal Gmail is fine) +- Loyalty module deployed and working + +### 25.1 Google Pay & Wallet Console + +Register as a Google Wallet Issuer: + +1. Go to [pay.google.com/business/console](https://pay.google.com/business/console) +2. Enter your business name (e.g., "Letzshop" or your company name) — this is for Google's review, customers don't see it on passes +3. Note your **Issuer ID** from the Google Wallet API section + +!!! info "Issuer ID" + The Issuer ID is a long numeric string (e.g., `3388000000023089598`). You'll find it under Google Wallet API → Manage in the Pay & Wallet Console. + +### 25.2 Google Cloud Project + +1. Go to [console.cloud.google.com](https://console.cloud.google.com) +2. Create a new project (e.g., "Orion") +3. Enable the **Google Wallet API**: + - Navigate to "APIs & Services" → "Library" + - Search for "Google Wallet API" and enable it + +### 25.3 Service Account + +Create a service account for API access: + +1. Go to "APIs & Services" → "Credentials" → "Create Credentials" +2. Select **Google Wallet API** as the API +3. Select **Application data** (not user data — your backend calls the API directly) +4. Name the service account (e.g., `wallet-service`) +5. Click "Done" + +Download the JSON key: + +1. Go to "IAM & Admin" → "Service Accounts" +2. Click on the service account you created +3. Go to **Keys** tab → **Add Key** → **Create new key** → **JSON** +4. Save the downloaded `.json` file securely + +### 25.4 Link Service Account to Issuer + +1. Go back to [pay.google.com/business/console](https://pay.google.com/business/console) +2. In the **left sidebar**, click **Users** (not inside the Wallet API section) +3. Invite the service account email (e.g., `wallet-service@orion-488322.iam.gserviceaccount.com`) +4. Assign **Admin** role +5. Verify it appears in the users list + +!!! warning "Common mistake" + The "Users" link is in the **left sidebar** of the Pay & Wallet Console, not inside the "Google Wallet API" → "Manage" section. The Manage page has "Setup test accounts" which is a different feature. + +### 25.5 Deploy to Server + +Upload the service account JSON key to the Hetzner server: + +```bash +# From your local machine +scp /path/to/orion-488322-xxxxx.json samir@91.99.65.229:~/apps/orion/google-wallet-sa.json +``` + +Add the environment variables to the production `.env`: + +```bash +ssh samir@91.99.65.229 +cd ~/apps/orion +nano .env +``` + +Add: + +```bash +# Google Wallet (Loyalty Module) +LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598 +LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/app/google-wallet-sa.json +``` + +!!! note "Docker path" + The path must be relative to the Docker container's filesystem. If the file is in `~/apps/orion/`, it maps to `/app/` inside the container (check your `docker-compose.yml` volumes). + +Mount the JSON file in `docker-compose.yml` if not already covered by the app volume: + +```yaml +services: + api: + volumes: + - ./google-wallet-sa.json:/app/google-wallet-sa.json:ro +``` + +Restart the application: + +```bash +docker compose --profile full up -d --build +``` + +### 25.6 Verify Configuration + +Check the API health and wallet service status: + +```bash +# Check the app logs for wallet service initialization +docker compose --profile full logs api | grep -i "wallet\|loyalty" + +# Test via API — create a program and enroll a customer, then check the response +# for google_object_id and google Wallet URL fields +curl -s https://api.wizard.lu/health | python3 -m json.tool +``` + +### 25.7 Testing Google Wallet Passes + +Google provides a **demo mode** — passes work in test without full production approval: + +1. Console admins and developers (your Google account) can always test passes +2. For additional testers, add their Gmail addresses in Pay & Wallet Console → Google Wallet API → Manage → **Setup test accounts** +3. Use `walletobjects.sandbox` scope for initial testing (the code uses `wallet_object.issuer` which covers both) + +**End-to-end test flow:** + +1. Create a loyalty program via the store panel +2. Enroll a customer (via store or storefront self-enrollment) +3. The API returns a Google Wallet save URL +4. Open the URL on an Android device — the pass is added to Google Wallet +5. Add a stamp or points — the pass in Google Wallet auto-updates + +### 25.8 Local Development Setup + +You can test the full Google Wallet integration from your local machine: + +```bash +# In your local .env (or export directly) +export LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598 +export LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/path/to/orion-488322-xxxxx.json +``` + +The `GoogleWalletService` calls Google's REST API directly over HTTPS — no special network configuration needed. The same service account JSON works on both local and server environments. + +**Local testing checklist:** + +- [x] Service account JSON downloaded and path set in env +- [x] `LOYALTY_GOOGLE_ISSUER_ID` set in env +- [ ] Start the app locally: `uvicorn app.main:app --reload` +- [ ] Create a loyalty program → verify `google_class_id` is set +- [ ] Enroll a customer → verify `google_object_id` is set +- [ ] Call `get_save_url()` → open the URL on Android to add pass +- [ ] Add stamps → verify pass updates in Google Wallet + +### 25.9 How It Works (Architecture) + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ +│ Merchant │────▶│ Orion API │────▶│ Google Wallet API │ +│ creates │ │ │ │ │ +│ program │ │ create_class │ │ POST /loyaltyClass │ +└─────────────┘ └──────────────┘ └─────────────────────┘ + +┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ +│ Customer │────▶│ Orion API │────▶│ Google Wallet API │ +│ enrolls │ │ │ │ │ +│ │ │create_object │ │ POST /loyaltyObject │ +│ │◀────│ save_url │ │ │ +│ │ └──────────────┘ └─────────────────────┘ +│ taps "Add │ +│ to Wallet" │────▶ Google Wallet app adds pass automatically +└─────────────┘ + +┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ +│ Staff adds │────▶│ Orion API │────▶│ Google Wallet API │ +│ stamp │ │ │ │ │ +│ │ │update_object │ │ PATCH /loyaltyObject│ +└─────────────┘ └──────────────┘ └─────────────────────┘ + Pass auto-updates on + customer's phone +``` + +No push notifications needed — Google syncs object changes automatically. + +### 25.10 Next Steps + +After Google Wallet is verified working: + +1. **Wire "Add to Google Wallet" button** into the storefront enrollment success page and card dashboard +2. **Submit for Google production approval** — required before non-test users can add passes +3. **Apple Wallet** — separate setup requiring Apple Developer account, APNs certificates, and pass signing certificates (see [Loyalty Module docs](../modules/loyalty.md#apple-wallet)) + +--- + ## Domain & Port Reference | Service | Internal Port | External Port | Domain (via Caddy) |