feat: wire Google Wallet into loyalty enrollment, stamps, and points flows

Connect the fully-implemented Google Wallet service to the loyalty module:
- Create wallet class/object on customer enrollment
- Sync wallet passes on stamp and points operations
- Expose wallet URLs in storefront API responses
- Add conditional "Add to Google Wallet" buttons on dashboard and enroll-success pages
- Use platform-wide env var config (not per-merchant DB column)
- Add Google service account patterns to .gitignore
- Add LOYALTY_GOOGLE_* fields to app Settings
- Update deployment docs and add local testing guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 10:38:46 +01:00
parent 6c78827c7f
commit 32e4aa6564
13 changed files with 358 additions and 56 deletions

View File

@@ -112,13 +112,19 @@ Complete step-by-step guide for deploying Orion on a Hetzner Cloud VPS.
- 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`
- `LOYALTY_GOOGLE_ISSUER_ID` and `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` added to `app/core/config.py` Settings class
- **End-to-end integration wired:**
- Enrollment auto-creates Google Wallet class + object (`card_service``wallet_service.create_wallet_objects`)
- Stamp/points operations auto-sync to Google Wallet (`stamp_service`/`points_service``wallet_service.sync_card_to_wallets`)
- Storefront API returns wallet URLs (`GET /loyalty/card`, `POST /loyalty/enroll`)
- "Add to Google Wallet" button wired in storefront dashboard and enrollment success page (Alpine.js conditional rendering)
- Google Wallet is a platform-wide config (env vars only) — merchants don't need to configure anything
**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)
@@ -1872,7 +1878,21 @@ Restart the application:
docker compose --profile full up -d --build
```
### 25.6 Verify Configuration
### 25.6 Platform-Level Configuration
Google Wallet is a **platform-wide setting** — all merchants on the platform share the same Issuer ID and service account. Merchants don't need to configure anything; wallet integration activates automatically when the env vars are set.
The two required env vars:
```bash
# In production .env
LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598
LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/app/google-wallet-sa.json
```
When both are set, every loyalty program on the platform automatically gets Google Wallet support: enrollment creates wallet passes, stamp/points operations sync to passes, and the storefront shows "Add to Google Wallet" buttons.
### 25.7 Verify Configuration
Check the API health and wallet service status:
@@ -1880,12 +1900,11 @@ Check the API health and wallet service status:
# 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
# Test via API — enroll a customer and check the response for wallet URLs
curl -s https://api.wizard.lu/health | python3 -m json.tool
```
### 25.7 Testing Google Wallet Passes
### 25.8 Testing Google Wallet Passes
Google provides a **demo mode** — passes work in test without full production approval:
@@ -1895,20 +1914,21 @@ Google provides a **demo mode** — passes work in test without full production
**End-to-end test flow:**
1. Create a loyalty program via the store panel
1. Create a loyalty program via the store panel and set the Google Wallet Issuer ID in Settings → Digital Wallet
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
- The system automatically creates a Google Wallet `LoyaltyClass` (for the program) and `LoyaltyObject` (for the card)
3. Open the storefront loyalty dashboard — the "Add to Google Wallet" button appears
4. Click the button (or 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 (no push needed, Google syncs)
### 25.8 Local Development Setup
### 25.9 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
# In your local .env
LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598
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.
@@ -1917,24 +1937,27 @@ The `GoogleWalletService` calls Google's REST API directly over HTTPS — no spe
- [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
- [ ] Start the app locally: `python3 -m uvicorn main:app --reload`
- [ ] Enroll a customer → check logs for "Created Google Wallet class" and "Created Google Wallet object"
- [ ] Open storefront dashboard → "Add to Google Wallet" button should appear
- [ ] Open the wallet URL on Android → pass added to Google Wallet
- [ ] Add stamps → check logs for "Updated Google Wallet object", verify pass updates
### 25.9 How It Works (Architecture)
### 25.10 How It Works (Architecture)
The integration is fully automatic — no manual API calls needed after initial setup.
```
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Merchant │────▶│ Orion API │────▶│ Google Wallet API │
creates │ │ │ │ │
program │ │ create_class │ │ POST /loyaltyClass
sets issuer │ │ │ │ │
ID in UI │ │ │ │
└─────────────┘ └──────────────┘ └─────────────────────┘
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Customer │────▶│ Orion API │────▶│ Google Wallet API │
│ enrolls │ │ │ │ │
│ │ │create_class +│ │ POST /loyaltyClass │
│ │ │create_object │ │ POST /loyaltyObject │
│ │◀────│ save_url │ │ │
│ │ └──────────────┘ └─────────────────────┘
@@ -1944,22 +1967,30 @@ The `GoogleWalletService` calls Google's REST API directly over HTTPS — no spe
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Staff adds │────▶│ Orion API │────▶│ Google Wallet API │
│ stamp │ │ │ │ │
│ stamp/pts │ │ │ │ │
│ │ │update_object │ │ PATCH /loyaltyObject│
└─────────────┘ └──────────────┘ └─────────────────────┘
Pass auto-updates on
customer's phone
```
**Automatic triggers:**
| Event | Wallet Action | Service Call |
|-------|---------------|--------------|
| Customer enrolls | Create class (if first) + create object | `wallet_service.create_wallet_objects()` |
| Stamp added/redeemed/voided | Update object with new balance | `wallet_service.sync_card_to_wallets()` |
| Points earned/redeemed/voided/adjusted | Update object with new balance | `wallet_service.sync_card_to_wallets()` |
| Customer opens dashboard | Generate save URL (JWT, 1h expiry) | `wallet_service.get_add_to_wallet_urls()` |
No push notifications needed — Google syncs object changes automatically.
### 25.10 Next Steps
### 25.11 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))
1. **Submit for Google production approval** — required before non-test users can add passes
2. **Apple Wallet** — separate setup requiring Apple Developer account, APNs certificates, and pass signing certificates (see [Loyalty Module docs](../modules/loyalty.md#apple-wallet))
---

View File

@@ -167,14 +167,19 @@ Maximum stamps per card per day (default: 5).
### Google Wallet
Architecture: **Server-side storage with API updates**
Architecture: **Server-side storage with automatic API updates**
1. Program created → Create `LoyaltyClass` via Google API
2. Customer enrolls → Create `LoyaltyObject` via Google API
3. Stamp/points change → `PATCH` the object
4. Generate JWT for "Add to Wallet" button
All wallet operations are triggered automatically — no manual API calls needed:
No device registration needed - Google syncs automatically.
| Event | Wallet Action | Trigger |
|-------|---------------|---------|
| Customer enrolls | Create `LoyaltyClass` (first time) + `LoyaltyObject` | `card_service.enroll_customer()``wallet_service.create_wallet_objects()` |
| Stamp/points change | `PATCH` the object with new balance | `stamp_service`/`points_service``wallet_service.sync_card_to_wallets()` |
| Customer views dashboard | Generate JWT "Add to Wallet" URL (1h expiry) | `GET /storefront/loyalty/card``wallet_service.get_add_to_wallet_urls()` |
**Setup:** Configure `LOYALTY_GOOGLE_ISSUER_ID` and `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` env vars. This is a platform-wide setting — all merchants automatically get Google Wallet support. See [Google Wallet Setup](../deployment/hetzner-server-setup.md#step-25-google-wallet-integration) for full instructions.
No device registration needed — Google syncs automatically.
### Apple Wallet

View File

@@ -0,0 +1,140 @@
# Google Wallet Local Testing — Step-by-Step
All code wiring is complete. This guide walks through end-to-end local testing.
## Prerequisites
`.env` must have:
```
LOYALTY_GOOGLE_ISSUER_ID=3388000000023089598
LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/home/samir/Documents/PycharmProjects/letzshop-product-import/orion-488322-2232195cbb62.json
```
Start the server:
```bash
python3 -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
No startup errors expected.
---
## Step 1: Log into the FASHIONHUB store panel
URL: <http://localhost:8000/platforms/loyalty/store/FASHIONHUB/login>
Log in with store credentials.
---
## Step 2: Verify a loyalty program exists
In the store dashboard, check that an active loyalty program exists with stamps or points enabled.
If none exists, create one from Settings > Loyalty:
- **Name**: e.g. "Fashion Rewards"
- **Stamps target** or **Points mode**: enable at least one
- **Welcome bonus points**: optional, set to e.g. 50 to test points on enrollment
---
## Step 3: Enroll a test customer
Two options:
### Option A — Staff enrollment (store panel)
In the store dashboard, go to Loyalty, use "Enroll Customer" by email or customer ID.
Watch terminal for:
```
Created Google Wallet class: <class_id>
Created Google Wallet object: <object_id>
Enrolled customer X in merchant Y loyalty program
```
### Option B — Customer self-enrollment (storefront)
1. Go to: <http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/>
2. Log in as a customer (or create an account)
3. Navigate to Loyalty > "Join Now"
4. After enrollment, you land on the **enroll-success** page
5. Watch terminal for the same Google Wallet creation logs
---
## Step 4: Verify DB records
```sql
-- Program should have google_class_id populated
SELECT id, name, google_issuer_id, google_class_id FROM loyalty_programs;
-- Card should have google_object_id populated
SELECT id, card_number, google_object_id, google_object_jwt
FROM loyalty_cards WHERE customer_id = <your_customer_id>;
```
Both `google_class_id` and `google_object_id` should be non-null.
---
## Step 5: Test the storefront dashboard (wallet button)
1. Go to: <http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/>
2. Log in as the enrolled customer
3. Go to **Account > My Loyalty**
4. Click **"Show Card"** — a modal appears
5. The **"Add to Google Wallet"** button (blue) should be visible
6. Click it — opens `https://pay.google.com/gp/v/save/...` in a new tab
If the button doesn't appear, check browser devtools > Network > `GET /storefront/loyalty/card` response and look at `wallet_urls.google_wallet_url`.
---
## Step 6: Test stamp/points sync
From the store panel (<http://localhost:8000/platforms/loyalty/store/FASHIONHUB/>), add a stamp or earn points for the enrolled customer's card.
Watch terminal for:
```
Updated Google Wallet object for card <card_id>
```
This confirms the wallet pass updates on the customer's phone in real time.
---
## Step 7: Verify the save URL works
The "Add to Google Wallet" URL is JWT-signed. Behaviour:
- **Demo/test mode** (before Google approves your issuer): preview page with unverified issuer warning — this is normal
- **Android**: pass gets added to Google Wallet app
- **Desktop**: Google shows a "Send to phone" option
---
## Troubleshooting
| Symptom | Check |
|---------|-------|
| No wallet logs on enrollment | Verify `LOYALTY_GOOGLE_ISSUER_ID` and `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` are set in `.env` |
| "Google Wallet not configured" in logs | Service account JSON file path is wrong or file is unreadable |
| Button doesn't appear on dashboard | Check `GET /storefront/loyalty/card` response in devtools — `wallet_urls` should have a URL |
| 403 from Google API | Service account doesn't have Wallet API permissions, or issuer ID mismatch |
| JWT URL opens but shows error | Issuer account may not be approved yet — normal for testing |
---
## Key URLs
| Panel | URL |
|-------|-----|
| Store panel (FASHIONHUB) | <http://localhost:8000/platforms/loyalty/store/FASHIONHUB/login> |
| Storefront | <http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/> |
| Admin panel | <http://localhost:8000/admin/login> |