fix(ux): book documents directly from inbox + attach existing underlag when booking transactions (#670)
* fix(inbox): re-add Bokför manuellt on unmatched documents Pilot feedback: a document in Dokumentinkorg could not be booked without first matching it to a bank transaction, which is impossible for cash expenses and other entries with no bank movement. The backend (/items/:id/book-direct) and BookDirectlyDialog already support standalone booking — re-expose the button in the unmatched state. The dialog still offers optional transaction selection inside. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): pick existing inbox document when booking manually Pilot feedback: "Bokför manuellt" from a transaction only allowed uploading new files — an already-uploaded underlag from the inbox could not be attached. Add a select mode to InboxDocumentPicker (onSelect prop; journalEntryId now optional) and mount it in TransactionBookingDialog: picked documents are linked after the journal entry is created via /api/documents/{id}/link with inbox_item_id, which also stamps the inbox item as consumed so it drops out of every pending surface. Non-ok link responses now count toward the failure toast (previously only network errors did). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(documents): address PR #670 review — stale preview dialog, JE tenancy check Review findings: - InboxDocumentPicker left the preview dialog floating open when a pick was confirmed from inside it (previewItem was never cleared before onClose; the component stays mounted, so the on-open reset never ran). Clear it in both select and link mode. (greptile) - linkToJournalEntry verified the document's company but trusted the client-supplied journal_entry_id (FK only requires existence). Add an explicit company-scoped journal entry lookup; misses map to the existing DOC_LINK_ENTRY_NOT_FOUND envelope. RLS prevented any data leak either way — this makes the rejection explicit. New regression test covers the cross-tenant case. (compliance-swarm A.8.28) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f7cd1b86e7
commit
cac692e293
@@ -74,6 +74,7 @@ describe('POST /api/documents/[id]/link', () => {
|
||||
})
|
||||
|
||||
it('links the document and stamps the inbox item when inbox_item_id is given', async () => {
|
||||
enqueue({ data: { id: 'je-1' } }) // journal entry company check
|
||||
enqueue({ data: { id: 'doc-1', journal_entry_id: 'je-1', file_name: 'x.pdf' } }) // link update
|
||||
enqueue({ data: null }) // inbox stamp update
|
||||
|
||||
@@ -90,6 +91,7 @@ describe('POST /api/documents/[id]/link', () => {
|
||||
})
|
||||
|
||||
it('does not touch the inbox when no inbox_item_id is given', async () => {
|
||||
enqueue({ data: { id: 'je-1' } }) // journal entry company check
|
||||
enqueue({ data: { id: 'doc-1', journal_entry_id: 'je-1', file_name: 'x.pdf' } }) // link update
|
||||
|
||||
const res = await POST(
|
||||
@@ -103,6 +105,7 @@ describe('POST /api/documents/[id]/link', () => {
|
||||
})
|
||||
|
||||
it('maps a period-lock trigger error to PERIOD_LOCKED', async () => {
|
||||
enqueue({ data: { id: 'je-1' } }) // journal entry company check
|
||||
enqueue({
|
||||
data: null,
|
||||
error: { message: 'new row violates ... locked/closed fiscal period' },
|
||||
@@ -118,6 +121,7 @@ describe('POST /api/documents/[id]/link', () => {
|
||||
})
|
||||
|
||||
it('maps an already-linked error to DOC_LINK_ALREADY_LINKED', async () => {
|
||||
enqueue({ data: { id: 'je-1' } }) // journal entry company check
|
||||
enqueue({
|
||||
data: null,
|
||||
error: { message: 'document already linked to another entry' },
|
||||
@@ -129,4 +133,18 @@ describe('POST /api/documents/[id]/link', () => {
|
||||
const { body } = await parseJsonResponse<{ error: { code: string } }>(res)
|
||||
expect(body.error.code).toBe('DOC_LINK_ALREADY_LINKED')
|
||||
})
|
||||
|
||||
it('rejects a journal entry outside the active company with DOC_LINK_ENTRY_NOT_FOUND', async () => {
|
||||
// The company-scoped lookup finds no row — same result whether the id is
|
||||
// bogus or belongs to another tenant. The document must never be updated.
|
||||
enqueue({ data: null }) // journal entry company check → no match
|
||||
const res = await POST(
|
||||
makeReq({ journal_entry_id: 'je-other-company' }),
|
||||
createMockRouteParams({ id: 'doc-1' }),
|
||||
)
|
||||
const { body } = await parseJsonResponse<{ error: { code: string } }>(res)
|
||||
expect(body.error.code).toBe('DOC_LINK_ENTRY_NOT_FOUND')
|
||||
expect(mockSupabase.from).not.toHaveBeenCalledWith('document_attachments')
|
||||
expect(mockSupabase.from).not.toHaveBeenCalledWith('invoice_inbox_items')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user