Files
accounted/lib/pending-operations/schemas/company-settings.ts
T
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* 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>
2026-09-02 11:51:16 +02:00

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()