From eea027b3b04f29ac47431aafeb9d1f2d3c54c23c Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 May 2026 13:14:36 +0200 Subject: [PATCH] Phase6c: Optional Company contact name --- src/app/[locale]/dashboard/new/page.tsx | 1 + src/app/[locale]/dashboard/page.tsx | 1 + src/components/onboarding/onboarding-flow.tsx | 9 ++ src/components/onboarding/wizard.tsx | 133 ++++++++++++++---- src/messages/de.json | 3 +- src/messages/en.json | 3 +- src/messages/fr.json | 3 +- src/messages/it.json | 3 +- 8 files changed, 121 insertions(+), 35 deletions(-) diff --git a/src/app/[locale]/dashboard/new/page.tsx b/src/app/[locale]/dashboard/new/page.tsx index 1f09a22..90e193b 100644 --- a/src/app/[locale]/dashboard/new/page.tsx +++ b/src/app/[locale]/dashboard/new/page.tsx @@ -76,6 +76,7 @@ export default async function NewInstancePage() { userName={user.name} userEmail={user.email} hasOrgBilling={hasOrgBilling} + existingOrgBilling={orgBilling} /> diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx index 459571a..73c0e3c 100644 --- a/src/app/[locale]/dashboard/page.tsx +++ b/src/app/[locale]/dashboard/page.tsx @@ -317,6 +317,7 @@ export default async function DashboardPage() { userName={user.name} userEmail={user.email} hasOrgBilling={hasOrgBilling} + existingOrgBilling={orgBilling} /> diff --git a/src/components/onboarding/onboarding-flow.tsx b/src/components/onboarding/onboarding-flow.tsx index b3c8f83..95aeaf6 100644 --- a/src/components/onboarding/onboarding-flow.tsx +++ b/src/components/onboarding/onboarding-flow.tsx @@ -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 diff --git a/src/components/onboarding/wizard.tsx b/src/components/onboarding/wizard.tsx index 2b89faf..155a82e 100644 --- a/src/components/onboarding/wizard.tsx +++ b/src/components/onboarding/wizard.tsx @@ -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({ - {/* 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 && ( -
{config.billingAddress.company}
- )} -
{config.billingAddress.street}
-
- {config.billingAddress.postalCode}{" "} - {config.billingAddress.city} -
-
- {tCountries( - config.billingAddress.country as SupportedCountry - )} -
- + (() => { + // 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 ( +
+ {/* 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 &&
{company}
} + {/* Phase 6 fix2: optional contact-person line + ("z.Hd. ") only present when the saved + org_billing has it set. */} + {contactName && contactName.trim().length > 0 && ( +
+ {t("reviewContactPersonPrefix")} {contactName} +
+ )} +
{street}
+
+ {postalCode} {city} +
+
+ {tCountries(country as SupportedCountry)} +
+
+ ); + })() } /> {/* 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 && ( - - )} + (() => { + const vat = + hasOrgBilling && !isEditing && existingOrgBilling + ? existingOrgBilling.vatNumber + : config.billingAddress.vatNumber; + return vat && vat.trim().length > 0 ? ( + + ) : null; + })()}