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:
+29
-2
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user