Phase6c: Optional Company contact name

This commit is contained in:
2026-05-25 13:14:36 +02:00
parent 522246e386
commit 31953e8f0a
8 changed files with 121 additions and 35 deletions

View File

@@ -76,6 +76,7 @@ export default async function NewInstancePage() {
userName={user.name}
userEmail={user.email}
hasOrgBilling={hasOrgBilling}
existingOrgBilling={orgBilling}
/>
</div>
</div>

View File

@@ -317,6 +317,7 @@ export default async function DashboardPage() {
userName={user.name}
userEmail={user.email}
hasOrgBilling={hasOrgBilling}
existingOrgBilling={orgBilling}
/>
</div>
</div>

View File

@@ -2,6 +2,7 @@
import { useRouter } from "next/navigation";
import { OnboardingWizard } from "./wizard";
import type { OrgBilling } from "@/types";
interface OnboardingFlowProps {
orgName: string;
@@ -19,6 +20,12 @@ interface OnboardingFlowProps {
* /settings/billing.
*/
hasOrgBilling?: boolean;
/**
* Phase 6 fix3: the actual org_billing record (or null). Drives
* the review-step "Billing to" rendering AND the confirm-step
* validation skip when the billing step was skipped.
*/
existingOrgBilling?: OrgBilling | null;
/**
* Bug 6: when present, the wizard is rendered in edit mode against
* the given pending request. See `OnboardingWizard` for the full
@@ -45,6 +52,7 @@ export function OnboardingFlow({
userName,
userEmail,
hasOrgBilling,
existingOrgBilling,
editingRequest,
}: OnboardingFlowProps) {
const router = useRouter();
@@ -55,6 +63,7 @@ export function OnboardingFlow({
userName={userName}
userEmail={userEmail}
hasOrgBilling={hasOrgBilling}
existingOrgBilling={existingOrgBilling}
editingRequest={editingRequest}
onComplete={() => {
// Navigate back to /dashboard and re-fetch on the server. The

View File

@@ -13,6 +13,7 @@ import {
SUPPORTED_COUNTRIES,
type SupportedCountry,
} from "@/lib/validation";
import type { OrgBilling } from "@/types";
type Step = "welcome" | "configure" | "billing" | "confirm";
@@ -96,6 +97,17 @@ interface WizardProps {
* fix it before admin approves.
*/
hasOrgBilling?: boolean;
/**
* Phase 6 fix3: the actual org_billing record when one exists.
* Used to render real values on the review-step "Billing to" block
* (rather than the wizard's empty default config.billingAddress)
* AND to skip the confirm-step's client-side validation of
* billingAddress — same logic that already strips billingAddress
* at submit time. Null when no org_billing row exists yet.
* Ignored in edit mode (the editingRequest carries its own
* billingAddress snapshot).
*/
existingOrgBilling?: OrgBilling | null;
/**
* Bug 6: when present, the wizard renders in "edit" mode — fields
* are pre-populated from the request, the SOUL.md auto-fetch is
@@ -134,6 +146,7 @@ export function OnboardingWizard({
userName,
userEmail,
hasOrgBilling,
existingOrgBilling,
editingRequest,
onComplete,
}: WizardProps) {
@@ -319,7 +332,23 @@ export function OnboardingWizard({
}
// confirm: validate the union (defence in depth — submit handler
// also runs onboardingSchema before POST).
const r = onboardingSchema.safeParse(config);
//
// Phase 6 fix3: when hasOrgBilling=true AND not editing, the
// billing step was skipped and config.billingAddress is the
// empty default. zod's .optional() doesn't help here because the
// field IS present (empty object), so billingAddressSchema
// validates it and fails with required-field errors that the
// user has no way to fix — the form to enter the values was
// skipped on purpose. Strip the field for validation, matching
// the same strip we already do at submit time.
const configForValidation =
hasOrgBilling && !isEditing
? (() => {
const { billingAddress: _b, ...rest } = config;
return rest;
})()
: config;
const r = onboardingSchema.safeParse(configForValidation);
if (r.success) {
setErrors({});
return true;
@@ -1101,42 +1130,84 @@ export function OnboardingWizard({
<ReviewRow
label={t("reviewBillingTo")}
value={
<div className="text-text-primary text-right">
{/* For personal: skip the company line so the
invoice rendering matches what the user actually
entered. For company: include it as the first
line. */}
{!isPersonal &&
config.billingAddress.company &&
config.billingAddress.company.trim().length > 0 && (
<div>{config.billingAddress.company}</div>
)}
<div>{config.billingAddress.street}</div>
<div>
{config.billingAddress.postalCode}{" "}
{config.billingAddress.city}
</div>
<div className="text-text-muted">
{tCountries(
config.billingAddress.country as SupportedCountry
)}
</div>
</div>
(() => {
// Phase 6 fix3: when the org has billing on file
// and we're not editing, render the saved
// org_billing record (the authoritative source)
// rather than config.billingAddress, which is the
// wizard's empty default state because the billing
// step was skipped. In edit mode, fall back to
// config.billingAddress, which is pre-populated
// from the request being edited.
const useSaved =
hasOrgBilling && !isEditing && existingOrgBilling;
const company = useSaved
? existingOrgBilling!.companyName
: config.billingAddress.company;
const street = useSaved
? existingOrgBilling!.streetAddress
: config.billingAddress.street;
const postalCode = useSaved
? existingOrgBilling!.postalCode
: config.billingAddress.postalCode;
const city = useSaved
? existingOrgBilling!.city
: config.billingAddress.city;
const country = useSaved
? existingOrgBilling!.country
: config.billingAddress.country;
const contactName = useSaved
? existingOrgBilling!.contactName
: null;
return (
<div className="text-text-primary text-right">
{/* For personal: skip the company line so the
invoice rendering matches what the user actually
entered. For company: include it as the first
line. */}
{!isPersonal &&
company &&
company.trim().length > 0 && <div>{company}</div>}
{/* Phase 6 fix2: optional contact-person line
("z.Hd. <name>") only present when the saved
org_billing has it set. */}
{contactName && contactName.trim().length > 0 && (
<div className="text-text-muted">
{t("reviewContactPersonPrefix")} {contactName}
</div>
)}
<div>{street}</div>
<div>
{postalCode} {city}
</div>
<div className="text-text-muted">
{tCountries(country as SupportedCountry)}
</div>
</div>
);
})()
}
/>
{/* Bug 35: VAT review row. Company customers see this so
they can verify the VAT id they typed before submitting.
Personal customers never see it — they don't have a
VAT number, the form didn't ask, the review hides it. */}
VAT number, the form didn't ask, the review hides it.
Phase 6 fix3: when reading from existingOrgBilling,
the value comes from there too. */}
{!isPersonal &&
config.billingAddress.vatNumber &&
config.billingAddress.vatNumber.trim().length > 0 && (
<ReviewRow
label={t("billingVatNumber")}
value={config.billingAddress.vatNumber}
mono
/>
)}
(() => {
const vat =
hasOrgBilling && !isEditing && existingOrgBilling
? existingOrgBilling.vatNumber
: config.billingAddress.vatNumber;
return vat && vat.trim().length > 0 ? (
<ReviewRow
label={t("billingVatNumber")}
value={vat}
mono
/>
) : null;
})()}
<ReviewRow
label={t("reviewContactEmail")}
value={userEmail || ""}

View File

@@ -121,7 +121,8 @@
"saveChanges": "Änderungen speichern",
"billingVatNumber": "MWST-Nummer",
"billingVatHelp": "Ihre registrierte MWST-Nummer. Falls Ihre Firma von der MWST befreit ist, leer lassen und in den Notizen erläutern.",
"billingNotesPlaceholderPersonal": "Was wir wissen sollten — bevorzugte Zahlungsart, Rechnungsreferenz, etc."
"billingNotesPlaceholderPersonal": "Was wir wissen sollten — bevorzugte Zahlungsart, Rechnungsreferenz, etc.",
"reviewContactPersonPrefix": "z.Hd."
},
"dashboard": {
"title": "Dashboard",

View File

@@ -121,7 +121,8 @@
"saveChanges": "Save changes",
"billingVatNumber": "VAT number",
"billingVatHelp": "Your registered VAT identifier. If your company is VAT-exempt, leave blank and explain in the notes field.",
"billingNotesPlaceholderPersonal": "Anything we should know — preferred payment method, billing reference, etc."
"billingNotesPlaceholderPersonal": "Anything we should know — preferred payment method, billing reference, etc.",
"reviewContactPersonPrefix": "Attn:"
},
"dashboard": {
"title": "Dashboard",

View File

@@ -121,7 +121,8 @@
"saveChanges": "Enregistrer les modifications",
"billingVatNumber": "Numéro de TVA",
"billingVatHelp": "Votre identifiant TVA enregistré. Si votre entreprise est exonérée de TVA, laissez vide et précisez dans les notes.",
"billingNotesPlaceholderPersonal": "Tout ce que nous devons savoir — moyen de paiement préféré, référence de facturation, etc."
"billingNotesPlaceholderPersonal": "Tout ce que nous devons savoir — moyen de paiement préféré, référence de facturation, etc.",
"reviewContactPersonPrefix": "À l'attention de"
},
"dashboard": {
"title": "Tableau de bord",

View File

@@ -121,7 +121,8 @@
"saveChanges": "Salva modifiche",
"billingVatNumber": "Partita IVA",
"billingVatHelp": "Il tuo identificativo IVA registrato. Se la tua azienda è esente IVA, lascia vuoto e spiega nelle note.",
"billingNotesPlaceholderPersonal": "Qualsiasi cosa dovremmo sapere — metodo di pagamento preferito, riferimento per fatturazione, ecc."
"billingNotesPlaceholderPersonal": "Qualsiasi cosa dovremmo sapere — metodo di pagamento preferito, riferimento per fatturazione, ecc.",
"reviewContactPersonPrefix": "c.a."
},
"dashboard": {
"title": "Dashboard",