diff --git a/src/app/api/settings/billing/route.ts b/src/app/api/settings/billing/route.ts
index 9bca7f1..d04c3fd 100644
--- a/src/app/api/settings/billing/route.ts
+++ b/src/app/api/settings/billing/route.ts
@@ -22,6 +22,10 @@ import { getOrgBilling, upsertOrgBilling } from "@/lib/db";
const upsertSchema = z.object({
companyName: z.string().trim().min(1).max(200),
+ // Phase 6 fix: optional "z.Hd." / "Attn:" line. Personal accounts
+ // never send this (the UI hides the field); orgs may set or leave
+ // it empty.
+ contactName: z.string().trim().max(200).optional().nullable(),
streetAddress: z.string().trim().min(1).max(200),
postalCode: z.string().trim().min(1).max(20),
city: z.string().trim().min(1).max(100),
@@ -73,6 +77,7 @@ export async function PUT(request: Request) {
const billing = await upsertOrgBilling({
zitadelOrgId: user.orgId,
companyName: data.companyName,
+ contactName: data.contactName ?? null,
streetAddress: data.streetAddress,
postalCode: data.postalCode,
city: data.city,
diff --git a/src/components/settings/billing-form.tsx b/src/components/settings/billing-form.tsx
index 821fa34..75ca199 100644
--- a/src/components/settings/billing-form.tsx
+++ b/src/components/settings/billing-form.tsx
@@ -37,6 +37,7 @@ export function BillingSettingsForm({ initial, isPersonal }: Props) {
const router = useRouter();
const [form, setForm] = useState({
companyName: initial?.companyName ?? "",
+ contactName: initial?.contactName ?? "",
streetAddress: initial?.streetAddress ?? "",
postalCode: initial?.postalCode ?? "",
city: initial?.city ?? "",
@@ -84,6 +85,10 @@ export function BillingSettingsForm({ initial, isPersonal }: Props) {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
companyName: form.companyName.trim(),
+ // Personal accounts don't have a contact-name field
+ // (companyName IS their name); force null so stale state
+ // from a previously-org-flagged account can't carry over.
+ contactName: isPersonal ? null : form.contactName.trim() || null,
streetAddress: form.streetAddress.trim(),
postalCode: form.postalCode.trim(),
city: form.city.trim(),
@@ -124,6 +129,17 @@ export function BillingSettingsForm({ initial, isPersonal }: Props) {
className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm"
/>
+ {!isPersonal && (
+
+
+
+ )}
= {
dueDate: "Zahlbar bis",
period: "Abrechnungsperiode",
billTo: "Rechnungsempfänger",
+ attentionPrefix: "z.Hd.",
description: "Beschreibung",
quantity: "Menge",
unitPrice: "Einzelpreis",
@@ -139,6 +145,7 @@ const MESSAGES: Record = {
dueDate: "Due date",
period: "Billing period",
billTo: "Bill to",
+ attentionPrefix: "Attn:",
description: "Description",
quantity: "Qty",
unitPrice: "Unit price",
@@ -171,6 +178,7 @@ const MESSAGES: Record = {
dueDate: "Échéance",
period: "Période de facturation",
billTo: "Destinataire",
+ attentionPrefix: "À l'attention de",
description: "Description",
quantity: "Qté",
unitPrice: "Prix unitaire",
@@ -203,6 +211,7 @@ const MESSAGES: Record = {
dueDate: "Scadenza",
period: "Periodo di fatturazione",
billTo: "Destinatario",
+ attentionPrefix: "c.a.",
description: "Descrizione",
quantity: "Qtà",
unitPrice: "Prezzo unitario",
@@ -524,6 +533,15 @@ const InvoicePdf: React.FC = ({ invoice, lines }) => {
{s.billTo}
{snap.companyName}
+ {/* Phase 6 fix: optional "z.Hd." / "Attn:" line for routing
+ the printed invoice internally at the customer. Prints
+ between the company name and street address, in the
+ invoice's locale (frozen at issue time). */}
+ {snap.contactName && (
+
+ {s.attentionPrefix} {snap.contactName}
+
+ )}
{snap.streetAddress}
{snap.postalCode} {snap.city}
diff --git a/src/lib/billing.ts b/src/lib/billing.ts
index 6adc501..6cf2fc2 100644
--- a/src/lib/billing.ts
+++ b/src/lib/billing.ts
@@ -645,6 +645,7 @@ export async function computeInvoiceDraft(opts: {
}
const snapshot: InvoiceBillingSnapshot = {
companyName: orgBilling.companyName,
+ contactName: orgBilling.contactName ?? null,
streetAddress: orgBilling.streetAddress,
postalCode: orgBilling.postalCode,
city: orgBilling.city,
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 33601c7..6be8555 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -198,6 +198,12 @@ const MIGRATION_SQL = `
CREATE TABLE IF NOT EXISTS org_billing (
zitadel_org_id TEXT PRIMARY KEY,
company_name TEXT NOT NULL,
+ -- Phase 6 fix: optional contact-person line shown on the
+ -- invoice PDF below the company name (e.g. "z.Hd. Herr Müller").
+ -- Not normally needed since invoices are delivered by email
+ -- link, but useful when customers forward the PDF internally
+ -- for AP routing in larger organizations.
+ contact_name TEXT,
street_address TEXT NOT NULL,
postal_code TEXT NOT NULL,
city TEXT NOT NULL,
@@ -208,6 +214,10 @@ const MIGRATION_SQL = `
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
+ -- Phase 6 fix: ensure the column exists on databases that were
+ -- created before contact_name was added to the base schema above.
+ -- IF NOT EXISTS makes this safe to run repeatedly via ensureSchema.
+ ALTER TABLE org_billing ADD COLUMN IF NOT EXISTS contact_name TEXT;
-- Feature 5: lightweight customer support / feedback tickets.
-- Scoped strictly per-user (zitadel_user_id), not per-org —
@@ -1262,6 +1272,7 @@ function rowToOrgBilling(row: any): OrgBilling {
return {
zitadelOrgId: row.zitadel_org_id,
companyName: row.company_name,
+ contactName: row.contact_name ?? null,
streetAddress: row.street_address,
postalCode: row.postal_code,
city: row.city,
@@ -1306,12 +1317,13 @@ export async function upsertOrgBilling(
await ensureSchema();
const result = await getPool().query(
`INSERT INTO org_billing (
- zitadel_org_id, company_name, street_address, postal_code,
- city, country, vat_number, billing_email, notes
+ zitadel_org_id, company_name, contact_name, street_address,
+ postal_code, city, country, vat_number, billing_email, notes
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (zitadel_org_id) DO UPDATE SET
company_name = EXCLUDED.company_name,
+ contact_name = EXCLUDED.contact_name,
street_address = EXCLUDED.street_address,
postal_code = EXCLUDED.postal_code,
city = EXCLUDED.city,
@@ -1324,6 +1336,7 @@ export async function upsertOrgBilling(
[
data.zitadelOrgId,
data.companyName,
+ data.contactName ?? null,
data.streetAddress,
data.postalCode,
data.city,
diff --git a/src/messages/de.json b/src/messages/de.json
index 8759116..62ef822 100644
--- a/src/messages/de.json
+++ b/src/messages/de.json
@@ -504,7 +504,9 @@
"invalidCountry": "Ländercode muss aus 2 Buchstaben bestehen (z.B. CH).",
"invalidEmail": "Bitte eine gültige E-Mail-Adresse eingeben.",
"fullNameLabel": "Vor- und Nachname",
- "subtitlePersonal": "Ihre Rechnungsadresse und Rechnungskontakt. Erforderlich, bevor Rechnungen ausgestellt werden können."
+ "subtitlePersonal": "Ihre Rechnungsadresse und Rechnungskontakt. Erforderlich, bevor Rechnungen ausgestellt werden können.",
+ "contactNameLabel": "Ansprechperson (optional)",
+ "contactNameHint": "Erscheint als 'z.Hd. ' auf der Rechnung unter dem Firmennamen. Hilfreich für die Zuordnung in der Buchhaltung grösserer Firmen."
},
"support": {
"title": "Support",
diff --git a/src/messages/en.json b/src/messages/en.json
index 2291002..cd70134 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -504,7 +504,9 @@
"invalidCountry": "Country code must be 2 letters (e.g. CH).",
"invalidEmail": "Please enter a valid email address.",
"fullNameLabel": "Full name",
- "subtitlePersonal": "Your billing address and invoice contact. Required before invoices can be issued."
+ "subtitlePersonal": "Your billing address and invoice contact. Required before invoices can be issued.",
+ "contactNameLabel": "Contact person (optional)",
+ "contactNameHint": "Prints as 'Attn: ' on the invoice below the company name. Useful for AP routing in larger organizations."
},
"support": {
"title": "Support",
diff --git a/src/messages/fr.json b/src/messages/fr.json
index 2b4af1b..4f18caa 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -504,7 +504,9 @@
"invalidCountry": "Le code pays doit comporter 2 lettres (p. ex. CH).",
"invalidEmail": "Veuillez saisir une adresse e-mail valide.",
"fullNameLabel": "Nom et prénom",
- "subtitlePersonal": "Votre adresse de facturation et votre contact. Requis avant l'émission de toute facture."
+ "subtitlePersonal": "Votre adresse de facturation et votre contact. Requis avant l'émission de toute facture.",
+ "contactNameLabel": "Personne à contacter (facultatif)",
+ "contactNameHint": "S'imprime « À l'attention de » sur la facture, sous le nom de l'entreprise. Utile pour le routage en comptabilité dans les grandes organisations."
},
"support": {
"title": "Support",
diff --git a/src/messages/it.json b/src/messages/it.json
index 8640f3f..1271295 100644
--- a/src/messages/it.json
+++ b/src/messages/it.json
@@ -504,7 +504,9 @@
"invalidCountry": "Il codice paese deve essere di 2 lettere (es. CH).",
"invalidEmail": "Inserisci un indirizzo e-mail valido.",
"fullNameLabel": "Nome e cognome",
- "subtitlePersonal": "Il tuo indirizzo di fatturazione e contatto. Necessari prima che possano essere emesse fatture."
+ "subtitlePersonal": "Il tuo indirizzo di fatturazione e contatto. Necessari prima che possano essere emesse fatture.",
+ "contactNameLabel": "Persona di contatto (facoltativa)",
+ "contactNameHint": "Stampato come 'c.a. ' sulla fattura, sotto il nome dell'azienda. Utile per l'instradamento contabile in grandi organizzazioni."
},
"support": {
"title": "Supporto",
diff --git a/src/types/index.ts b/src/types/index.ts
index 46e598b..7a195c7 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -234,6 +234,12 @@ export interface BillingAddress {
export interface OrgBilling {
zitadelOrgId: string;
companyName: string;
+ // Optional contact-person line ("z.Hd. / Attn:") shown on the
+ // invoice PDF below the company name. Useful when invoicing
+ // larger companies where the mailroom needs a name to route
+ // the document. Personal accounts don't expose this in the UI —
+ // their "Full name" already lives in companyName.
+ contactName?: string | null;
streetAddress: string;
postalCode: string;
city: string;
@@ -575,6 +581,7 @@ export type InvoiceLineKind =
*/
export interface InvoiceBillingSnapshot {
companyName: string;
+ contactName: string | null;
streetAddress: string;
postalCode: string;
city: string;