Bug/gh issues fiz (#1103)

* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
This commit is contained in:
Mattsson
2026-07-21 23:00:15 +02:00
committed by GitHub
parent 702512437a
commit e11f70b347
467 changed files with 17569 additions and 2221 deletions
+156
View File
@@ -81,6 +81,8 @@ import { CreateDimensionValueParamsSchema } from '@/lib/pending-operations/schem
import { RetagLineDimensionsParamsSchema } from '@/lib/pending-operations/schemas/retag-line-dimensions'
import { CreateAccountParamsSchema, UpdateAccountParamsSchema } from '@/lib/pending-operations/schemas/account'
import { SetVoucherNoteParamsSchema } from '@/lib/pending-operations/schemas/voucher-note'
import { UpdateCompanySettingsParamsSchema } from '@/lib/pending-operations/schemas/company-settings'
import { UpdateCustomerParamsSchema } from '@/lib/pending-operations/schemas/customer'
import { BulkBookInboxSchema } from '@/lib/api/schemas'
import { ensureArticleNumber } from '@/lib/articles/ensure-article-number'
import { isValidRevenueAccount } from '@/lib/articles/validate-revenue-account'
@@ -279,6 +281,154 @@ async function commitCreateCustomer(
return { data: { customer_id: data.id } }
}
async function commitUpdateCustomer(
supabase: SupabaseClient,
companyId: string,
params: Record<string, unknown>,
): Promise<ExecutorResult> {
let validated
try {
validated = UpdateCustomerParamsSchema.parse(params)
} catch (err) {
if (err instanceof z.ZodError) {
const issue = err.issues[0]
return {
error: `Invalid ${issue?.path?.join('.') ?? 'params'}: ${issue?.message ?? 'validation failed'}`,
status: 400,
}
}
throw err
}
const { customer_id: customerId, changes } = validated
const { data: current, error: currentError } = await supabase
.from('customers')
.select('customer_type')
.eq('id', customerId)
.eq('company_id', companyId)
.maybeSingle()
if (currentError) return { error: currentError.message, status: 500 }
if (!current) return { error: 'Customer not found', status: 404 }
const updateData: Record<string, unknown> = { ...changes }
if (changes.customer_number !== undefined) {
updateData.customer_number = changes.customer_number || null
}
const effectiveType = changes.customer_type ?? current.customer_type
if (changes.customer_type !== undefined && effectiveType !== 'individual') {
updateData.personal_number = null
}
if (changes.vat_number !== undefined) {
if (effectiveType === 'eu_business') {
if (changes.vat_number) {
try {
const vatResult = await validateVatNumber(changes.vat_number)
updateData.vat_number_validated = vatResult.valid
updateData.vat_number_validated_at = vatResult.valid
? new Date().toISOString()
: null
} catch (err) {
log.warn('Auto-VIES validation failed on staged customer update:', err)
updateData.vat_number_validated = false
updateData.vat_number_validated_at = null
}
} else {
updateData.vat_number_validated = false
updateData.vat_number_validated_at = null
}
}
}
const { data, error } = await supabase
.from('customers')
.update(updateData)
.eq('id', customerId)
.eq('company_id', companyId)
.select('id, name, customer_type, customer_number, email, phone, address_line1, address_line2, postal_code, city, country, org_number, vat_number, vat_number_validated, language, default_payment_terms, notes')
.maybeSingle()
if (error) {
if (error.code === '23505') {
return { error: 'A customer with this organization number already exists', status: 409 }
}
return { error: error.message, status: 500 }
}
if (!data) return { error: 'Customer not found', status: 404 }
return {
data: {
customer_id: data.id,
name: data.name,
customer_type: data.customer_type,
customer_number: data.customer_number ?? null,
email: data.email ?? null,
phone: data.phone ?? null,
address_line1: data.address_line1 ?? null,
address_line2: data.address_line2 ?? null,
postal_code: data.postal_code ?? null,
city: data.city ?? null,
country: data.country,
org_number: data.org_number ?? null,
vat_number: data.vat_number ?? null,
vat_number_validated: data.vat_number_validated ?? false,
language: data.language ?? 'sv',
default_payment_terms: data.default_payment_terms,
notes: data.notes ?? null,
},
}
}
async function commitUpdateCompanySettings(
supabase: SupabaseClient,
companyId: string,
params: Record<string, unknown>,
): Promise<ExecutorResult> {
let validated
try {
validated = UpdateCompanySettingsParamsSchema.parse(params)
} catch (err) {
if (err instanceof z.ZodError) {
const issue = err.issues[0]
return {
error: `Invalid ${issue?.path?.join('.') ?? 'params'}: ${issue?.message ?? 'validation failed'}`,
status: 400,
}
}
throw err
}
const { data, error } = await supabase
.from('company_settings')
.update(validated.changes)
.eq('company_id', companyId)
.select('bank_name, clearing_number, account_number, bankgiro, plusgiro, swish, iban, bic, default_our_reference')
.single()
if (error) {
if (error.code === 'PGRST116') {
return { error: 'Company settings not found', status: 404 }
}
return { error: error.message, status: 500 }
}
return {
data: {
company_id: companyId,
bank_name: data.bank_name ?? null,
clearing_number: data.clearing_number ?? null,
account_number: data.account_number ?? null,
bankgiro: data.bankgiro ?? null,
plusgiro: data.plusgiro ?? null,
swish: data.swish ?? null,
iban: data.iban ?? null,
bic: data.bic ?? null,
contact_person: data.default_our_reference ?? null,
},
}
}
async function commitCreateArticle(
supabase: SupabaseClient,
userId: string,
@@ -4292,6 +4442,12 @@ async function commitPendingOperationInner(
case 'create_customer':
result = await commitCreateCustomer(supabase, userId, companyId, pendingOp.params)
break
case 'update_customer':
result = await commitUpdateCustomer(supabase, companyId, pendingOp.params)
break
case 'update_company_settings':
result = await commitUpdateCompanySettings(supabase, companyId, pendingOp.params)
break
case 'create_article':
result = await commitCreateArticle(supabase, userId, companyId, pendingOp.params)
break