Bug/customer cron job (#516)

* fix(reminder-processor): filter out credited invoices in overdue reminders

* feat: implement linking of transactions to journal entries

- Added POST endpoint for linking a bank transaction to an existing journal entry without creating new bookkeeping.
- Implemented validation for required fields and error handling for various scenarios (e.g., missing journal_entry_id, transaction already linked, journal entry not found).
- Created tests for the new endpoint to cover various cases including successful linking, error responses, and invoice handling.
- Introduced duplicate payment detection logic to prevent double-booking of bank receipts.
- Added a new component for correction affordance in the UI to facilitate user corrections on journal entries.

* feat(invoice-matching): enhance force matching with expected journal entry validation
This commit is contained in:
Mattsson
2026-05-18 13:17:15 +02:00
committed by GitHub
parent a652dcae1a
commit d27c3dd3dc
22 changed files with 2364 additions and 13 deletions
+29 -2
View File
@@ -360,8 +360,35 @@ export const BookInboxItemDirectlySchema = z.object({
transaction_id: uuid.optional(),
})
export const MatchInvoiceSchema = z.object({
invoice_id: uuid,
export const MatchInvoiceSchema = z
.object({
invoice_id: uuid,
// Bypass the soft-duplicate guard (MATCH_INVOICE_POSSIBLE_DUPLICATE).
// Set after the user reviews the candidate verifikation and confirms it
// is not this payment. v1 callers must use a fresh Idempotency-Key on
// the retry — the original is body-hash bound.
force: z.boolean().optional(),
// Required whenever force=true. Echoes the journal_entry_id of the
// candidate the user reviewed in the duplicate-payment-check pre-flight.
// The server re-detects the candidate and refuses force=true unless the
// re-detected id matches this value. That binds the override to a
// specific, user-seen duplicate so an automation can't sweep through
// force=true to bypass the guard without ever consulting the candidate.
expected_journal_entry_id: uuid.optional(),
})
.refine((v) => !v.force || !!v.expected_journal_entry_id, {
message: 'expected_journal_entry_id is required when force=true',
path: ['expected_journal_entry_id'],
})
export const LinkTransactionJournalEntrySchema = z.object({
journal_entry_id: uuid,
// Optional invoice to settle alongside the link. When provided, the
// server inserts an invoice_payments row pointing at the existing JE
// and flips the invoice status with the same optimistic-lock pattern
// as the match-invoice route. Omit to only link the bank transaction
// (e.g. when the JE doesn't relate to a customer invoice).
invoice_id: uuid.optional(),
})
export const CreateTransactionFromDocumentSchema = z.object({
+7
View File
@@ -45,6 +45,13 @@ export interface RouteContext {
* resolved, so handlers can treat this as guaranteed non-null. Routes that
* need to opt out of the guarantee (e.g. onboarding) shouldn't use
* withRouteContext.
*
* Membership invariant: `getActiveCompanyId` only returns a company the
* authenticated user is a current member of (it validates
* `company_members` and excludes archived companies). The handler may
* therefore treat `companyId` as "a company the caller is authorized to
* read", and routes that mutate state additionally enforce a non-viewer
* role via `requireWrite: true`. ASVS V8.2.1 / SOC 2 CC6.3.
*/
companyId: string
}