mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
docs(couples): clarify invite cancellation retention
This commit is contained in:
1 parent
1fed348072
commit
5ac4843095
3 files changed
+26
-6
No files matched your search
@@ -288,7 +288,7 @@ export class PostgresCoupleRepository implements CoupleRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const members = await client.query<{ id: string }>(
|
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],
|
[profile.couple_id],
|
||||||
);
|
);
|
||||||
if (members.rows.length > 1) {
|
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
|
// At this point the couple has precisely the authenticated creator as a
|
||||||
// member. Removing it cannot delete paired data, and the foreign key
|
// member. Removing it cannot delete paired data. Answers made before
|
||||||
// clears the creator's couple_id while leaving sealed answers intact.
|
// 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]);
|
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 () => {
|
it('cancels a waiting invite only for its creator', async () => {
|
||||||
const coupleId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
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 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 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 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 () => {
|
it('never deletes a joined couple through the invite cancellation path', async () => {
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
`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
|
removes only an unjoined invite. Sealed answers made before invite creation
|
||||||
poll surfaces an inline offline state but continues retrying.
|
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
|
When pairing succeeds, both members are routed once per account and couple to
|
||||||
the `/(app)/paired` payoff screen. It reads only `newMatchesCount`: it never
|
the `/(app)/paired` payoff screen. It reads only `newMatchesCount`: it never
|
||||||
|
|||||||
Reference in new issue
Block a user