Phase6c: Optional Company contact name
All checks were successful
Build and Push / build (push) Successful in 1m38s

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

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 || ""}