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>
86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
/**
|
|
* POST /api/transactions/[id]/link-journal-entry
|
|
*
|
|
* Link a bank transaction to an already-posted journal entry without
|
|
* creating new bookkeeping. Used by the duplicate-payment UI when the user
|
|
* confirms the suggested candidate already books this receipt: typically
|
|
* a manual verifikation made outside the match-invoice flow.
|
|
*
|
|
* Body:
|
|
* - journal_entry_id (required): the existing posted JE to link to.
|
|
* - invoice_id (optional): when supplied, also inserts an
|
|
* invoice_payments row pointing at the existing JE and flips the
|
|
* invoice status to 'paid' / 'partially_paid'.
|
|
*
|
|
* Effects:
|
|
* - transactions.journal_entry_id = je_id
|
|
* - transactions.is_business = true
|
|
* - transactions.potential_invoice_id = null
|
|
* - transactions.potential_supplier_invoice_id = null
|
|
* - if invoice_id provided:
|
|
* - invoice_payments row inserted (transaction_id, amount, journal_entry_id)
|
|
* - invoice.status / paid_amount / remaining_amount updated
|
|
*
|
|
* NEVER creates a new journal entry; the underlying double-entry already
|
|
* exists. The match log records 'linked_to_existing_voucher' for audit.
|
|
*
|
|
* Core logic is shared with the MCP commit handler in lib/pending-operations/commit.ts
|
|
* (gnubok_link_transaction_to_journal_entry): see lib/transactions/link-journal-entry.ts.
|
|
*/
|
|
import { NextResponse } from 'next/server'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { errorResponse, errorResponseFromCode } from '@/lib/errors/get-structured-error'
|
|
import { validateBody } from '@/lib/api/validate'
|
|
import { LinkTransactionJournalEntrySchema } from '@/lib/api/schemas'
|
|
import { ensureInitialized } from '@/lib/init'
|
|
import { linkTransactionToJournalEntry } from '@/lib/transactions/link-journal-entry'
|
|
|
|
ensureInitialized()
|
|
|
|
export const POST = withRouteContext(
|
|
'transaction.link_journal_entry',
|
|
async (request, ctx, { params }: { params: Promise<{ id: string }> }) => {
|
|
const { id: transactionId } = await params
|
|
const { user, supabase, companyId, log, requestId } = ctx
|
|
|
|
const validation = await validateBody(request, LinkTransactionJournalEntrySchema, {
|
|
log,
|
|
operation: 'transaction.link_journal_entry',
|
|
})
|
|
if (!validation.success) return validation.response
|
|
const { journal_entry_id, invoice_id } = validation.data
|
|
|
|
const txLog = log.child({ transactionId, journalEntryId: journal_entry_id, invoiceId: invoice_id })
|
|
|
|
const outcome = await linkTransactionToJournalEntry(supabase, user.id, companyId, {
|
|
transactionId,
|
|
journalEntryId: journal_entry_id,
|
|
invoiceId: invoice_id,
|
|
})
|
|
|
|
if (!outcome.ok) {
|
|
// LINK_TX_DB_ERROR is the only code emitted on raw DB failure; route it
|
|
// through the generic errorResponse fallback so the INTERNAL_ERROR envelope
|
|
// matches the rest of the API. Everything else maps to a structured-error
|
|
// entry with the right HTTP status.
|
|
if (outcome.code === 'LINK_TX_DB_ERROR') {
|
|
return errorResponse(new Error(String(outcome.details?.reason ?? 'Database error')), txLog, {
|
|
requestId,
|
|
})
|
|
}
|
|
return errorResponseFromCode(outcome.code, txLog, { requestId, details: outcome.details })
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
journal_entry_id: outcome.result.journalEntryId,
|
|
voucher_label: outcome.result.voucherLabel,
|
|
invoice_id: outcome.result.invoiceId,
|
|
invoice_status: outcome.result.invoiceStatus,
|
|
paid_amount: outcome.result.paidAmount,
|
|
remaining_amount: outcome.result.remainingAmount,
|
|
})
|
|
},
|
|
{ requireWrite: true },
|
|
)
|