docs(couples): clarify invite cancellation retention

This commit is contained in:
jjbrunton committed 2026-09-06 12:56:11 +01:00
1 parent 1fed348072
commit 5ac4843095
3 files changed
+26 -6

No files matched your search

+4 -3
View File
@@ -288,7 +288,7 @@ export class PostgresCoupleRepository implements CoupleRepository {
}
const members = await client.query<{ id: string }>(
'select id from profiles where couple_id = $1 for update',
'select id from profiles where couple_id = $1',
[profile.couple_id],
);
if (members.rows.length > 1) {
@@ -299,8 +299,9 @@ export class PostgresCoupleRepository implements CoupleRepository {
}
// At this point the couple has precisely the authenticated creator as a
// member. Removing it cannot delete paired data, and the foreign key
// clears the creator's couple_id while leaving sealed answers intact.
// member. Removing it cannot delete paired data. Answers made before
// invite creation stay sealed; answers made while waiting are coupled
// and are deleted with the cancelled invite.
await client.query('delete from couples where id = $1', [profile.couple_id]);
});
}
@@ -70,12 +70,30 @@ describe.skipIf(!databaseUrl || !localDatabase)('PostgresCoupleRepository', () =
it('cancels a waiting invite only for its creator', async () => {
const coupleId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
const pack = 'abababab-abab-4bab-8bab-abababababab';
const sealedQuestion = 'acacacac-acac-4cac-8cac-acacacacacac';
const waitingQuestion = 'adadadad-adad-4dad-8dad-adadadadadad';
await pool.query('insert into question_packs(id, name) values ($1, $2)', [pack, 'Invite cancellation']);
await pool.query(
'insert into questions(id, pack_id, text) values ($1, $3, $2), ($4, $3, $5)',
[sealedQuestion, 'Sealed before invite', pack, waitingQuestion, 'Answered while waiting'],
);
await pool.query(
"insert into responses(id, user_id, question_id, couple_id, answer) values ($1, $2, $3, null, 'yes')",
[randomUUID(), charlie, sealedQuestion],
);
await repository.create(charlie, coupleId, 'WAIT2345');
await pool.query(
"insert into responses(id, user_id, question_id, couple_id, answer) values ($1, $2, $3, $4, 'no')",
[randomUUID(), charlie, waitingQuestion, coupleId],
);
await repository.cancelInvite(charlie);
await expect(repository.getState(charlie)).resolves.toEqual({ couple: null, partner: null, sealed_count: 0 });
await expect(repository.getState(charlie)).resolves.toEqual({ couple: null, partner: null, sealed_count: 1 });
expect((await pool.query('select count(*)::int as count from couples where id = $1', [coupleId])).rows[0].count).toBe(0);
expect((await pool.query('select count(*)::int as count from responses where question_id = $1', [sealedQuestion])).rows[0].count).toBe(1);
expect((await pool.query('select count(*)::int as count from responses where question_id = $1', [waitingQuestion])).rows[0].count).toBe(0);
});
it('never deletes a joined couple through the invite cancellation path', async () => {
+3 -2
View File
@@ -199,8 +199,9 @@ This prevents race conditions where multiple users try to join simultaneously.
The invite journey leads with the shared outcome, then shows the recoverable
eight-character code. It offers both a bare-code copy and a full
`https://sauci.app/join/{code}` link copy. Cancelling an invite warns that it
removes only an unjoined invite and retains sealed answers. The five-second
poll surfaces an inline offline state but continues retrying.
removes only an unjoined invite. Sealed answers made before invite creation
remain, while answers made while waiting are deleted with the invite. The
five-second poll surfaces an inline offline state but continues retrying.
When pairing succeeds, both members are routed once per account and couple to
the `/(app)/paired` payoff screen. It reads only `newMatchesCount`: it never