docs(manual): Batch 6 — correct Billing webhook flow, add CouplePremiumChecker, fix quiet-hours and notification_queue claims

This commit is contained in:
null 2026-06-28 11:10:08 -05:00
parent 74e46761f2
commit e4175ebb52
2 changed files with 7 additions and 7 deletions

View File

@ -79,8 +79,8 @@ collection name, and architectural fact. Never assume.
| 2 | ✅ done | No anonymous sign-in or account linking in code; Android uses legacy Google Sign-In SDK (idToken), not Credential Manager; `encryptionMigrationUsers` field does not exist | Removed anonymous auth and account-linking claims; corrected Google Sign-In description; removed `encryptionMigrationUsers` from couples model and added note that `encryptionVersion` is always `2`. |
| 3 | ✅ done | `/users/{uid}` model missing `sex`, `partnerId`, `plan`, `lastActiveAt`, notification prefs, quiet-hours, `fcmToken`; `hasPremium` is not a real root field (premium lives in `/entitlements/premium`); `/couples/{coupleId}` listed non-existent `encryptionMigrationUsers`; date plan fields wrong; date plan preference fields wrong; bucket list fields wrong; missing `/answers/{userId}/secure/{doc}` for schemaVersion 2 | Updated `/users/{uid}` to full allowlist; removed `hasPremium` root field and added note about `/entitlements/premium`; removed `encryptionMigrationUsers`; corrected date plan, preference, and bucket list fields; added `secure` subdoc to daily-question model. |
| 4 | ✅ done | Handler table missing functions (`syncEntitlement`, `sendDailyQuestionProactiveReminder`, `sendReengagementReminder`, `sendChallengeDayReminders`, `unlockDueMemoryCapsules`, `sendGentleReminderCallable`, `onEntitlementChanged`, `onAnswerRevealed`, `onCoupleLeave`, `leaveCoupleCallable`, `submitOutcomeCallable`, `scheduledOutcomesReminder`, `onGamePartFinished`, `notifyOnDateMatch`); still listed removed `health` endpoint and old name `createDateMatchOnMutualLove`; webhook section said 200-ack-before-process (now process-before-ack); `scheduledOutcomesReminder` timezone wrong | Removed `health`; added all current functions and corrected `notifyOnDateMatch`; updated module responsibilities; corrected webhook to process-before-ack with 500-on-failure; fixed schedule timezones (only `assignDailyQuestion`, `sendDailyQuestionProactiveReminder`, `sendReengagementReminder` use `America/Chicago`). |
| 5 | in progress | | |
| 6 | todo | | |
| 5 | ✅ done | `users/{uid}` section still described non-existent `hasPremium` root field; `couples/{coupleId}` section said all client writes denied (create is shape-restricted but possible); helper paragraph listed non-existent `isStartingEncryptionMigration`/`isCompletingOwnEncryptionMigration` | Updated `users/{uid}` to clarify `plan` is client-written and premium lives in `/entitlements/premium`; corrected couples create/update description; removed migration-helper references. |
| 6 | in_progress | | |
| 7 | todo | | |
| 8 | todo | | |
| 9 | todo | | |

View File

@ -757,7 +757,7 @@ Every function module follows the same shape:
| HTTPS onRequest | `revenueCatWebhook` | Path-based; bypass callable auth. Webhook requires Ed25519 signature verification. The unauthenticated `health` endpoint was removed in a security review. |
| HTTPS onCall | `createInviteCallable`, `acceptInviteCallable`, `syncEntitlement`, `sendDailyQuestionReminder`, `sendPartnerAnsweredNotification`, `sendGentleReminderCallable`, `submitOutcomeCallable`, `leaveCoupleCallable`, `checkDeviceIntegrity`, `assignDailyQuestionCallable` | Caller must be authenticated. Errors throw `HttpsError`. |
| Firestore onCreate | `onAnswerWritten`, `onAnswerRevealed`, `onMessageWritten`, `onCoupleLeave`, `onUserDelete`, `onGameSessionUpdate`, `onGamePartFinished`, `notifyOnDateMatch` | Event-driven; best-effort. |
| Firestore onUpdate | `onEntitlementChanged` | Watches entitlement doc and mirrors premium state to the user root doc for clients that read `plan`. |
| Firestore onUpdate | `onEntitlementChanged` | Fires when `users/{uid}/entitlements/premium` changes; sends a partner-facing FCM when the user gains premium. It does NOT mirror premium state to the user root doc. |
| Auth onDelete | `onUserDelete` | Auth user deletion cascade. |
| Pub/Sub schedule | `assignDailyQuestion`, `scheduledOutcomesReminder`, `sendDailyQuestionProactiveReminder`, `sendReengagementReminder`, `unlockDueMemoryCapsules`, `sendChallengeDayReminders` | Cron expression; timezone is `America/Chicago` only where explicitly set. |
@ -814,7 +814,7 @@ The Android `FirestoreEntitlementChecker` is the source of truth for premium sta
The iOS `DefaultEntitlementChecker` actor does **not** observe Firestore entitlements. It reads RevenueCat `CustomerInfo` only, via `Purchases.shared.customerInfoStream`. **iOS premium state is therefore client-verified, not server-verified** — this is a known gap that should be closed before production.
The `EntitlementChecker` interface (Android) is intentionally narrow:
The Android app uses both `EntitlementChecker` (per-user server-verified premium) and `CouplePremiumChecker` (couple-shared premium: true if either partner has premium). `CouplePremiumChecker` is the drop-in used for couple-wide gates like chat media, shared games, and date features so a single subscription covers both partners.
```kotlin
interface EntitlementChecker {
@ -833,7 +833,7 @@ Callers collect `isPremium()` reactively rather than caching a one-time snapshot
- **Path**: HTTPS, POST only. GETs return 405.
- **Auth**: Ed25519 signature verification. `REVENUECAT_SIGNING_KEY` env var holds the base64-encoded DER/SPKI public key. Missing key → 500 (config error). Invalid/missing signature → 401.
- **Body**: RevenueCat event payload. Malformed payload → 400.
- **Flow**: acknowledge 200 immediately, then call `applyEntitlementEvent(event)`.
- **Flow**: `applyEntitlementEvent(event)` runs **before** the HTTP 200 acknowledgement. If it throws, the function returns HTTP 500 so RevenueCat retries. The function is idempotent, so retries are safe.
- **Idempotency**: `entitlement_events/{eventId}` records processed events. Re-delivered events are dropped.
### Premium-gated features and gate pattern
@ -880,7 +880,7 @@ Both clients use the Firebase Messaging SDK. Android uses FirebaseMessagingServi
### Quiet hours
`QuietHours` is a `DataStore`-persisted data class in `SettingsRepository`. It suppresses non-critical notifications during a configured window. **Server-side quiet-hour suppression is not implemented today** — all suppression happens on the client. The push is still delivered, the client decides whether to display it. This is a known gap; if battery/UX becomes a problem, the suppression should move to the server.
`QuietHours` is a `DataStore`-persisted data class in `SettingsRepository`. It suppresses non-critical notifications during a configured window. **Server-side quiet-hour suppression is implemented** (`functions/src/notifications/quietHours.ts`): `onAnswerWritten` reads the recipient's `quietHoursEnabled` / `quietHoursStartMinutes` / `quietHoursEndMinutes` / `timezone` fields and skips the FCM push when the recipient is in their quiet window. Client-side suppression remains as a fail-open backup; the server-side check is what keeps the "no notifications" promise when the app is backgrounded (M-001).
### Daily question reminders
@ -915,7 +915,7 @@ The notification is both an FCM push (for the system tray) and an entry in `user
### Per-user notification_queue
`users/{uid}/notification_queue/` is a server-only collection that stores pending partner notifications. The FCM push is the user-visible surface; the queue is for in-app polling and for tracking delivery. Reads are denied for clients; the app reacts to FCM, not the queue.
`users/{uid}/notification_queue/` is a server-written collection that stores pending partner notifications. The FCM push is the user-visible surface; the queue is also read by the Android `FirestoreActivityDataSource` for the in-app "Together" activity feed. The owner can update the `read` flag; everything else is server-only.
### Game session push semantics (idempotent flag-claim)