fix: SIE import voucher range bugs + remove describe transaction (#161)
* fix: fix SIE import voucher range bugs from multi-tenant migration and remove describe transaction feature - Fix reserve_voucher_range RPC parameter mismatch (p_user_id → p_company_id) causing duplicate voucher errors - Fix create-accounts onConflict from user_id to company_id - Add reserve-then-adjust pattern: pre-reserve voucher range before batch insert, release unused range on partial failure - Add release_voucher_range DB function for safe rollback - Remove DescribeTransactionDialog, describe/batch-describe API routes, and related schemas (feature superseded) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — voucher range race condition and orphaned entries - Add p_reserved_highest upper-bound guard to release_voucher_range to prevent rolling back past numbers claimed by concurrent operations - Move highestInsertedVoucher tracking to after both headers AND lines succeed, preventing orphaned journal entries with no lines from being counted as "used" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
197beb1c58
commit
9dceb6285c
@@ -340,16 +340,6 @@ export const MatchSupplierInvoiceSchema = z.object({
|
||||
supplier_invoice_id: uuid,
|
||||
})
|
||||
|
||||
export const DescribeTransactionSchema = z.object({
|
||||
description: z.string().min(3).max(500),
|
||||
})
|
||||
|
||||
export const BatchDescribeSchema = z.object({
|
||||
merchant_name: z.string().min(1),
|
||||
template_id: z.string().min(1),
|
||||
is_business: z.boolean(),
|
||||
user_description: z.string().max(500).optional(),
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// Settings schemas
|
||||
|
||||
@@ -596,6 +596,16 @@ async function importVouchers(
|
||||
|
||||
const currentVoucherNumber = (startNumber as number) || 1
|
||||
|
||||
// Reserve the full voucher number range upfront to prevent concurrent
|
||||
// operations from claiming numbers in our range during batch insertion.
|
||||
const reservedHighest = currentVoucherNumber + preparedVouchers.length - 1
|
||||
await supabase.rpc('reserve_voucher_range', {
|
||||
p_company_id: companyId,
|
||||
p_fiscal_period_id: fiscalPeriodId,
|
||||
p_series: voucherSeries,
|
||||
p_highest_used: reservedHighest,
|
||||
})
|
||||
|
||||
// Batch insert journal entries (in chunks of 100) with retry logic.
|
||||
// Retries handle transient errors (Supabase rate limits, Cloudflare 500s).
|
||||
const BATCH_SIZE = 100
|
||||
@@ -603,6 +613,7 @@ async function importVouchers(
|
||||
const INTER_BATCH_DELAY_MS = 50 // Prevent rate limiting under sustained load
|
||||
let retriedBatches = 0
|
||||
let failedBatches = 0
|
||||
let highestInsertedVoucher = currentVoucherNumber - 1 // nothing inserted yet
|
||||
|
||||
for (let batchStart = 0; batchStart < preparedVouchers.length; batchStart += BATCH_SIZE) {
|
||||
const batch = preparedVouchers.slice(batchStart, batchStart + BATCH_SIZE)
|
||||
@@ -723,6 +734,11 @@ async function importVouchers(
|
||||
}
|
||||
|
||||
if (linesInserted) {
|
||||
// Track highest voucher number only after both headers AND lines succeed,
|
||||
// to avoid counting orphaned entries with no lines as "used".
|
||||
const batchHighest = currentVoucherNumber + batchStart + batch.length - 1
|
||||
highestInsertedVoucher = Math.max(highestInsertedVoucher, batchHighest)
|
||||
|
||||
// Track movements ONLY for successfully inserted vouchers.
|
||||
// This ensures the migration adjustment correctly compensates for
|
||||
// any batches that failed completely.
|
||||
@@ -767,16 +783,19 @@ async function importVouchers(
|
||||
}
|
||||
}
|
||||
|
||||
// Update voucher sequence to reflect all assigned numbers.
|
||||
// next_voucher_number() was called once but we assigned N numbers manually,
|
||||
// so the sequence only got incremented by 1. Fix with GREATEST to avoid races.
|
||||
if (results.created > 0) {
|
||||
const highestUsed = currentVoucherNumber + preparedVouchers.length - 1
|
||||
await supabase.rpc('reserve_voucher_range', {
|
||||
// Adjust voucher sequence after insertion.
|
||||
// Range was pre-reserved to `reservedHighest`. If some batches failed,
|
||||
// release the unused portion to avoid burned numbers and gap-explanation friction.
|
||||
if (highestInsertedVoucher < reservedHighest) {
|
||||
const releaseTarget = highestInsertedVoucher >= currentVoucherNumber
|
||||
? highestInsertedVoucher // partial success: set to actual highest
|
||||
: currentVoucherNumber - 1 // total failure: roll back fully
|
||||
await supabase.rpc('release_voucher_range', {
|
||||
p_company_id: companyId,
|
||||
p_fiscal_period_id: fiscalPeriodId,
|
||||
p_series: voucherSeries,
|
||||
p_highest_used: highestUsed,
|
||||
p_actual_last: releaseTarget,
|
||||
p_reserved_highest: reservedHighest,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user