- 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.
146 lines
4.0 KiB
TypeScript
146 lines
4.0 KiB
TypeScript
/**
|
|
* POST /api/orders/create
|
|
*
|
|
* Create a new order from the user's cart
|
|
*
|
|
* Request Body:
|
|
* {
|
|
* salutation: 'male' | 'female' | 'other'
|
|
* firstName: string
|
|
* lastName: string
|
|
* dateOfBirth: string (YYYY-MM-DD)
|
|
* street: string
|
|
* postCode: string
|
|
* city: string
|
|
* countryCode: string
|
|
* saveAddress: boolean (optional, default: false)
|
|
* }
|
|
*
|
|
* Behavior:
|
|
* - Creates order with status 'pending'
|
|
* - Copies cart items to order_items with price snapshot
|
|
* - Stores billing address snapshot in order
|
|
* - Generates unique order number (format: EXP-2025-00001)
|
|
* - Optionally saves address to user profile
|
|
* - Does NOT clear cart (cart is cleared after order confirmation)
|
|
*
|
|
* Response:
|
|
* {
|
|
* success: true
|
|
* orderId: string
|
|
* orderNumber: string
|
|
* message: string
|
|
* }
|
|
*/
|
|
|
|
import { checkoutSchema } from '../../utils/schemas/checkout'
|
|
import { orders, orderItems, users } from '../../database/schema'
|
|
import { eq, desc, sql } from 'drizzle-orm'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// Require authentication
|
|
const { user } = await requireUserSession(event)
|
|
|
|
// Validate request body
|
|
const body = await readBody(event)
|
|
const checkoutData = await checkoutSchema.parseAsync(body)
|
|
|
|
const db = useDatabase()
|
|
|
|
// Get user's cart
|
|
const cart = await getOrCreateCart(event)
|
|
const cartSummary = await getCartWithItems(cart.id)
|
|
|
|
// Validate cart has items
|
|
if (cartSummary.items.length === 0) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Warenkorb ist leer',
|
|
})
|
|
}
|
|
|
|
// Generate unique order number
|
|
// Format: EXP-YYYY-NNNNN (e.g., EXP-2025-00001)
|
|
const year = new Date().getFullYear()
|
|
|
|
// Get the highest order number for this year
|
|
const lastOrder = await db.query.orders.findFirst({
|
|
where: sql`${orders.orderNumber} LIKE ${`EXP-${year}-%`}`,
|
|
orderBy: desc(orders.createdAt),
|
|
})
|
|
|
|
let sequenceNumber = 1
|
|
if (lastOrder) {
|
|
// Extract sequence number from last order number (EXP-2025-00123 -> 123)
|
|
const match = lastOrder.orderNumber.match(/EXP-\d{4}-(\d{5})/)
|
|
if (match) {
|
|
sequenceNumber = Number.parseInt(match[1], 10) + 1
|
|
}
|
|
}
|
|
|
|
const orderNumber = `EXP-${year}-${String(sequenceNumber).padStart(5, '0')}`
|
|
|
|
// Prepare billing address (exclude saveAddress flag)
|
|
const billingAddress = {
|
|
salutation: checkoutData.salutation,
|
|
firstName: checkoutData.firstName,
|
|
lastName: checkoutData.lastName,
|
|
dateOfBirth: checkoutData.dateOfBirth,
|
|
street: checkoutData.street,
|
|
postCode: checkoutData.postCode,
|
|
city: checkoutData.city,
|
|
countryCode: checkoutData.countryCode,
|
|
}
|
|
|
|
// Create order
|
|
const [order] = await db
|
|
.insert(orders)
|
|
.values({
|
|
orderNumber,
|
|
userId: user.id,
|
|
totalAmount: cartSummary.total.toFixed(2),
|
|
status: 'pending', // Order starts as pending (awaiting mock payment)
|
|
billingAddress,
|
|
})
|
|
.returning()
|
|
|
|
// Create order items with price snapshots
|
|
const orderItemsData = cartSummary.items.map((item) => ({
|
|
orderId: order.id,
|
|
productId: item.productId,
|
|
quantity: item.quantity,
|
|
priceSnapshot: item.product.price, // Snapshot price at time of order
|
|
productSnapshot: {
|
|
name: item.product.name,
|
|
description: item.product.description,
|
|
navProductId: item.product.navProductId,
|
|
category: item.product.category,
|
|
},
|
|
}))
|
|
|
|
await db.insert(orderItems).values(orderItemsData)
|
|
|
|
// Optionally save address to user profile
|
|
if (checkoutData.saveAddress) {
|
|
await db
|
|
.update(users)
|
|
.set({
|
|
salutation: checkoutData.salutation,
|
|
dateOfBirth: new Date(checkoutData.dateOfBirth),
|
|
street: checkoutData.street,
|
|
postCode: checkoutData.postCode,
|
|
city: checkoutData.city,
|
|
countryCode: checkoutData.countryCode,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(users.id, user.id))
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
orderId: order.id,
|
|
orderNumber: order.orderNumber,
|
|
message: 'Bestellung erfolgreich erstellt',
|
|
}
|
|
})
|