Built the entire Korean payment flow in one session, shipped receipt emails for both Toss and Paddle, cleaned up a messy design system — then spotted that a single config value had been wrong the whole time, silently routing all Korean users to an unconfigured Paddle endpoint.
The fix was one line. The miss was embarrassing.
Toss Payments: full integration
FateSaju is a Korean fortune-telling (사주, Four Pillars) app with a dual payment structure: Toss Payments for Korea, Paddle for the rest of the world. Toss is the dominant payment method in Korea — think Stripe but deeply embedded in the Korean banking and mobile ecosystem.
The previous QA session had temporarily routed Korean users to Paddle because Toss wasn't implemented yet. This session was about doing it properly.
The prompt given to Claude covered the entire surface area at once:
"Add Toss payment widget page
/checkout/toss, success/fail redirect pages,/api/checkout/toss/confirmfor server-side verification, updateusePaywallto route Korean users to Toss, add business registration info to the footer across all 8 locales."
It came back in one pass: SDK install, widget rendering, HMAC server verification, success/fail flow. Many files changed, but each individual delta was small and easy to review.
- Payment widget`/checkout/toss` — receives amount + orderName params, renders Toss widget
- Server verification`/api/checkout/toss/confirm` — double-checks paymentKey + amount server-side
- Success/fail pageshandles Toss redirect URLs, locale-specific error display on failure
- Business footer8 locales — company name, representative, business registration number, address
Toss webhook + receipt email
Payment confirmation needed two paths: the synchronous confirm API call the user triggers directly, and the async webhook Toss pushes server-to-server. Both need to fire the receipt email.
The webhook uses HMAC-SHA-256 signature verification with a TOSS_WEBHOOK_SECRET env var. Receipt email was wired into Toss confirm, Toss webhook, and Paddle webhook — all three paths covered.
sendReceiptEmail.ts is 326 lines. Eight locales, payment amount/order name/date table, branded HTML template, customer email, multilingual CTA. Claude generated it in one shot with no missing i18n keys.
sendComingSoonEmail.ts went in the same session — subscription confirmation for the early access list. Eight locales, early-bird discount promise, launch notification confirmation.
Design system cleanup
The codebase had five font families in active use: Pretendard, Outfit, Sora, Manrope, Italiana — mixed inconsistently across globals.css. Consolidated to two: Pretendard for Korean body text, Outfit for display/headings. Removed the rest.
Along the way: unified button border-radius to 14px, defined a proper typography scale (h1–h3, body, button, input), cleaned up glassmorphism card styles.
| Item | Before | After |
|---|---|---|
| Font families | 5 (Pretendard, Outfit, Sora, Manrope, Italiana) | 2 (Pretendard, Outfit) |
| Button radius | mixed | 14px across the board |
| Typography scale | ad-hoc values | h1–h3, body, button, input defined |
| Contact email | inconsistent across 8 locale files | dbswn428@gmail.com unified |
The one-line bug that killed Korean checkout
After shipping all of the above, English-locale browsers (/en/) were hitting 500 errors on checkout. Root cause: /en/ routes to Paddle, but PADDLE_API_KEY wasn't set in the environment. Fixed with a fallback — if Paddle isn't configured, the endpoint delegates to the Toss create flow internally.
While debugging that, a worse problem surfaced.
In packages/shared/src/config/countries.ts, there's a per-country config object. Korea's entry:
// packages/shared/src/config/countries.ts
paymentProvider: "paddle", // ← this was the problem
The previous QA session (2026-03-10) had a commit that deliberately set this to "paddle" — at the time, Toss wasn't implemented and Korean routing was going to Toss, which was broken. Correct fix for that moment. But this session implemented Toss end-to-end, and the countries.ts restoration got missed. So every Korean user was being routed to Paddle, which had no API key, and getting a 500.
The fix:
paymentProvider: "toss", // ← restored
One character changed. Committed with (critical) in the message because it genuinely was.
7feature commits (excl. merges)
72files changed
2,591lines added
1lines in the critical fix
2new email templates
8locales covered
Commit log
2c58ca3 feat: add Toss Payments integration and business info footer eb3b125 feat: add Toss webhook, payment receipt email (8-locale i18n) 5f7ef27 feat: add coming-soon confirmation email, fix checkout test 6646720 디자인 시스템 통일 및 이메일 업데이트 9d5e4e9 debug: expose checkout error message for diagnosing DB connection issue 099a202 fix: fallback to Toss when Paddle API key not configured 8704207 fix: restore Korean paymentProvider to toss (critical)
Three debug commits at the end of a session is a pattern worth watching. Larger features touching more files create more surface area for "restoration" bugs — where a temporary config change from a previous session gets left behind after the full implementation lands. Next time a big feature branch merges, explicitly diff any config files that were touched across both sessions.
📌 Originally published at Jidong Lab
More AI news and dev logs → jidonglab.com
Top comments (0)