fix(transactions): steer duplicate guard to matching for ledger-only vouchers (#958)

When the booking-time duplicate guard flags a ledger-only voucher
candidate (transaction_id null: a verifikat from an SIE import, a paid
invoice, a salary payout), the dialog's primary action is now "Matcha
mot verifikatet": it links the bank transaction to the existing voucher
via /api/reconciliation/bank/link (the same path MatchVoucherDialog
uses) instead of double-booking the same affarshandelse. "Bokfor anda"
stays available but demoted to an outline button. Sibling-transaction
candidates keep today's layout: matching a second bank line onto a
voucher that already has one is the N:1 edge case, not the default.

Wired through both call sites: the transactions page (runCategorize)
reuses handleVoucherLinked's refresh, and TransactionBookingDialog
(JournalEntryForm, /api/transactions/[id]/book path) reuses the booked
flow so in-dialog attached documents land on the matched verifikat and
the row leaves the list, with a "Bankhandelsen kopplad" toast instead
of "Bokford". No lib change: the candidate payload already carries the
transaction_id discriminator, covered by existing detection tests.

Closes #919.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-09 22:08:44 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent d7e110b6b2
commit 82c66739d7
9 changed files with 200 additions and 25 deletions
+19 -1
View File
@@ -26,7 +26,7 @@ import { TemplateForm } from '@/components/settings/TemplateForm'
import CreatePeriodDialog from '@/components/bookkeeping/CreatePeriodDialog'
import { ActivateAccountsDialog } from '@/components/bookkeeping/ActivateAccountsDialog'
import { AddAccountDialog } from '@/components/bookkeeping/AddAccountDialog'
import DuplicateBookingDialog from '@/components/transactions/DuplicateBookingDialog'
import DuplicateBookingDialog, { type DuplicateMatchTransaction } from '@/components/transactions/DuplicateBookingDialog'
import { Skeleton } from '@/components/ui/skeleton'
import {
useSubmitWithAccountActivation,
@@ -82,6 +82,13 @@ interface Props {
editEntryId?: string
/** Fired after a successful draft edit (editEntryId path). */
onUpdated?: () => void
/** The bank transaction being booked (set by TransactionBookingDialog).
* Enables the duplicate guard's "Matcha mot verifikatet" action for
* ledger-only voucher candidates. */
duplicateMatchTransaction?: DuplicateMatchTransaction
/** Fired after the duplicate guard's match action links the transaction to
* the existing voucher (no new entry was created). */
onDuplicateMatched?: (journalEntryId: string) => void
}
const BLANK_LINE: FormLine = { account_number: '', debit_amount: '', credit_amount: '', line_description: '' }
@@ -101,6 +108,8 @@ export default function JournalEntryForm({
bare,
editEntryId,
onUpdated,
duplicateMatchTransaction,
onDuplicateMatched,
}: Props) {
const { canWrite } = useCanWrite()
const { toast } = useToast()
@@ -1939,6 +1948,15 @@ export default function JournalEntryForm({
processing={isSubmitting}
onCancel={() => setDuplicateCandidate(null)}
onBookAnyway={handleBookAnyway}
matchTransaction={duplicateMatchTransaction ?? null}
onMatched={
onDuplicateMatched
? (_transactionId, journalEntryId) => {
setDuplicateCandidate(null)
onDuplicateMatched(journalEntryId)
}
: undefined
}
/>
</div>
)