Files
accounted/extensions/general/invoice-inbox/__tests__/match-transaction.test.ts
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

139 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { invoiceInboxExtension } from '@/extensions/general/invoice-inbox'
import {
createQueuedMockSupabase,
createMockRequest,
parseJsonResponse,
} from '@/tests/helpers'
import type { ExtensionContext } from '@/lib/extensions/types'
function findRoute(method: string, path: string) {
return invoiceInboxExtension.apiRoutes!.find(
(r) => r.method === method && r.path === path,
)!
}
const matchRoute = findRoute('POST', '/items/:id/match-transaction')
const unmatchRoute = findRoute('POST', '/items/:id/unmatch-transaction')
function buildCtx(supabase: unknown): ExtensionContext {
return {
userId: 'user-1',
companyId: 'company-1',
extensionId: 'invoice-inbox',
supabase: supabase as ExtensionContext['supabase'],
emit: vi.fn(),
settings: { get: vi.fn(), set: vi.fn() },
storage: { from: vi.fn() } as unknown as ExtensionContext['storage'],
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as unknown as ExtensionContext['log'],
services: {},
} as ExtensionContext
}
function makeReq(path: string, body?: unknown) {
return createMockRequest(path, {
method: 'POST',
searchParams: { _id: 'item-1' },
body,
})
}
describe('POST /items/:id/match-transaction', () => {
let mock: ReturnType<typeof createQueuedMockSupabase>
beforeEach(() => {
mock = createQueuedMockSupabase()
})
it('returns 401 without ctx', async () => {
const res = await matchRoute.handler(makeReq('/items/item-1/match-transaction'))
expect(res.status).toBe(401)
})
it('returns 400 when transaction_id is missing', async () => {
const ctx = buildCtx(mock.supabase)
const res = await matchRoute.handler(
makeReq('/items/item-1/match-transaction', {}),
ctx,
)
expect(res.status).toBe(400)
})
it('returns 404 when transaction is outside this company', async () => {
// tx lookup returns null → 404 before any further calls
mock.enqueue({ data: null })
const ctx = buildCtx(mock.supabase)
const res = await matchRoute.handler(
makeReq('/items/item-1/match-transaction', { transaction_id: 'tx-1' }),
ctx,
)
expect(res.status).toBe(404)
})
it('sets matched_transaction_id and mirrors document_id onto the transaction', async () => {
// Sequence: tx lookup (no doc yet) → inbox doc lookup → inbox update → tx update
mock.enqueue({ data: { id: 'tx-1', document_id: null } })
mock.enqueue({ data: { id: 'item-1', document_id: 'doc-1' } })
mock.enqueue({ data: { id: 'item-1', matched_transaction_id: 'tx-1' } })
mock.enqueue({ data: null }) // tx update result (we don't read)
const ctx = buildCtx(mock.supabase)
const res = await matchRoute.handler(
makeReq('/items/item-1/match-transaction', { transaction_id: 'tx-1' }),
ctx,
)
const { body } = await parseJsonResponse<{ data: { matched_transaction_id: string } }>(res)
expect(res.status).toBe(200)
expect(body.data.matched_transaction_id).toBe('tx-1')
})
it('does not overwrite an existing tx.document_id', async () => {
// tx already has a doc: match still succeeds but the tx update for
// document_id is skipped (no enqueue beyond the inbox update).
mock.enqueue({ data: { id: 'tx-1', document_id: 'existing-doc' } })
mock.enqueue({ data: { id: 'item-1', document_id: 'doc-1' } })
mock.enqueue({ data: { id: 'item-1', matched_transaction_id: 'tx-1' } })
const ctx = buildCtx(mock.supabase)
const res = await matchRoute.handler(
makeReq('/items/item-1/match-transaction', { transaction_id: 'tx-1' }),
ctx,
)
const { body } = await parseJsonResponse<{ data: { matched_transaction_id: string } }>(res)
expect(res.status).toBe(200)
expect(body.data.matched_transaction_id).toBe('tx-1')
})
})
describe('POST /items/:id/unmatch-transaction', () => {
let mock: ReturnType<typeof createQueuedMockSupabase>
beforeEach(() => {
mock = createQueuedMockSupabase()
})
it('clears matched_transaction_id and mirrors the unmatch onto the transaction', async () => {
// Sequence: existing-state lookup → inbox update → tx update
mock.enqueue({
data: {
id: 'item-1',
document_id: 'doc-1',
matched_transaction_id: 'tx-1',
},
})
mock.enqueue({ data: { id: 'item-1', matched_transaction_id: null } })
mock.enqueue({ data: null }) // tx update result
const ctx = buildCtx(mock.supabase)
const res = await unmatchRoute.handler(
makeReq('/items/item-1/unmatch-transaction'),
ctx,
)
const { body } = await parseJsonResponse<{
data: { matched_transaction_id: string | null }
}>(res)
expect(res.status).toBe(200)
expect(body.data.matched_transaction_id).toBeNull()
})
it('returns 401 without ctx', async () => {
const res = await unmatchRoute.handler(makeReq('/items/item-1/unmatch-transaction'))
expect(res.status).toBe(401)
})
})