Files
accounted/tests/pg/invoice-inbox-kind-hint.pg.test.ts
Mattsson 867767a22f feat(inbox): document-type badge and filter, +lev/+ver plus-addressing (#2129) (#2148)
* feat(inbox): document-type badge and filter, +lev/+ver plus-addressing (#2129)

Phase 1: every inbox row shows its document kind (Kvitto, Leverantorsfaktura, Myndighetsbrev, Ovrigt) from the existing AI documentKind, and a second menu next to the status filter narrows the list to leverantorsfakturor or underlag. Pure predicate in lib/documents/inbox-kind.ts with tests.

Phase 2: the shared inbox address accepts RFC 5233 plus-addressing. The webhook splits the local part at the first + and looks up the base, so <local>+anything@ now reaches the company instead of 404ing. +lev and +ver land in the new nullable invoice_inbox_items.kind_hint column (CHECK supplier_invoice | receipt), threaded through EmailMeta into both inbox inserts and returned by GET /items. kind_hint wins over documentKind for the badge and the filter and survives re-extraction because it is a column. The sources panel shows both tagged addresses with a one-line hint (sv + en).

Tests: filter predicate per kind and null; parser and tag mapping; webhook routes +LEV and an unknown tag; pg test pins the CHECK and NULL default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hzv2Z2eCq8iJAAe8XC1hNr

* fix(inbox): honest empty state under a type filter, detail pane shares the row's kind resolution

Skeptic findings on #2148: with a type filter narrowing 'Att göra' to zero the empty state claimed 'allt är bearbetat' while the status trigger still counted pending rows; it now says no items of that type are here (sv + en). The fields rail printed the AI documentKind only, so a +lev hint could disagree with the row badge; it now uses resolveInboxKind like the list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hzv2Z2eCq8iJAAe8XC1hNr

* fix(inbox): keep the type-filter empty state off purchase lists, carry kind_hint onto rejected attachment rows

CodeRabbit on #2148: the purchase lists (Saknar underlag, Hämta från portal) ignore the type menu, so a leftover kind filter must not pick their empty-state copy. A rejected attachment (unsupported MIME, too large) now keeps the sender's +lev / +ver hint on its error row like every other inbox insert; the allowlist test covers it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hzv2Z2eCq8iJAAe8XC1hNr

* fix(inbox): set the +lev/+ver kind hint only when the shared address resolved the company

CodeRabbit on #2148: the hint was computed before recipient resolution, so a tag on an unknown or retired shared address could ride along onto a custom-domain match. It is now assigned inside the active shared-inbox branch only; regression test covers the multi-recipient case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hzv2Z2eCq8iJAAe8XC1hNr

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 08:50:42 +02:00

51 lines
1.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { getPool } from './setup'
import { seedCompany } from './fixtures'
/**
* invoice_inbox_items.kind_hint (migration 20260901210000, issue #2129).
*
* The plus-address tag on the shared inbox address (+lev / +ver) lands here
* as the sender's declared document kind. These tests pin the two accepted
* values, that the CHECK refuses anything else, and that the column is
* nullable with a NULL default so untagged mail and pre-migration rows are
* untouched.
*/
describe('invoice_inbox_items.kind_hint (pg)', () => {
it.each(['supplier_invoice', 'receipt'])('accepts %s', async (hint) => {
const { userId, companyId } = await seedCompany()
const { rows } = await getPool().query<{ kind_hint: string }>(
`INSERT INTO public.invoice_inbox_items (company_id, user_id, source, kind_hint)
VALUES ($1, $2, 'email', $3)
RETURNING kind_hint`,
[companyId, userId, hint],
)
expect(rows[0].kind_hint).toBe(hint)
})
it('refuses a hint outside the two documented tags', async () => {
const { userId, companyId } = await seedCompany()
await expect(
getPool().query(
`INSERT INTO public.invoice_inbox_items (company_id, user_id, source, kind_hint)
VALUES ($1, $2, 'email', 'government_letter')`,
[companyId, userId],
),
).rejects.toThrow(/invoice_inbox_items_kind_hint_check|violates check constraint/i)
})
it('defaults to NULL when the writer says nothing', async () => {
const { userId, companyId } = await seedCompany()
const { rows } = await getPool().query<{ kind_hint: string | null }>(
`INSERT INTO public.invoice_inbox_items (company_id, user_id, source)
VALUES ($1, $2, 'upload')
RETURNING kind_hint`,
[companyId, userId],
)
expect(rows[0].kind_hint).toBeNull()
})
})