867767a22f
* 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>
51 lines
1.8 KiB
TypeScript
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()
|
|
})
|
|
})
|