* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { z } from 'zod'
|
|
import { UpdateSettingsSchema } from '@/lib/api/schemas'
|
|
import {
|
|
validateBankgiroNumber,
|
|
validatePlusgiroNumber,
|
|
} from '@/lib/bankgiro/luhn'
|
|
import { INVOICE_EMAIL_PLACEHOLDER_KEYS } from '@/lib/email/invoice-templates'
|
|
|
|
// Placeholders in the company-editable invoice email texts are a FIXED set.
|
|
// applyPlaceholders() (lib/email/user-text.ts) leaves an unrecognised key
|
|
// untouched by design, so an invented "{faktura_nr}" would reach the customer
|
|
// with the braces intact. Agents invent placeholder names freely, so reject
|
|
// them at the staging boundary rather than in the outgoing mail.
|
|
const ALLOWED_PLACEHOLDERS: ReadonlySet<string> = new Set(INVOICE_EMAIL_PLACEHOLDER_KEYS)
|
|
const ALLOWED_PLACEHOLDER_LIST = INVOICE_EMAIL_PLACEHOLDER_KEYS.map((key) => `{${key}}`).join(' ')
|
|
|
|
// Same token pattern applyPlaceholders() substitutes on, and the same
|
|
// trim + lower-case key normalisation, so validation and rendering agree.
|
|
function findUnknownPlaceholders(text: string): string[] {
|
|
const tokens = text.match(/\{[^{}]*\}/g) ?? []
|
|
return tokens.filter(
|
|
(token) => !ALLOWED_PLACEHOLDERS.has(token.slice(1, -1).trim().toLowerCase()),
|
|
)
|
|
}
|
|
|
|
const INVOICE_EMAIL_TEXT_FIELDS = ['subject', 'greeting', 'body', 'signoff'] as const
|
|
const INVOICE_EMAIL_TEXT_LANGS = ['sv', 'en'] as const
|
|
|
|
const CompanySettingsChangesSchema = z
|
|
.object({
|
|
bank_name: UpdateSettingsSchema.shape.bank_name,
|
|
clearing_number: UpdateSettingsSchema.shape.clearing_number,
|
|
account_number: UpdateSettingsSchema.shape.account_number,
|
|
bankgiro: UpdateSettingsSchema.shape.bankgiro,
|
|
plusgiro: UpdateSettingsSchema.shape.plusgiro,
|
|
swish: UpdateSettingsSchema.shape.swish,
|
|
iban: UpdateSettingsSchema.shape.iban,
|
|
bic: UpdateSettingsSchema.shape.bic,
|
|
default_our_reference: UpdateSettingsSchema.shape.default_our_reference,
|
|
email: UpdateSettingsSchema.shape.email,
|
|
phone: UpdateSettingsSchema.shape.phone,
|
|
website: UpdateSettingsSchema.shape.website,
|
|
invoice_email_texts: UpdateSettingsSchema.shape.invoice_email_texts,
|
|
})
|
|
.strict()
|
|
.superRefine((changes, ctx) => {
|
|
if (Object.keys(changes).length === 0) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: 'At least one company setting must be supplied',
|
|
})
|
|
}
|
|
|
|
if (changes.bankgiro && !validateBankgiroNumber(changes.bankgiro)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['bankgiro'],
|
|
message: 'Invalid Bankgiro number',
|
|
})
|
|
}
|
|
|
|
if (changes.plusgiro && !validatePlusgiroNumber(changes.plusgiro)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['plusgiro'],
|
|
message: 'Invalid Plusgiro number',
|
|
})
|
|
}
|
|
|
|
const texts = changes.invoice_email_texts
|
|
if (texts) {
|
|
for (const lang of INVOICE_EMAIL_TEXT_LANGS) {
|
|
const langTexts = texts[lang]
|
|
if (!langTexts) continue
|
|
for (const field of INVOICE_EMAIL_TEXT_FIELDS) {
|
|
const value = langTexts[field]
|
|
if (typeof value !== 'string') continue
|
|
const unknown = findUnknownPlaceholders(value)
|
|
if (unknown.length > 0) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['invoice_email_texts', lang, field],
|
|
message: `Unknown placeholder ${unknown.join(', ')}. Allowed placeholders: ${ALLOWED_PLACEHOLDER_LIST}`,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
export const UpdateCompanySettingsParamsSchema = z
|
|
.object({
|
|
changes: CompanySettingsChangesSchema,
|
|
})
|
|
.strict()
|
|
|