- Added AddressForm and CheckoutForm components for user input during checkout. - Implemented validation using Zod and VeeValidate for billing address fields. - Created OrderSummary and MockPayPalButton components for order confirmation and payment simulation. - Updated CartSheet and CartSidebar to navigate to the new checkout page at '/kasse'. - Introduced new API endpoints for validating checkout data and creating orders. - Enhanced user experience with responsive design and error handling. These changes complete the checkout functionality, allowing users to enter billing information, simulate payment, and confirm orders.
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
/**
|
|
* Checkout Schema
|
|
*
|
|
* Validation schema for billing address data during checkout.
|
|
* All fields are required at checkout (even though optional in users table).
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
|
|
/**
|
|
* Salutation validation
|
|
* Maps to: male → HERR, female → FRAU, other → K_ANGABE (X-API format)
|
|
*/
|
|
export const salutationEnum = z.enum(['male', 'female', 'other'], {
|
|
errorMap: () => ({ message: 'Bitte wähle eine Anrede' }),
|
|
})
|
|
|
|
/**
|
|
* German postal code validation (5 digits)
|
|
* Format: XXXXX (e.g., 74072, 10115)
|
|
*/
|
|
const germanPostCodeRegex = /^\d{5}$/
|
|
|
|
/**
|
|
* Date validation in YYYY-MM-DD format
|
|
* Must be a valid date in the past (birth date)
|
|
*/
|
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
|
|
|
/**
|
|
* Complete checkout schema
|
|
*
|
|
* Validates all billing address fields required for annual pass orders.
|
|
* These fields are also used for saving to user profile if "save address" checkbox is checked.
|
|
*/
|
|
export const checkoutSchema = z.object({
|
|
// Personal information
|
|
salutation: salutationEnum,
|
|
firstName: z
|
|
.string()
|
|
.min(2, 'Vorname muss mindestens 2 Zeichen lang sein')
|
|
.max(100, 'Vorname darf maximal 100 Zeichen lang sein')
|
|
.trim(),
|
|
lastName: z
|
|
.string()
|
|
.min(2, 'Nachname muss mindestens 2 Zeichen lang sein')
|
|
.max(100, 'Nachname darf maximal 100 Zeichen lang sein')
|
|
.trim(),
|
|
dateOfBirth: z
|
|
.string()
|
|
.regex(dateRegex, 'Geburtsdatum muss im Format YYYY-MM-DD sein')
|
|
.refine(
|
|
(date) => {
|
|
const parsed = new Date(date)
|
|
// Check if date is valid and in the past
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
return !isNaN(parsed.getTime()) && parsed < today
|
|
},
|
|
{ message: 'Geburtsdatum muss ein gültiges Datum in der Vergangenheit sein' }
|
|
),
|
|
|
|
// Address information
|
|
street: z
|
|
.string()
|
|
.min(3, 'Straße und Hausnummer müssen mindestens 3 Zeichen lang sein')
|
|
.max(200, 'Straße darf maximal 200 Zeichen lang sein')
|
|
.trim(),
|
|
postCode: z
|
|
.string()
|
|
.regex(germanPostCodeRegex, 'Postleitzahl muss aus 5 Ziffern bestehen (z.B. 74072)')
|
|
.trim(),
|
|
city: z
|
|
.string()
|
|
.min(2, 'Stadt muss mindestens 2 Zeichen lang sein')
|
|
.max(100, 'Stadt darf maximal 100 Zeichen lang sein')
|
|
.trim(),
|
|
countryCode: z
|
|
.string()
|
|
.length(2, 'Ländercode muss aus 2 Zeichen bestehen (ISO 3166-1 alpha-2)')
|
|
.toUpperCase()
|
|
.default('DE'), // Default to Germany
|
|
|
|
// Optional: flag to save address to user profile
|
|
saveAddress: z.boolean().optional().default(false),
|
|
})
|
|
|
|
/**
|
|
* TypeScript type inferred from Zod schema
|
|
*/
|
|
export type CheckoutData = z.infer<typeof checkoutSchema>
|
|
|
|
/**
|
|
* Billing address type (subset of CheckoutData, without saveAddress flag)
|
|
* Used for storing in order.billingAddress JSONB field
|
|
*/
|
|
export type BillingAddress = Omit<CheckoutData, 'saveAddress'>
|