mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Merge solo sealed answers that unlock on pairing into staging
This commit is contained in:
commit
7d392e756d
26 files changed
+510
-75
No files matched your search
@@ -16,6 +16,54 @@ A user can be in one of three states:
|
||||
| **Waiting** | Set | `null` | Created couple, waiting for partner |
|
||||
| **Paired** | Set | Exists | Both partners joined |
|
||||
|
||||
Unpaired and Waiting are not dead ends: both states can answer questions
|
||||
immediately through the standalone API (`POST /v1/responses`). See
|
||||
[Solo answering and sealed answers](#solo-answering-and-sealed-answers) below.
|
||||
|
||||
## Solo answering and sealed answers
|
||||
|
||||
An Unpaired or Waiting user answers straight into the swipe flow instead of
|
||||
being blocked until a partner joins. `responses.couple_id` is nullable
|
||||
(`apps/api/drizzle/0021_solo_sealed_answers.sql`): a response submitted with no
|
||||
couple is inserted with `couple_id IS NULL` and is called a **sealed answer**.
|
||||
It is banked, not lost, and counts toward the daily response limit like any
|
||||
other answer, but no match reconciliation runs for it (there is no partner
|
||||
response to compare against).
|
||||
|
||||
`GET /v1/couple` reports `sealed_count`, the caller's count of responses with
|
||||
`couple_id IS NULL`, alongside `couple` and `partner`, so the client can show
|
||||
"N sealed answers" before and while waiting to pair. `POST /v1/responses` also
|
||||
returns `sealed_count` in its response body for a solo submission (its `match`
|
||||
is always `null`).
|
||||
|
||||
### Claim on pair
|
||||
|
||||
`PostgresCoupleRepository.join()` (`apps/api/src/domains/couples/repository.ts`)
|
||||
is the only place membership grows from one member to two. In the same
|
||||
transaction that adds the second member, it:
|
||||
|
||||
1. Reassigns every sealed answer (`couple_id IS NULL`) belonging to either
|
||||
member to the new couple.
|
||||
2. For every question where both members now have an answer, computes the
|
||||
match type with the same rule `calculateMatchType` uses for live answers
|
||||
and upserts into `matches` (respecting the `(couple_id, question_id)`
|
||||
uniqueness), so a couple's history of solo answers surfaces as matches the
|
||||
moment they pair, not only for answers given after pairing.
|
||||
|
||||
A user who re-pairs after their previous couple was deleted only has
|
||||
`couple_id IS NULL` answers left to claim (the old couple's responses were
|
||||
cascade-deleted with it), which this handles the same way as first-time
|
||||
pairing.
|
||||
|
||||
### Push nudge escalation
|
||||
|
||||
The unpaired push reminder (`apps/api/src/domains/operations/repository.ts`,
|
||||
`produce()`) reads each eligible recipient's sealed count and leads with it
|
||||
once it is greater than zero: "You have N sealed answers waiting. Invite your
|
||||
partner to unlock what you said about them." Recipients with no sealed
|
||||
answers yet see the original invite-code copy. The existing 3-day cadence and
|
||||
18:00-local gate are unchanged.
|
||||
|
||||
## Edge Functions
|
||||
|
||||
### `manage-couple` (POST)
|
||||
|
||||
@@ -139,11 +139,16 @@ const sorted = filtered.sort((a, b) => {
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| No packs enabled | Shows "No packs enabled" prompt |
|
||||
| No packs enabled | Shows "No packs enabled" prompt (paired couples only; a couple with no configured packs has not chosen yet) |
|
||||
| All questions answered | Shows "All caught up!" with refresh button |
|
||||
| No partner paired | Shows pairing prompt |
|
||||
| No partner paired | Answers straight into the swipe flow instead of a blocking prompt. Every answer is sealed (`couple_id IS NULL`, no match). A persistent banner reports the sealed count with an invite CTA. See [Solo answering and sealed answers](couple-pairing.md#solo-answering-and-sealed-answers). |
|
||||
| Pack disabled mid-session | Client filters out questions from disabled packs |
|
||||
|
||||
An unpaired user has no `couple_packs` row to configure (it is keyed to
|
||||
`couple_id`), so `recommended()`'s "unconfigured couple" fallback (every
|
||||
public pack) is what they always see; the pack-selection screen only applies
|
||||
once paired.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Value | Location |
|
||||
|
||||
+8
-1
@@ -68,7 +68,7 @@ erDiagram
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid question_id FK
|
||||
uuid couple_id FK
|
||||
uuid couple_id FK "nullable: null is a sealed solo answer, claimed at pairing"
|
||||
answer_type answer "yes/no/maybe"
|
||||
timestamptz created_at
|
||||
}
|
||||
@@ -196,10 +196,17 @@ allowed.
|
||||
5. Match triggers push notification to both partners
|
||||
6. Match unlocks chat thread in `messages` table
|
||||
|
||||
Without a couple yet, step 3 has no partner response to check: the response is
|
||||
saved with `couple_id IS NULL` (a sealed answer) and no match is created. See
|
||||
[Solo answering and sealed answers](couple-pairing.md#solo-answering-and-sealed-answers).
|
||||
|
||||
### Couple Pairing Flow
|
||||
1. First user creates couple, gets `invite_code`
|
||||
2. Second user joins via `manage-couple` edge function with invite code
|
||||
3. Both profiles' `couple_id` set to same couple
|
||||
4. Every sealed answer (`couple_id IS NULL`) belonging to either member is
|
||||
claimed into the couple, and matches are computed for every question both
|
||||
members have now answered
|
||||
|
||||
### Pack Selection Flow
|
||||
1. Couples can enable/disable packs via `couple_packs` junction table
|
||||
|
||||
Reference in new issue
Block a user