fix(parties): skip nameless suppliers and customers in the party backfill (#2174)

* fix(parties): skip nameless suppliers and customers in the party backfill

The substrate migration failed on prod at the backfill: three rows (one
supplier, two customers) have an empty name and ensure_party refuses a
nameless party. The file never applied there, so it is corrected in place
rather than chased with a migration that could not run before it. Rows
without a name keep party_id NULL; the suggestion pipeline names them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(parties): ensure_party writes only under the caller's own identity

Authenticated callers must pass their own user id; the service role
(auth.uid() NULL: migrations, MCP, cron) may act for another user. Same
guard as apply_party_suggestions and decide_parties. Superagent P2 on
#2172.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-02 17:57:34 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent daeab67dca
commit a24982463b
3 changed files with 29 additions and 1 deletions
+18
View File
@@ -139,6 +139,24 @@ describe('parties substrate (pg)', () => {
expect(visibleToStranger).toBe(0)
})
it('ensure_party refuses a p_user_id that is not the authenticated caller', async () => {
const mine = await seedCompany()
const other = await seedCompany()
await expect(
withUserContext(mine.userId, (client) =>
client.query(`SELECT public.ensure_party($1, $2, 'Spoofed AB', NULL, 'company', 'manual')`, [mine.companyId, other.userId]),
),
).rejects.toMatchObject({ code: '42501' })
const own = await withUserContext(mine.userId, async (client) => {
const { rows } = await client.query<{ id: string }>(
`SELECT public.ensure_party($1, $2, 'Own AB', NULL, 'company', 'manual') AS id`,
[mine.companyId, mine.userId],
)
return rows[0]!.id
})
expect(own).toMatch(/^[0-9a-f-]{36}$/)
})
it('refuses to attach facts, identities, decisions, roles or merges to another company\'s party', async () => {
const mine = await seedCompany()
const theirs = await seedCompany()