Enhance checkout flow with new components and validation

- 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.
This commit is contained in:
Bastian Masanek
2025-11-03 15:38:16 +01:00
parent 47fe14c6cc
commit 527379a2cd
44 changed files with 4957 additions and 142 deletions

View File

@@ -117,6 +117,7 @@ export async function getCartWithItems(cartId: string): Promise<CartSummary> {
active: item.product.active,
category: item.product.category,
imageUrl: item.product.imageUrl,
navProductId: item.product.navProductId,
},
subtotal: Number.parseFloat(item.product.price) * item.quantity,
})

View File

@@ -0,0 +1,97 @@
/**
* 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'>