The checkout API truncates the message and catches idempotency as a typed error
The checkout route creates a Stripe PaymentIntent with five metadata fields — productId, productName, customerName, customerEmail, and customerMessage. Those metadata values travel with the PaymentIntent through the entire Stripe lifecycle. The webhook reads them later to build the confirmation email without touching the database. The customer message is free-form text from a textarea with a 500-character maxLength on the frontend, but the API enforces the same limit server-side with String(message).slice(0, 500). Stripe metadata values have a 500-character ceiling. If a request somehow bypasses the frontend validation — a direct API call, a modified DOM — the slice catches it before Stripe rejects the payload. The idempotency key comes from the CheckoutWizard, which generates a crypto.randomUUID on mount and only regenerates it when the customer changes their name, email, or message. Same details, same key, same PaymentIntent — Stripe deduplicates the request. Different details, new key, new PaymentIntent. The catch block handles the case where two requests arrive with different details but the same key. Instead of a generic 500 error, the route checks whether err.type equals StripeIdempotencyError and returns a 409 with the message Payment already processing. The CheckoutWizard does not show a retry button for a 409 — it tells the user to wait. A specific error type, a specific status code, a specific user message. The route validates the product too. It finds the product by ID in the catalog and calls isPurchasable to confirm it has a fixed price. A quote-only product like a web application build cannot reach the PaymentIntent creation because the guard returns a 400 before the Stripe call happens.
Comments coming soon
Sign in with TikTok to leave a comment. Coming soon.