This commit is contained in:
@@ -30,23 +30,73 @@ export const SUPPORTED_COUNTRIES = ["CH", "DE", "AT", "FR", "IT", "LI"] as const
|
||||
export type SupportedCountry = (typeof SUPPORTED_COUNTRIES)[number];
|
||||
|
||||
/**
|
||||
* Billing address — every field required at minimum non-empty length.
|
||||
* Postal code rules vary too much across DACH+ to enforce a single
|
||||
* regex usefully; we settle for "non-empty, ≤ 12 chars". Country is a
|
||||
* fixed enum to prevent free-text typos that break invoicing.
|
||||
* Country-specific postal-code patterns. Bug 33: previously a postal
|
||||
* code could be anything (e.g. "abc"), which broke invoicing.
|
||||
*
|
||||
* Patterns are deliberately conservative — they reject obviously wrong
|
||||
* input but don't try to be exhaustive valid-range checkers (e.g. CH
|
||||
* codes are 1000-9999 in practice but \d{4} accepts 0000; the post
|
||||
* office will reject downstream if it matters). If a future country
|
||||
* has multi-format codes (e.g. UK postcodes with the inner-outer
|
||||
* structure), add it as a regex here rather than trying to fit
|
||||
* every country into the same shape.
|
||||
*/
|
||||
export const billingAddressSchema = z.object({
|
||||
// Company line is structurally optional — personal accounts leave it
|
||||
// empty by design (Bug 2). Server-side, the wizard's UI hides the
|
||||
// field for personals; the schema just doesn't require it.
|
||||
company: z.string().trim().max(100).optional().default(""),
|
||||
street: z.string().trim().min(1, "required").max(200),
|
||||
postalCode: z.string().trim().min(1, "required").max(12),
|
||||
city: z.string().trim().min(1, "required").max(100),
|
||||
country: z.enum(SUPPORTED_COUNTRIES, {
|
||||
message: "Please choose a country from the list",
|
||||
}),
|
||||
});
|
||||
const POSTAL_CODE_PATTERNS: Record<SupportedCountry, RegExp> = {
|
||||
CH: /^\d{4}$/,
|
||||
DE: /^\d{5}$/,
|
||||
AT: /^\d{4}$/,
|
||||
FR: /^\d{5}$/,
|
||||
IT: /^\d{5}$/,
|
||||
LI: /^\d{4}$/,
|
||||
};
|
||||
|
||||
/**
|
||||
* Postal-code expectation in human terms — used in error messages so
|
||||
* the user gets a useful hint ("expected 4 digits") rather than just
|
||||
* a regex failure. Keep in sync with POSTAL_CODE_PATTERNS.
|
||||
*/
|
||||
const POSTAL_CODE_HINTS: Record<SupportedCountry, string> = {
|
||||
CH: "4 digits",
|
||||
DE: "5 digits",
|
||||
AT: "4 digits",
|
||||
FR: "5 digits",
|
||||
IT: "5 digits",
|
||||
LI: "4 digits",
|
||||
};
|
||||
|
||||
/**
|
||||
* Billing address — every field required at minimum non-empty length.
|
||||
* Postal code is validated against the chosen country (Bug 33). Country
|
||||
* is a fixed enum to prevent free-text typos that break invoicing.
|
||||
*
|
||||
* `superRefine` is the right hook here because we need to look at two
|
||||
* fields (country + postalCode) together. The error path is set on
|
||||
* `postalCode` so the wizard renders the inline error under the right
|
||||
* input rather than at the form root.
|
||||
*/
|
||||
export const billingAddressSchema = z
|
||||
.object({
|
||||
// Company line is structurally optional — personal accounts leave it
|
||||
// empty by design (Bug 2). Server-side, the wizard's UI hides the
|
||||
// field for personals; the schema just doesn't require it.
|
||||
company: z.string().trim().max(100).optional().default(""),
|
||||
street: z.string().trim().min(1, "required").max(200),
|
||||
postalCode: z.string().trim().min(1, "required").max(12),
|
||||
city: z.string().trim().min(1, "required").max(100),
|
||||
country: z.enum(SUPPORTED_COUNTRIES, {
|
||||
message: "Please choose a country from the list",
|
||||
}),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const pattern = POSTAL_CODE_PATTERNS[data.country];
|
||||
if (!pattern.test(data.postalCode)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["postalCode"],
|
||||
message: `Invalid postal code (expected ${POSTAL_CODE_HINTS[data.country]})`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type BillingAddressInput = z.infer<typeof billingAddressSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user