Files
accounted/app/api/customers/route.ts
T
Mattsson 072aedeaf9 Fix/supp ag fb (#1023)
* fix: prevent credit notes from entering payment flow

* fix: persist and display customer personal numbers

* feat: configure automatic invoice reminder days

* fix: issue credit notes through send flow

* chore: add repository agent guidance

* feat(mcp): route tools across user companies

* fix(articles): delete unused register entries

* feat(invoices): improve issued invoice actions

* feat(supplier-invoices): retain uploaded source documents

* docs: record implementation decisions

* feat: enhance customer personal number handling and validation

- Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers.
- Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema.
- Implemented masking and encryption for personal numbers to enhance data protection.
- Introduced new utility functions for masking and encrypting personal numbers.
- Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries.
- Enhanced error handling and logging for credit note issuance and invoice processing.
- Updated tests to cover new credit note creation guards and personal number handling.

* test: enhance list companies test with supabase query mocks
2026-07-15 15:53:15 +02:00

119 lines
3.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { CreateCustomerSchema } from '@/lib/api/schemas'
import { validateVatNumber } from '@/lib/vat/vies-client'
import { withRouteContext } from '@/lib/api/with-route-context'
import { errorResponse, errorResponseFromCode } from '@/lib/errors/get-structured-error'
import type { Customer } from '@/types'
import { encryptCustomerPersonalNumber, maskCustomerRow } from '@/lib/customers/protect-personal-number'
ensureInitialized()
export const GET = withRouteContext(
'customer.list',
async (_request, ctx) => {
const { supabase, companyId, log, requestId } = ctx
const { data, error } = await supabase
.from('customers')
.select('*')
.eq('company_id', companyId)
.order('name', { ascending: true })
if (error) {
log.error('customer list failed', error)
return errorResponse(error, log, { requestId })
}
return NextResponse.json({ data: (data ?? []).map(maskCustomerRow) })
},
)
export const POST = withRouteContext(
'customer.create',
async (request, ctx) => {
const { user, supabase, companyId, log, requestId } = ctx
const result = await validateBody(request, CreateCustomerSchema, {
log,
operation: 'customer.create',
})
if (!result.success) return result.response
const body = result.data
const { data, error } = await supabase
.from('customers')
.insert({
user_id: user.id,
company_id: companyId,
name: body.name,
customer_type: body.customer_type,
customer_number: body.customer_number || null,
email: body.email,
phone: body.phone,
address_line1: body.address_line1,
address_line2: body.address_line2,
postal_code: body.postal_code,
city: body.city,
country: body.country || 'Sweden',
org_number: body.org_number,
vat_number: body.vat_number,
personal_number: encryptCustomerPersonalNumber(body.personal_number),
language: body.language || 'sv',
default_payment_terms: body.default_payment_terms || 30,
notes: body.notes,
})
.select()
.single()
if (error) {
if (error.code === '23505') {
return errorResponseFromCode('CUSTOMER_DUPLICATE_ORG_NUMBER', log, {
requestId,
details: { orgNumber: body.org_number },
})
}
log.error('customer insert failed', error)
return errorResponseFromCode('CUSTOMER_CREATE_FAILED', log, {
requestId,
details: { reason: error.message },
})
}
// Auto-validate VAT number for EU business customers (non-blocking).
if (body.customer_type === 'eu_business' && body.vat_number) {
try {
const vatResult = await validateVatNumber(body.vat_number)
if (vatResult.valid) {
await supabase
.from('customers')
.update({
vat_number_validated: true,
vat_number_validated_at: new Date().toISOString(),
})
.eq('id', data.id)
.eq('company_id', companyId)
data.vat_number_validated = true
data.vat_number_validated_at = new Date().toISOString()
}
} catch (err) {
log.warn('auto-VIES validation failed on customer create', err as Error, {
customerId: data.id,
})
}
}
const safeCustomer = maskCustomerRow(data)
await eventBus.emit({
type: 'customer.created',
payload: { customer: safeCustomer as Customer, companyId: companyId!, userId: user.id },
})
return NextResponse.json({ data: safeCustomer })
},
{ requireWrite: true },
)