feat: wire Zod validation into API routes, improve types and components
- Add 8 new Zod schemas (UpdateCustomer, UpdateSupplier, UpdateSupplierInvoice, UpdateAccount, BankUnlink, RunReconciliation, CorrectJournalEntry, EvaluateMappingRules) and wire validateBody() into 24 JSON-body API routes - Remove redundant manual validation checks replaced by Zod - Add comprehensive schema tests (222 tests) - Improve type definitions in types/index.ts with expanded interfaces - Refactor extension types (push-notifications, receipt-ocr) for cleaner imports - Update transaction components (BatchCategorySelector, SwipeCategorizationView, QuickReviewDialog, VatTreatmentSelect) and invoice inbox workspace - Add invoice-inbox utilities and type decoupling tests - Fix NE-bilaga, SRU export, and invoice PDF template type usage - Update CLAUDE.md with expanded architecture documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cfc4cb6849
commit
acb85edf4a
@@ -1,5 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { UpdateAccountSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
@@ -70,19 +72,13 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updates: Record<string, unknown> = {}
|
||||
if (body.account_name !== undefined) updates.account_name = body.account_name
|
||||
if (body.is_active !== undefined) updates.is_active = body.is_active
|
||||
if (body.description !== undefined) updates.description = body.description
|
||||
if (body.default_vat_code !== undefined) updates.default_vat_code = body.default_vat_code
|
||||
if (body.sru_code !== undefined) updates.sru_code = body.sru_code
|
||||
const validation = await validateBody(request, UpdateAccountSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.update(updates)
|
||||
.update(body)
|
||||
.eq('user_id', user.id)
|
||||
.eq('account_number', number)
|
||||
.select()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateAccountSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const supabase = await createClient()
|
||||
@@ -47,7 +49,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validation = await validateBody(request, CreateAccountSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateFiscalPeriodInput } from '@/types'
|
||||
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateFiscalPeriodSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
@@ -32,7 +33,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json() as CreateFiscalPeriodInput
|
||||
const validation = await validateBody(request, CreateFiscalPeriodSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Validate period duration (max 18 months per BFL 3 kap.)
|
||||
const durationError = validatePeriodDuration(body.period_start, body.period_end)
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('POST /api/bookkeeping/journal-entries/[id]/correct', () => {
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('Lines are required')
|
||||
expect(body.error).toBe('Validation failed')
|
||||
})
|
||||
|
||||
it('returns 400 when lines array is empty', async () => {
|
||||
@@ -69,7 +69,7 @@ describe('POST /api/bookkeeping/journal-entries/[id]/correct', () => {
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('Lines are required')
|
||||
expect(body.error).toBe('Validation failed')
|
||||
})
|
||||
|
||||
it('returns reversal and corrected entries on success', async () => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { correctEntry } from '@/lib/core/bookkeeping/storno-service'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateJournalEntryLineInput } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CorrectJournalEntrySchema } from '@/lib/api/schemas'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -18,16 +19,9 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
let body: { lines: CreateJournalEntryLineInput[] }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!body.lines || !Array.isArray(body.lines) || body.lines.length === 0) {
|
||||
return NextResponse.json({ error: 'Lines are required' }, { status: 400 })
|
||||
}
|
||||
const validation = await validateBody(request, CorrectJournalEntrySchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
try {
|
||||
const result = await correctEntry(user.id, id, body.lines)
|
||||
|
||||
@@ -88,6 +88,8 @@ describe('GET /api/bookkeeping/journal-entries', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
describe('POST /api/bookkeeping/journal-entries', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
@@ -116,7 +118,7 @@ describe('POST /api/bookkeeping/journal-entries', () => {
|
||||
mockCreateJournalEntry.mockResolvedValue(entry)
|
||||
|
||||
const input = {
|
||||
fiscal_period_id: 'period-1',
|
||||
fiscal_period_id: VALID_UUID,
|
||||
entry_date: '2024-06-15',
|
||||
description: 'Test entry',
|
||||
source_type: 'manual',
|
||||
@@ -144,11 +146,14 @@ describe('POST /api/bookkeeping/journal-entries', () => {
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
fiscal_period_id: 'period-1',
|
||||
fiscal_period_id: VALID_UUID,
|
||||
entry_date: '2024-06-15',
|
||||
description: 'Bad entry',
|
||||
source_type: 'manual',
|
||||
lines: [{ account_number: '1930', debit_amount: 1000, credit_amount: 0 }],
|
||||
lines: [
|
||||
{ account_number: '1930', debit_amount: 1000, credit_amount: 0 },
|
||||
{ account_number: '3001', debit_amount: 0, credit_amount: 500 },
|
||||
],
|
||||
},
|
||||
})
|
||||
const response = await POST(request)
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createJournalEntry } from '@/lib/bookkeeping/engine'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateJournalEntryInput } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateJournalEntrySchema } from '@/lib/api/schemas'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -63,7 +64,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json() as CreateJournalEntryInput
|
||||
const validation = await validateBody(request, CreateJournalEntrySchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
try {
|
||||
const entry = await createJournalEntry(user.id, body)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { evaluateMappingRules } from '@/lib/bookkeeping/mapping-engine'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { EvaluateMappingRulesSchema } from '@/lib/api/schemas'
|
||||
import type { Transaction } from '@/types'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -11,12 +13,14 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validation = await validateBody(request, EvaluateMappingRulesSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Accept either a transaction ID or raw transaction data
|
||||
let transaction: Transaction
|
||||
|
||||
if (body.transaction_id) {
|
||||
if ('transaction_id' in body) {
|
||||
const { data, error } = await supabase
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
@@ -30,7 +34,7 @@ export async function POST(request: Request) {
|
||||
|
||||
transaction = data as Transaction
|
||||
} else {
|
||||
transaction = body as Transaction
|
||||
transaction = body as unknown as Transaction
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateMappingRuleSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
@@ -31,7 +33,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const result = await validateBody(request, CreateMappingRuleSchema)
|
||||
if (!result.success) return result.response
|
||||
const body = result.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('mapping_rules')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateCustomerInput } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { UpdateCustomerSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
@@ -62,7 +63,9 @@ export async function PATCH(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: Partial<CreateCustomerInput> = await request.json()
|
||||
const result = await validateBody(request, UpdateCustomerSchema)
|
||||
if (!result.success) return result.response
|
||||
const body = result.data
|
||||
|
||||
const updateData: Record<string, unknown> = {}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { eventBus } from '@/lib/events'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateCustomerInput, Customer } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateCustomerSchema } from '@/lib/api/schemas'
|
||||
import type { Customer } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -37,7 +39,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: CreateCustomerInput = await request.json()
|
||||
const result = await validateBody(request, CreateCustomerSchema)
|
||||
if (!result.success) return result.response
|
||||
const body = result.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('customers')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateDeadlineInput } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateDeadlineSchema } from '@/lib/api/schemas'
|
||||
|
||||
/**
|
||||
* GET /api/deadlines
|
||||
@@ -78,12 +79,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: CreateDeadlineInput = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.title || !body.due_date || !body.deadline_type) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
||||
}
|
||||
const validation = await validateBody(request, CreateDeadlineSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Insert the deadline
|
||||
const { data, error } = await supabase
|
||||
|
||||
@@ -111,6 +111,9 @@ describe('GET /api/invoices', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
const VALID_UUID_2 = '550e8400-e29b-41d4-a716-446655440001'
|
||||
|
||||
describe('POST /api/invoices (create invoice)', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
@@ -126,7 +129,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { customer_id: 'cust-1', items: [] },
|
||||
body: { customer_id: VALID_UUID, items: [] },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
@@ -141,7 +144,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
customer_id: 'cust-999',
|
||||
customer_id: VALID_UUID_2,
|
||||
invoice_date: '2024-06-15',
|
||||
due_date: '2024-07-15',
|
||||
currency: 'SEK',
|
||||
@@ -156,7 +159,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
})
|
||||
|
||||
it('creates invoice with items and emits event', async () => {
|
||||
const customer = makeCustomer({ id: 'cust-1' })
|
||||
const customer = makeCustomer({ id: VALID_UUID })
|
||||
const createdInvoice = makeInvoice({ id: 'inv-1' })
|
||||
|
||||
mockGetVatRules.mockReturnValue({
|
||||
@@ -189,7 +192,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
customer_id: 'cust-1',
|
||||
customer_id: VALID_UUID,
|
||||
invoice_date: '2024-06-15',
|
||||
due_date: '2024-07-15',
|
||||
currency: 'SEK',
|
||||
@@ -207,7 +210,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
})
|
||||
|
||||
it('rolls back invoice when items insertion fails', async () => {
|
||||
const customer = makeCustomer({ id: 'cust-1' })
|
||||
const customer = makeCustomer({ id: VALID_UUID })
|
||||
const createdInvoice = makeInvoice({ id: 'inv-1' })
|
||||
|
||||
mockGetVatRules.mockReturnValue({
|
||||
@@ -235,7 +238,7 @@ describe('POST /api/invoices (create invoice)', () => {
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
customer_id: 'cust-1',
|
||||
customer_id: VALID_UUID,
|
||||
invoice_date: '2024-06-15',
|
||||
due_date: '2024-07-15',
|
||||
currency: 'SEK',
|
||||
@@ -265,7 +268,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { credited_invoice_id: 'inv-999' },
|
||||
body: { credited_invoice_id: VALID_UUID_2 },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -275,12 +278,12 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
})
|
||||
|
||||
it('returns 400 when invoice is already credited', async () => {
|
||||
const original = makeInvoice({ id: 'inv-1', status: 'credited' })
|
||||
const original = makeInvoice({ id: VALID_UUID, status: 'credited' })
|
||||
enqueue({ data: original, error: null })
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { credited_invoice_id: 'inv-1' },
|
||||
body: { credited_invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -290,12 +293,12 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
})
|
||||
|
||||
it('returns 400 when invoice is in draft status', async () => {
|
||||
const original = makeInvoice({ id: 'inv-1', status: 'draft' })
|
||||
const original = makeInvoice({ id: VALID_UUID, status: 'draft' })
|
||||
enqueue({ data: original, error: null })
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { credited_invoice_id: 'inv-1' },
|
||||
body: { credited_invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -319,7 +322,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
},
|
||||
]
|
||||
const original = makeInvoice({
|
||||
id: 'inv-1',
|
||||
id: VALID_UUID,
|
||||
status: 'sent',
|
||||
subtotal: 10000,
|
||||
vat_amount: 2500,
|
||||
@@ -328,7 +331,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
})
|
||||
const creditNote = makeInvoice({
|
||||
id: 'cn-1',
|
||||
credited_invoice_id: 'inv-1',
|
||||
credited_invoice_id: VALID_UUID,
|
||||
subtotal: -10000,
|
||||
vat_amount: -2500,
|
||||
total: -12500,
|
||||
@@ -356,7 +359,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { credited_invoice_id: 'inv-1' },
|
||||
body: { credited_invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ data: unknown }>(response)
|
||||
@@ -370,7 +373,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
|
||||
it('rolls back credit note when items insertion fails', async () => {
|
||||
const original = makeInvoice({
|
||||
id: 'inv-1',
|
||||
id: VALID_UUID,
|
||||
status: 'sent',
|
||||
items: [
|
||||
{
|
||||
@@ -397,7 +400,7 @@ describe('POST /api/invoices (create credit note)', () => {
|
||||
|
||||
const request = createMockRequest('/api/invoices', {
|
||||
method: 'POST',
|
||||
body: { credited_invoice_id: 'inv-1' },
|
||||
body: { credited_invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
+41
-15
@@ -2,7 +2,8 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { eventBus } from '@/lib/events'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateInvoiceInput, EntityType, AccountingMethod, Invoice, CreditNote, InvoiceDocumentType } from '@/types'
|
||||
import { CreateInvoiceSchema, CreateCreditNoteSchema } from '@/lib/api/schemas'
|
||||
import type { EntityType, AccountingMethod, Invoice, CreditNote, InvoiceDocumentType } from '@/types'
|
||||
import { getVatRules, calculateVat, calculateTotal, getAvailableVatRates, getVatTreatmentForRate } from '@/lib/invoices/vat-rules'
|
||||
import { fetchExchangeRate, convertToSEK } from '@/lib/currency/riksbanken'
|
||||
import {
|
||||
@@ -11,11 +12,6 @@ import {
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
interface CreateCreditNoteInput {
|
||||
credited_invoice_id: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const supabase = await createClient()
|
||||
|
||||
@@ -59,15 +55,45 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Check if this is a credit note creation request
|
||||
if (body.credited_invoice_id) {
|
||||
return createCreditNote(supabase, user.id, body as CreateCreditNoteInput)
|
||||
let rawBody: unknown
|
||||
try {
|
||||
rawBody = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid JSON in request body', type: 'validation_error' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const invoiceInput = body as CreateInvoiceInput
|
||||
const documentType: InvoiceDocumentType = body.document_type || 'invoice'
|
||||
// Check if this is a credit note creation request
|
||||
if (typeof rawBody === 'object' && rawBody !== null && 'credited_invoice_id' in rawBody) {
|
||||
const parsed = CreateCreditNoteSchema.safeParse(rawBody)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
errors: parsed.error.issues.map((i) => ({ field: i.path.join('.'), message: i.message, code: i.code })),
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
return createCreditNote(supabase, user.id, parsed.data)
|
||||
}
|
||||
|
||||
const parsed = CreateInvoiceSchema.safeParse(rawBody)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
errors: parsed.error.issues.map((i) => ({ field: i.path.join('.'), message: i.message, code: i.code })),
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
const invoiceInput = parsed.data
|
||||
const documentType: InvoiceDocumentType = invoiceInput.document_type || 'invoice'
|
||||
|
||||
// Get customer for VAT calculation
|
||||
const { data: customer, error: customerError } = await supabase
|
||||
@@ -225,7 +251,7 @@ export async function POST(request: Request) {
|
||||
async function createCreditNote(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
input: CreateCreditNoteInput
|
||||
input: { credited_invoice_id: string; reason?: string }
|
||||
) {
|
||||
// Fetch the original invoice with items
|
||||
const { data: originalInvoice, error: originalError } = await supabase
|
||||
@@ -310,7 +336,7 @@ async function createCreditNote(
|
||||
unit: item.unit,
|
||||
unit_price: item.unit_price,
|
||||
line_total: -Math.abs(item.line_total),
|
||||
vat_rate: item.vat_rate ?? 25,
|
||||
vat_rate: item.vat_rate ?? 0,
|
||||
vat_amount: -(item.vat_amount ? Math.abs(item.vat_amount) : 0),
|
||||
}))
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import { manualLink } from '@/lib/reconciliation/bank-reconciliation'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { BankLinkSchema } from '@/lib/api/schemas'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -13,15 +15,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { transaction_id, journal_entry_id } = body
|
||||
|
||||
if (!transaction_id || !journal_entry_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'transaction_id and journal_entry_id are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const validation = await validateBody(request, BankLinkSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { transaction_id, journal_entry_id } = validation.data
|
||||
|
||||
const result = await manualLink(supabase, user.id, transaction_id, journal_entry_id)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import { runReconciliation } from '@/lib/reconciliation/bank-reconciliation'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { RunReconciliationSchema } from '@/lib/api/schemas'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -13,8 +15,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { date_from, date_to, dry_run } = body
|
||||
const validation = await validateBody(request, RunReconciliationSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { date_from, date_to, dry_run } = validation.data
|
||||
|
||||
const result = await runReconciliation(supabase, user.id, {
|
||||
dateFrom: date_from,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { unlinkReconciliation } from '@/lib/reconciliation/bank-reconciliation'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { BankUnlinkSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createClient()
|
||||
@@ -10,15 +12,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { transaction_id } = body
|
||||
|
||||
if (!transaction_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'transaction_id is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const validation = await validateBody(request, BankUnlinkSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { transaction_id } = validation.data
|
||||
|
||||
const result = await unlinkReconciliation(supabase, user.id, transaction_id)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { didTaxFieldsChange, regenerateTaxDeadlinesForUser } from '@/lib/tax/deadline-generator'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { UpdateSettingsSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
@@ -40,7 +42,9 @@ export async function PUT(request: Request) {
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
const body = await request.json()
|
||||
const validation = await validateBody(request, UpdateSettingsSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Validate: enskild firma must use calendar year (BFL 3 kap.)
|
||||
const effectiveEntityType = body.entity_type || oldSettings?.entity_type
|
||||
|
||||
@@ -4,15 +4,10 @@ import {
|
||||
createSupplierInvoicePaymentEntry,
|
||||
createSupplierInvoiceCashEntry,
|
||||
} from '@/lib/bookkeeping/supplier-invoice-entries'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { MarkSupplierInvoicePaidSchema } from '@/lib/api/schemas'
|
||||
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
|
||||
|
||||
interface MarkPaidRequest {
|
||||
amount?: number
|
||||
payment_date?: string
|
||||
exchange_rate_difference?: number
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -26,7 +21,9 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: MarkPaidRequest = await request.json()
|
||||
const validation = await validateBody(request, MarkSupplierInvoicePaidSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Fetch invoice with supplier and items
|
||||
const { data: invoice, error: fetchError } = await supabase
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { UpdateSupplierInvoiceSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
@@ -60,18 +62,13 @@ export async function PUT(
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validation = await validateBody(request, UpdateSupplierInvoiceSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('supplier_invoices')
|
||||
.update({
|
||||
supplier_invoice_number: body.supplier_invoice_number,
|
||||
invoice_date: body.invoice_date,
|
||||
due_date: body.due_date,
|
||||
delivery_date: body.delivery_date,
|
||||
payment_reference: body.payment_reference,
|
||||
notes: body.notes,
|
||||
})
|
||||
.update(body)
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
.select()
|
||||
|
||||
@@ -98,6 +98,9 @@ describe('GET /api/supplier-invoices', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
const VALID_UUID_2 = '550e8400-e29b-41d4-a716-446655440001'
|
||||
|
||||
describe('POST /api/supplier-invoices', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
@@ -112,7 +115,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
|
||||
const request = createMockRequest('/api/supplier-invoices', {
|
||||
method: 'POST',
|
||||
body: { supplier_id: 'sup-1', items: [] },
|
||||
body: { supplier_id: VALID_UUID, items: [] },
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
@@ -127,7 +130,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
const request = createMockRequest('/api/supplier-invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
supplier_id: 'sup-999',
|
||||
supplier_id: VALID_UUID_2,
|
||||
supplier_invoice_number: 'LF-001',
|
||||
invoice_date: '2024-06-01',
|
||||
due_date: '2024-07-01',
|
||||
@@ -142,7 +145,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
})
|
||||
|
||||
it('creates supplier invoice with items and arrival number', async () => {
|
||||
const supplier = makeSupplier({ id: 'sup-1' })
|
||||
const supplier = makeSupplier({ id: VALID_UUID })
|
||||
const createdInvoice = makeSupplierInvoice({ id: 'si-1' })
|
||||
|
||||
// Fetch supplier
|
||||
@@ -163,7 +166,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
const request = createMockRequest('/api/supplier-invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
supplier_id: 'sup-1',
|
||||
supplier_id: VALID_UUID,
|
||||
supplier_invoice_number: 'LF-001',
|
||||
invoice_date: '2024-06-01',
|
||||
due_date: '2024-07-01',
|
||||
@@ -190,7 +193,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
})
|
||||
|
||||
it('skips registration entry for cash method', async () => {
|
||||
const supplier = makeSupplier({ id: 'sup-1' })
|
||||
const supplier = makeSupplier({ id: VALID_UUID })
|
||||
const createdInvoice = makeSupplierInvoice({ id: 'si-1' })
|
||||
|
||||
enqueue({ data: supplier, error: null })
|
||||
@@ -202,7 +205,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
const request = createMockRequest('/api/supplier-invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
supplier_id: 'sup-1',
|
||||
supplier_id: VALID_UUID,
|
||||
supplier_invoice_number: 'LF-002',
|
||||
invoice_date: '2024-06-01',
|
||||
due_date: '2024-07-01',
|
||||
@@ -220,7 +223,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
})
|
||||
|
||||
it('rolls back on items insertion failure', async () => {
|
||||
const supplier = makeSupplier({ id: 'sup-1' })
|
||||
const supplier = makeSupplier({ id: VALID_UUID })
|
||||
const createdInvoice = makeSupplierInvoice({ id: 'si-1' })
|
||||
|
||||
enqueue({ data: supplier, error: null })
|
||||
@@ -234,7 +237,7 @@ describe('POST /api/supplier-invoices', () => {
|
||||
const request = createMockRequest('/api/supplier-invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
supplier_id: 'sup-1',
|
||||
supplier_id: VALID_UUID,
|
||||
supplier_invoice_number: 'LF-003',
|
||||
invoice_date: '2024-06-01',
|
||||
due_date: '2024-07-01',
|
||||
|
||||
@@ -2,7 +2,9 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createSupplierInvoiceRegistrationEntry } from '@/lib/bookkeeping/supplier-invoice-entries'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateSupplierInvoiceInput, SupplierInvoice, SupplierInvoiceItem } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateSupplierInvoiceSchema } from '@/lib/api/schemas'
|
||||
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -49,7 +51,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: CreateSupplierInvoiceInput = await request.json()
|
||||
const validation = await validateBody(request, CreateSupplierInvoiceSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
|
||||
// Validate supplier exists and belongs to user
|
||||
const { data: supplier, error: supplierError } = await supabase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateSupplierInput } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { UpdateSupplierSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
@@ -66,7 +67,9 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: Partial<CreateSupplierInput> = await request.json()
|
||||
const result = await validateBody(request, UpdateSupplierSchema)
|
||||
if (!result.success) return result.response
|
||||
const body = result.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('suppliers')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateSupplierInput, Supplier } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CreateSupplierSchema } from '@/lib/api/schemas'
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
@@ -33,7 +34,9 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: CreateSupplierInput = await request.json()
|
||||
const result = await validateBody(request, CreateSupplierSchema)
|
||||
if (!result.success) return result.response
|
||||
const body = result.data
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('suppliers')
|
||||
|
||||
@@ -25,10 +25,12 @@ vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
|
||||
import { POST } from '../route'
|
||||
|
||||
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
describe('POST /api/transactions/[id]/book', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
const validBody = {
|
||||
fiscal_period_id: 'period-1',
|
||||
fiscal_period_id: VALID_UUID,
|
||||
entry_date: '2025-01-15',
|
||||
description: 'Test booking',
|
||||
lines: [
|
||||
@@ -61,13 +63,13 @@ describe('POST /api/transactions/[id]/book', () => {
|
||||
it('returns 400 when missing required fields', async () => {
|
||||
const request = createMockRequest('/api/transactions/tx-1/book', {
|
||||
method: 'POST',
|
||||
body: { fiscal_period_id: 'period-1' },
|
||||
body: { fiscal_period_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toContain('Missing required fields')
|
||||
expect(body.error).toBe('Validation failed')
|
||||
})
|
||||
|
||||
it('returns 404 when transaction not found', async () => {
|
||||
@@ -154,7 +156,7 @@ describe('POST /api/transactions/[id]/book', () => {
|
||||
expect(body.data.id).toBe('je-new')
|
||||
|
||||
expect(mockCreateJournalEntry).toHaveBeenCalledWith('user-1', {
|
||||
fiscal_period_id: 'period-1',
|
||||
fiscal_period_id: VALID_UUID,
|
||||
entry_date: '2025-01-15',
|
||||
description: 'Test booking',
|
||||
source_type: 'bank_transaction',
|
||||
|
||||
@@ -3,17 +3,12 @@ import { NextResponse } from 'next/server'
|
||||
import { eventBus } from '@/lib/events'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import { createJournalEntry } from '@/lib/bookkeeping/engine'
|
||||
import type { CreateJournalEntryLineInput, Transaction } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { BookTransactionSchema } from '@/lib/api/schemas'
|
||||
import type { Transaction } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
interface BookRequest {
|
||||
fiscal_period_id: string
|
||||
entry_date: string
|
||||
description: string
|
||||
lines: CreateJournalEntryLineInput[]
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -27,15 +22,9 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: BookRequest = await request.json()
|
||||
const { fiscal_period_id, entry_date, description, lines } = body
|
||||
|
||||
if (!fiscal_period_id || !entry_date || !description || !lines?.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: fiscal_period_id, entry_date, description, lines' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const validation = await validateBody(request, BookTransactionSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { fiscal_period_id, entry_date, description, lines } = validation.data
|
||||
|
||||
// Fetch transaction (validates ownership)
|
||||
const { data: transaction, error: fetchError } = await supabase
|
||||
|
||||
@@ -5,17 +5,12 @@ import { ensureInitialized } from '@/lib/init'
|
||||
import { buildMappingResultFromCategory } from '@/lib/bookkeeping/category-mapping'
|
||||
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
|
||||
import { saveUserMappingRule } from '@/lib/bookkeeping/mapping-engine'
|
||||
import type { Transaction, TransactionCategory, EntityType, VatTreatment } from '@/types'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { CategorizeTransactionSchema } from '@/lib/api/schemas'
|
||||
import type { Transaction, TransactionCategory, EntityType } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
interface CategorizeRequest {
|
||||
is_business: boolean
|
||||
category?: TransactionCategory
|
||||
vat_treatment?: VatTreatment
|
||||
account_override?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a fiscal period exists for the given date, create one if needed
|
||||
*/
|
||||
@@ -98,8 +93,10 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body: CategorizeRequest = await request.json()
|
||||
// Parse and validate request body
|
||||
const validation = await validateBody(request, CategorizeTransactionSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const body = validation.data
|
||||
const { is_business, category } = body
|
||||
|
||||
// Fetch the transaction (validates ownership)
|
||||
|
||||
@@ -25,6 +25,9 @@ vi.mock('@/lib/bookkeeping/invoice-entries', () => ({
|
||||
|
||||
import { POST } from '../route'
|
||||
|
||||
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
|
||||
const VALID_UUID_2 = '550e8400-e29b-41d4-a716-446655440001'
|
||||
|
||||
describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
@@ -39,7 +42,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
@@ -57,7 +60,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('invoice_id is required')
|
||||
expect(body.error).toBe('Validation failed')
|
||||
})
|
||||
|
||||
it('returns 404 when transaction not found', async () => {
|
||||
@@ -65,7 +68,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-999/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-999' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -80,7 +83,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -95,7 +98,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -111,7 +114,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-999' },
|
||||
body: { invoice_id: VALID_UUID_2 },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -122,13 +125,13 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
it('returns 400 when invoice is not in unpaid state', async () => {
|
||||
const tx = makeTransaction({ id: 'tx-1', amount: 12500, invoice_id: null })
|
||||
const invoice = makeInvoice({ id: 'inv-1', status: 'paid' })
|
||||
const invoice = makeInvoice({ id: VALID_UUID, status: 'paid' })
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: invoice, error: null })
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
@@ -141,7 +144,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
const tx = makeTransaction({ id: 'tx-1', amount: 12500, invoice_id: null, date: '2024-06-15' })
|
||||
const customer = makeCustomer()
|
||||
const invoice = makeInvoice({
|
||||
id: 'inv-1',
|
||||
id: VALID_UUID,
|
||||
status: 'sent',
|
||||
total: 12500,
|
||||
subtotal: 10000,
|
||||
@@ -166,7 +169,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{
|
||||
@@ -185,14 +188,14 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
// Verify accrual payment entry was called
|
||||
expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
expect.objectContaining({ id: 'inv-1' }),
|
||||
expect.objectContaining({ id: VALID_UUID }),
|
||||
'2024-06-15'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns success with journal_entry_error when journal entry fails (non-blocking)', async () => {
|
||||
const tx = makeTransaction({ id: 'tx-1', amount: 12500, invoice_id: null, date: '2024-06-15' })
|
||||
const invoice = makeInvoice({ id: 'inv-1', status: 'sent', total: 12500 })
|
||||
const invoice = makeInvoice({ id: VALID_UUID, status: 'sent', total: 12500 })
|
||||
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: invoice, error: null })
|
||||
@@ -207,7 +210,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
|
||||
const request = createMockRequest('/api/transactions/tx-1/match-invoice', {
|
||||
method: 'POST',
|
||||
body: { invoice_id: 'inv-1' },
|
||||
body: { invoice_id: VALID_UUID },
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
createInvoicePaymentJournalEntry,
|
||||
createInvoiceCashEntry,
|
||||
} from '@/lib/bookkeeping/invoice-entries'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { MatchInvoiceSchema } from '@/lib/api/schemas'
|
||||
import type { EntityType, Invoice } from '@/types'
|
||||
|
||||
interface MatchInvoiceRequest {
|
||||
invoice_id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/transactions/[id]/match-invoice
|
||||
*
|
||||
@@ -34,13 +32,10 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body: MatchInvoiceRequest = await request.json()
|
||||
const { invoice_id } = body
|
||||
|
||||
if (!invoice_id) {
|
||||
return NextResponse.json({ error: 'invoice_id is required' }, { status: 400 })
|
||||
}
|
||||
// Parse and validate request body
|
||||
const validation = await validateBody(request, MatchInvoiceSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { invoice_id } = validation.data
|
||||
|
||||
// Fetch the transaction (validates ownership)
|
||||
const { data: transaction, error: fetchTxError } = await supabase
|
||||
|
||||
@@ -5,12 +5,10 @@ import {
|
||||
createSupplierInvoicePaymentEntry,
|
||||
createSupplierInvoiceCashEntry,
|
||||
} from '@/lib/bookkeeping/supplier-invoice-entries'
|
||||
import { validateBody } from '@/lib/api/validate'
|
||||
import { MatchSupplierInvoiceSchema } from '@/lib/api/schemas'
|
||||
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
|
||||
|
||||
interface MatchRequest {
|
||||
supplier_invoice_id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a fiscal period exists for the given date
|
||||
*/
|
||||
@@ -64,12 +62,9 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: MatchRequest = await request.json()
|
||||
const { supplier_invoice_id } = body
|
||||
|
||||
if (!supplier_invoice_id) {
|
||||
return NextResponse.json({ error: 'supplier_invoice_id is required' }, { status: 400 })
|
||||
}
|
||||
const validation = await validateBody(request, MatchSupplierInvoiceSchema)
|
||||
if (!validation.success) return validation.response
|
||||
const { supplier_invoice_id } = validation.data
|
||||
|
||||
// Fetch the transaction
|
||||
const { data: transaction, error: fetchTxError } = await supabase
|
||||
|
||||
Reference in New Issue
Block a user