Bug/accounting suggestion (#456)

* feat: add bike benefit handling and optional vacation accrual

- Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value.
- Updated schemas to include new benefit types and validation rules.
- Implemented API routes for creating, updating, and deleting employee benefits.
- Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual.
- Added UI components for managing employee benefits, including input for bike benefit specifics.
- Created database migrations for employee benefits and updated salary line items to support new benefit types.

* chore: remove Langfuse env var checks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: enhance OAuth callback URL handling and update default scopes for Visma integration

* feat: remove trade_name field and simplify company naming in invoices

* refactor: destructure canWrite from useCanWrite for consistency across components

* feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates

* feat: add missing label for bike benefit in salary line item types

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-13 00:26:04 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent cd96e5ec26
commit 3fa871c742
53 changed files with 1182 additions and 125 deletions
+44 -3
View File
@@ -359,7 +359,6 @@ export const MatchSupplierInvoiceSchema = z.object({
export const UpdateSettingsSchema = z.object({
entity_type: EntityTypeSchema.optional(),
company_name: z.string().optional(),
trade_name: z.string().nullable().optional(),
org_number: z.string().optional(),
address_line1: z.string().optional(),
address_line2: z.string().optional(),
@@ -399,6 +398,7 @@ export const UpdateSettingsSchema = z.object({
invoice_show_ocr: z.boolean().optional(),
invoice_show_bankgiro: z.boolean().optional(),
invoice_show_plusgiro: z.boolean().optional(),
invoice_show_logo: z.boolean().optional(),
invoice_late_fee_text: z.string().nullable().optional(),
invoice_credit_terms_text: z.string().nullable().optional(),
// AI agent flow
@@ -677,13 +677,13 @@ export const SupplierImportExecuteSchema = z.object({
export const EmploymentTypeSchema = z.enum(['employee', 'company_owner', 'board_member'])
export const SalaryTypeSchema = z.enum(['monthly', 'hourly'])
export const FSkattStatusSchema = z.enum(['a_skatt', 'f_skatt', 'fa_skatt', 'not_verified'])
export const VacationRuleSchema = z.enum(['procentregeln', 'sammaloneregeln'])
export const VacationRuleSchema = z.enum(['procentregeln', 'sammaloneregeln', 'none'])
export const SalaryRunStatusSchema = z.enum(['draft', 'review', 'approved', 'paid', 'booked', 'corrected'])
export const SalaryLineItemTypeSchema = z.enum([
'monthly_salary', 'hourly_salary', 'overtime', 'bonus', 'commission',
'gross_deduction_pension', 'gross_deduction_other',
'benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_other',
'benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_bike', 'benefit_other',
'sick_karens', 'sick_day2_14', 'sick_day15_plus',
'vab', 'parental_leave', 'vacation',
'traktamente_taxfree', 'traktamente_taxable',
@@ -795,6 +795,47 @@ export const UpdateEmployeeSchema = EmployeeSchemaBase.partial().superRefine((da
}
})
export const EmployeeBenefitTypeSchema = z.enum(['bike', 'car', 'meals', 'housing', 'wellness', 'other'])
export const CreateEmployeeBenefitSchema = z.object({
benefit_type: EmployeeBenefitTypeSchema,
description: z.string().min(1).max(200),
monthly_value: z.number().nonnegative().optional(),
/** For bike benefit: annual market value of the förmån. The server computes
* monthly_value = max(0, annual − 3000) / 12 per Skatteverket schablon. */
annual_market_value: z.number().nonnegative().optional(),
valid_from: isoDate,
valid_to: isoDate.optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
is_active: z.boolean().optional(),
}).superRefine((data, ctx) => {
if (data.benefit_type === 'bike') {
if (data.annual_market_value === undefined && data.monthly_value === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Cykelförmån kräver årligt marknadsvärde',
path: ['annual_market_value'],
})
}
} else if (data.monthly_value === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Månatligt förmånsvärde krävs',
path: ['monthly_value'],
})
}
})
export const UpdateEmployeeBenefitSchema = z.object({
description: z.string().min(1).max(200).optional(),
monthly_value: z.number().nonnegative().optional(),
annual_market_value: z.number().nonnegative().optional(),
valid_from: isoDate.optional(),
valid_to: isoDate.nullable().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
is_active: z.boolean().optional(),
})
export const CreateSalaryRunSchema = z.object({
period_year: z.number().int().min(2020).max(2100),
period_month: z.number().int().min(1).max(12),
-1
View File
@@ -137,7 +137,6 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
<p style="margin: 0; color: #666; font-size: 14px;">
Med vänliga hälsningar,<br>
<strong>${getCompanyPrimaryName(company)}</strong>
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
</p>
${company.org_number ? `
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
-1
View File
@@ -180,7 +180,6 @@ export function generateReminderEmailHtml(data: ReminderEmailData): string {
<p style="margin: 0; color: #666; font-size: 14px;">
Med vänliga hälsningar,<br>
<strong>${getCompanyPrimaryName(company)}</strong>
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
</p>
${company.org_number ? `
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
-11
View File
@@ -24,11 +24,6 @@ const REQUIRED_EXTENSION_VARS = [
'OPENAI_API_KEY',
] as const
const OPTIONAL_VARS = [
'LANGFUSE_SECRET_KEY',
'LANGFUSE_PUBLIC_KEY',
] as const
function validateEnvironment(): void {
// During builds (CI, Docker, Vercel), env vars may be absent or set to
// placeholder sentinels. Skip validation so Next.js page collection
@@ -54,12 +49,6 @@ function validateEnvironment(): void {
if (missingExt.length > 0) {
log.warn(`Missing extension environment variables (extensions needing them may not work): ${missingExt.join(', ')}`)
}
for (const v of OPTIONAL_VARS) {
if (!process.env[v]) {
log.warn(`Optional environment variable ${v} is not set`)
}
}
}
/**
+10 -19
View File
@@ -374,21 +374,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{/* Header */}
<View style={styles.header}>
<View style={styles.companyInfo}>
{company.logo_url && (
{company.logo_url && (company.invoice_show_logo ?? true) && (
<Image src={company.logo_url} style={{ maxHeight: 40, maxWidth: 150, marginBottom: 6, alignSelf: 'flex-start' }} />
)}
<Text style={styles.companyName}>{company.trade_name || company.company_name}</Text>
{company.trade_name && company.company_name && (
<Text style={{ fontSize: 8, color: '#666' }}>({company.company_name})</Text>
)}
{company.address_line1 && <Text>{company.address_line1}</Text>}
{(company.postal_code || company.city) && (
<Text>{company.postal_code} {company.city}</Text>
)}
{company.org_number && (
<Text style={{ marginTop: 4 }}>Org.nr: {formatOrgNumber(company.org_number)}</Text>
)}
{company.vat_number && <Text>VAT: {company.vat_number}</Text>}
<Text style={styles.companyName}>{company.company_name}</Text>
</View>
<View style={{ textAlign: 'right' }}>
<Text style={[styles.title, isCreditNote ? styles.creditNoteTitle : {}]}>
@@ -672,14 +661,16 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
</View>
)}
{/* Footer */}
{/* Footer — collected legal info per ML 17 kap 24§ */}
<View style={styles.footer}>
<Text style={styles.footerText}>
{company.trade_name || company.company_name}
{company.trade_name && company.company_name ? ` (${company.company_name})` : ''}
{company.org_number ? ` | Org.nr: ${formatOrgNumber(company.org_number)}` : ''}
{company.f_skatt ? ' | Godkänd för F-skatt' : ''}
{company.vat_number ? ` | Momsreg.nr: ${company.vat_number}` : ''}
{[
company.address_line1,
(company.postal_code || company.city) ? `${company.postal_code ?? ''} ${company.city ?? ''}`.trim() : null,
company.org_number ? `Org.nr: ${formatOrgNumber(company.org_number)}` : null,
company.vat_number ? `Momsreg.nr: ${company.vat_number}` : null,
company.f_skatt ? 'Godkänd för F-skatt' : null,
].filter(Boolean).join(' · ')}
</Text>
</View>
</Page>
+1 -1
View File
@@ -116,7 +116,7 @@ export async function sendReminder(
html: generateReminderEmailHtml(emailData),
text: generateReminderEmailText(emailData),
replyTo: company.email || undefined,
fromName: company.trade_name || company.company_name || undefined
fromName: company.company_name || undefined
})
return result
+1 -1
View File
@@ -616,7 +616,7 @@ async function commitSendInvoice(
html: generateInvoiceEmailHtml(emailData),
text: generateInvoiceEmailText(emailData),
replyTo: company.email || undefined,
fromName: company.trade_name || company.company_name,
fromName: company.company_name,
attachments: [{ filename, content: pdfBuffer, contentType: 'application/pdf' }],
})
+5 -4
View File
@@ -7,11 +7,12 @@ import {
} from '@/lib/http/fetch-with-timeout';
const DEFAULT_SCOPES = [
'ea:api',
'offline_access',
'ea:sales_readonly',
'ea:accounting_readonly',
'ea:purchase_readonly',
'ea:api',
'ea:sales',
'ea:accounting',
'ea:purchase',
'vls:api',
];
const EACCOUNTING_ACR_VALUE = 'service:44643EB1-3F76-4C1C-A672-402AE8085934';
@@ -5,8 +5,7 @@ import type { CompanySettings } from '@/types'
function fakeCompany(): CompanySettings {
return {
company_name: 'Gnubok AB',
trade_name: 'Gnubok',
company_name: 'Gnubok',
org_number: '5566778899',
vat_number: 'SE556677889901',
address_line1: 'Kungsgatan 1',
@@ -67,7 +67,6 @@ const mockGetAuditLog = vi.mocked(getAuditLog)
const COMPANY_ROW = {
company_name: 'Test AB',
trade_name: null,
org_number: '5566778899',
moms_period: 'quarterly',
}
@@ -265,7 +265,7 @@ export function FinancialStatementPDF({
company,
generatedAt,
}: FinancialStatementPDFProps) {
const companyDisplayName = company.trade_name || company.company_name || ''
const companyDisplayName = company.company_name || ''
const periodLabel = period.start && period.end
? `${formatDateSv(period.start)} – ${formatDateSv(period.end)}`
: ''
+1 -4
View File
@@ -43,7 +43,6 @@ interface FiscalPeriodRow {
interface CompanyInfo {
company_name: string | null
trade_name: string | null
org_number: string | null
moms_period: string | null
}
@@ -116,7 +115,6 @@ export async function generateFullArchive(
const sie = await generateSIEExport(supabase, companyId, {
fiscal_period_id: period.id,
company_name: company.company_name || 'Unknown',
trade_name: company.trade_name,
org_number: company.org_number,
})
sieFolder.file(`${periodLabel(period)}.se`, sie)
@@ -132,7 +130,6 @@ export async function generateFullArchive(
const sie = await generateSIEExport(supabase, companyId, {
fiscal_period_id: period.id,
company_name: company.company_name || 'Unknown',
trade_name: company.trade_name,
org_number: company.org_number,
})
zip.file('bokforing.se', sie)
@@ -227,7 +224,7 @@ export async function estimateArchiveSize(
async function fetchCompany(supabase: SupabaseClient, companyId: string): Promise<CompanyInfo> {
const { data } = await supabase
.from('company_settings')
.select('company_name, trade_name, org_number, moms_period')
.select('company_name, org_number, moms_period')
.eq('company_id', companyId)
.single()
+2 -2
View File
@@ -699,7 +699,7 @@ export async function generateINK2Declaration(
// Fetch company settings
const { data: settings } = await supabase
.from('company_settings')
.select('company_name, trade_name, org_number, entity_type, address_line1, postal_code, city, email')
.select('company_name, org_number, entity_type, address_line1, postal_code, city, email')
.eq('company_id', companyId)
.single()
@@ -934,7 +934,7 @@ export async function generateINK2Declaration(
resultAfterFinancial,
},
companyInfo: {
companyName: settings?.trade_name || settings?.company_name || 'Okänt företag',
companyName: settings?.company_name || 'Okänt företag',
orgNumber: settings?.org_number || null,
addressLine1: settings?.address_line1 || null,
postalCode: settings?.postal_code || null,
+2 -2
View File
@@ -175,7 +175,7 @@ export async function generateNEDeclaration(
// Fetch company settings
const { data: settings } = await supabase
.from('company_settings')
.select('company_name, trade_name, org_number, entity_type')
.select('company_name, org_number, entity_type')
.eq('company_id', companyId)
.single()
@@ -331,7 +331,7 @@ export async function generateNEDeclaration(
rutor,
breakdown,
companyInfo: {
companyName: settings?.trade_name || settings?.company_name || 'Okänt företag',
companyName: settings?.company_name || 'Okänt företag',
orgNumber: settings?.org_number || null,
},
warnings,
@@ -255,7 +255,7 @@ interface CommonHeaderProps {
}
function HeaderBlock({ title, company, period }: CommonHeaderProps) {
const companyDisplayName = company.trade_name || company.company_name || ''
const companyDisplayName = company.company_name || ''
const periodLabel = period.start && period.end
? `${formatDateSv(period.start)} – ${formatDateSv(period.end)}`
: ''
@@ -288,7 +288,7 @@ function HeaderBlock({ title, company, period }: CommonHeaderProps) {
}
function FooterBlock({ company, generatedAt }: { company: CompanySettings; generatedAt: string }) {
const companyDisplayName = company.trade_name || company.company_name || ''
const companyDisplayName = company.company_name || ''
return (
<View style={styles.footer} fixed>
<Text style={styles.footerText}>
+1 -1
View File
@@ -94,7 +94,7 @@ export async function generateSIEExport(
lines.push(`#ORGNR ${options.org_number}`)
}
lines.push(`#FNAMN "${escapeQuotes(options.trade_name || options.company_name)}"`)
lines.push(`#FNAMN "${escapeQuotes(options.company_name)}"`)
// === Fiscal year ===
// #RAR 0 = current year, #RAR -1 = previous year (both should be present per spec)
+27 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { calculateCarBenefit, getMealBenefitValue, calculateWellnessBenefit } from '../benefits'
import { calculateCarBenefit, getMealBenefitValue, calculateWellnessBenefit, calculateBikeBenefit, BIKE_BENEFIT_TAX_FREE_ALLOWANCE } from '../benefits'
import type { PayrollConfig } from '../payroll-config'
const config: PayrollConfig = {
@@ -151,3 +151,29 @@ describe('calculateWellnessBenefit', () => {
expect(result.taxableAmount).toBe(6000)
})
})
describe('calculateBikeBenefit', () => {
it('is tax-free entirely when annual value is below the 3 000 kr schablon', () => {
const result = calculateBikeBenefit(2500)
expect(result.annualTaxable).toBe(0)
expect(result.monthlyValue).toBe(0)
expect(result.taxFreePortion).toBe(2500)
})
it('taxes only the excess over 3 000 kr', () => {
const result = calculateBikeBenefit(8400)
// 8400 - 3000 = 5400 taxable annually → 450/month
expect(result.annualTaxable).toBe(5400)
expect(result.monthlyValue).toBe(450)
expect(result.taxFreePortion).toBe(3000)
})
it('handles 0 or negative input gracefully', () => {
expect(calculateBikeBenefit(0).monthlyValue).toBe(0)
expect(calculateBikeBenefit(-100).monthlyValue).toBe(0)
})
it('exposes the schablon constant', () => {
expect(BIKE_BENEFIT_TAX_FREE_ALLOWANCE).toBe(3000)
})
})
+15 -1
View File
@@ -173,13 +173,27 @@ export function calculateParentalLeaveDeduction(
export function calculateVacationPay(params: {
monthlySalary: number
vacationDaysTaken: number
vacationRule: 'procentregeln' | 'sammaloneregeln'
vacationRule: 'procentregeln' | 'sammaloneregeln' | 'none'
semestertillaggRate: number
vacationDaysPerYear: number
}): { amount: number; tillagg: number; steps: AbsenceStep[] } {
const r = (x: number) => Math.round(x * 100) / 100
const dailyRate = r(params.monthlySalary / 21)
if (params.vacationRule === 'none') {
// No accrual — vacation is included in monthly pay. No tillägg paid.
return {
amount: 0,
tillagg: 0,
steps: [{
label: 'Semesterlön (avstängd)',
formula: 'ingen separat semesterlön — ingår i månadslönen',
input: { days: params.vacationDaysTaken },
output: 0,
}],
}
}
if (params.vacationRule === 'sammaloneregeln') {
// Sammalöneregeln: regular pay + semestertillägg per day
const tillagg = r(params.monthlySalary * params.semestertillaggRate * params.vacationDaysTaken)
+1
View File
@@ -21,6 +21,7 @@ const LINE_ITEM_ACCOUNTS: Record<SalaryLineItemType, string> = {
benefit_housing: '7381',
benefit_meals: '7382',
benefit_wellness: '7699',
benefit_bike: '7388',
benefit_other: '7389',
// Absence
sick_karens: '7281',
+1
View File
@@ -80,6 +80,7 @@ export const BENEFIT_RUTA_MAP: Record<string, string> = {
benefit_housing: INDIVID_FIELDS.RUTA_014,
benefit_meals: INDIVID_FIELDS.RUTA_015,
benefit_wellness: INDIVID_FIELDS.RUTA_019,
benefit_bike: INDIVID_FIELDS.RUTA_019,
benefit_other: INDIVID_FIELDS.RUTA_019,
}
+50
View File
@@ -120,6 +120,56 @@ export function getMealBenefitValue(mealType: MealType, config: PayrollConfig):
}
}
// ============================================================
// Bike Benefit (Cykelförmån)
// ============================================================
/**
* Annual tax-free allowance for cykelförmån, in SEK.
* Per Skatteverket schablon effective from 2022: the first 3 000 kr/year of
* a bike benefit is tax-free; the excess is taxable. The employer must offer
* the benefit on equal terms to all employees.
*/
export const BIKE_BENEFIT_TAX_FREE_ALLOWANCE = 3000
/**
* Calculate monthly taxable cykelförmån from an annual market value.
*
* taxable_annual = max(0, annual_market_value - 3000)
* taxable_monthly = taxable_annual / 12
*
* `annualMarketValue` is the yearly värde av förmånen — typically the
* marknadsmässiga hyran or the subscription cost the employer pays.
*/
export function calculateBikeBenefit(annualMarketValue: number): {
monthlyValue: number
annualTaxable: number
taxFreePortion: number
steps: BenefitStep[]
} {
const r = (x: number) => Math.round(x * 100) / 100
const annual = Math.max(0, annualMarketValue)
const taxFreePortion = Math.min(annual, BIKE_BENEFIT_TAX_FREE_ALLOWANCE)
const annualTaxable = r(annual - taxFreePortion)
const monthlyValue = r(annualTaxable / 12)
return {
monthlyValue,
annualTaxable,
taxFreePortion,
steps: [{
label: 'Cykelförmån (schablon)',
formula: 'max(0, årligt marknadsvärde − 3 000 kr skattefritt) / 12',
input: {
annual_market_value: annual,
tax_free_allowance: BIKE_BENEFIT_TAX_FREE_ALLOWANCE,
annual_taxable: annualTaxable,
},
output: monthlyValue,
}],
}
}
// ============================================================
// Wellness Benefit (Friskvårdsbidrag)
// ============================================================
+22 -4
View File
@@ -31,7 +31,7 @@ export interface SalaryCalculationInput {
paymentDate: string
/** Vacation */
vacationRule: 'procentregeln' | 'sammaloneregeln'
vacationRule: 'procentregeln' | 'sammaloneregeln' | 'none'
vacationDaysPerYear: number
semestertillaggRate: number
@@ -217,7 +217,7 @@ export function calculateSalary(
// ─── Step 5: Add förmånsvärden to tax base ───
const benefitItems = input.lineItems.filter(
li => ['benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_other'].includes(li.itemType)
li => ['benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_bike', 'benefit_other'].includes(li.itemType)
)
const totalBenefits = r(benefitItems.reduce((sum, li) => sum + li.amount, 0))
if (totalBenefits > 0) {
@@ -355,7 +355,15 @@ export function calculateSalary(
baseSalary + vacationBasisItems.reduce((sum, li) => sum + li.amount, 0)
)
let vacationAccrual: number
if (input.vacationRule === 'procentregeln') {
if (input.vacationRule === 'none') {
vacationAccrual = 0
steps.push({
label: 'Semesteravsättning (avstängd)',
formula: 'ingen semesteravsättning bokas — semester ingår i månadslönen',
input: {},
output: 0,
})
} else if (input.vacationRule === 'procentregeln') {
const rate = input.vacationDaysPerYear >= 30 ? 0.144 : 0.12
vacationAccrual = r(vacationBasis * rate)
steps.push({
@@ -585,13 +593,23 @@ export function calculateSjuklon(
*/
export function calculateVacationAccrual(params: {
monthlySalary: number
vacationRule: 'procentregeln' | 'sammaloneregeln'
vacationRule: 'procentregeln' | 'sammaloneregeln' | 'none'
vacationDaysPerYear: number
semestertillaggRate: number
vacationBasis: number
}): { accrual: number; steps: CalculationStep[] } {
const steps: CalculationStep[] = []
if (params.vacationRule === 'none') {
steps.push({
label: 'Semesteravsättning (avstängd)',
formula: 'ingen semesteravsättning',
input: {},
output: 0,
})
return { accrual: 0, steps }
}
if (params.vacationRule === 'procentregeln') {
const rate = params.vacationDaysPerYear >= 30 ? 0.144 : 0.12
const accrual = r(params.vacationBasis * rate)
+1 -1
View File
@@ -142,7 +142,7 @@ async function createSalaryEntry(
// Add salary line items that are cash expenses
// Förmånsvärden (benefits) are excluded — they affect the tax base but
// have no cash flow and should not appear as expense lines in the journal.
const BENEFIT_TYPES = ['benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_other']
const BENEFIT_TYPES = ['benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_bike', 'benefit_other']
let lineItemTotal = 0
for (const li of emp.line_items) {
if (li.is_net_deduction || li.is_gross_deduction) continue
+4 -17
View File
@@ -46,25 +46,12 @@ export function formatOrgNumber(orgNumber: string): string {
return orgNumber
}
/**
* Returns the display name for a company, using trade name as primary
* with legal name in parentheses if both exist.
*/
export function getCompanyDisplayName(settings: { trade_name?: string | null; company_name?: string | null }): string {
const tradeName = settings.trade_name?.trim()
const legalName = settings.company_name?.trim()
if (tradeName && legalName) {
return `${tradeName} (${legalName})`
}
return legalName || tradeName || ''
export function getCompanyDisplayName(settings: { company_name?: string | null }): string {
return settings.company_name?.trim() || ''
}
/**
* Returns just the primary name for contexts where a short name is needed
* (e.g. email from name). Uses trade name if set, otherwise legal name.
*/
export function getCompanyPrimaryName(settings: { trade_name?: string | null; company_name?: string | null }): string {
return settings.trade_name?.trim() || settings.company_name?.trim() || ''
export function getCompanyPrimaryName(settings: { company_name?: string | null }): string {
return settings.company_name?.trim() || ''
}
export function generateInvoiceNumber(): string {