
MemberK - Cross-Border Commerce & Rewards PlatformProfessional Work
MemberK is a cross-border commerce and rewards platform launched in Thailand. Members shop a curated catalogue of Korean products and earn rewards they can withdraw as real money — which makes the reward ledger a financial system, not a loyalty widget.
I worked as a Full Stack Developer across the consumer app, the admin console, and the NestJS API — 64 consumer screens, 59 admin screens, and 50 backend modules. The platform is country-configurable so new markets can be enabled without re-architecture, and ships in English, Korean, and Thai.
Full Stack Developer
Payments — Three Rails, One Order Lifecycle
Built Cash on Delivery end to end across backend, admin, and frontend — order management, payment-status tracking, courier restrictions, a remittance panel for reconciling collected cash, and auto-stamped collection on delivery.
Integrated Omise as the Thai gateway, carrying PromptPay QR and app-to-app mobile banking across five banks, restructuring checkout so an order is created only after payment succeeds rather than before.
Built asynchronous payment reconciliation for rails that confirm out of band.
Added webhook signature validation and a payment guard to reject unverified callbacks.
Implemented Reward-covered checkout — a basket paid entirely in rewards completes with no payment method attached.
Refunds & Returns
Replaced separate COD and prepaid refund paths with a unified refund management system in the admin console, with approve/reject actions and locked-step rendering.
Gated prepaid refunds on return arrival, and made a reopened return resolve as one dispute rather than two.
Required a return reason and surfaced refund-rate reporting to operations.
Shipping & Fulfilment — SHIPPOP Integration
Integrated SHIPPOP, Thailand's multi-carrier shipping aggregator — parcel booking, waybill retrieval as PDF, and tracking.
Let members pick a courier at checkout and surfaced that choice to operations, naming both couriers when a booking overrides the member's pick.
Guarded shipment cancellation against a mid-flight booking confirmation, and unstuck cancellation of an uncommitted booking.
Classified provider errors such as
SHIPPOP_MEMBER_NOT_CONFIGUREDinstead of surfacing them as generic failures.
Rewards, Tiers & Referrals
Implemented atomic member balance adjustment with retry handling on tier evaluation — a reward balance that double-counts or silently drops is a financial bug, not a display bug.
Built the membership tier system with admin adjustments, and a deterministic migration that renamed only the ladders wearing the known tier names.
Moved L1/L2 referral rates out of hardcoded values into the reward policy engine, and corrected level-2 attribution when a member joins through a referral code.
Held pending-withdrawal reward across every balance read, so a member cannot spend what is already being withdrawn.
Excluded refund-return entries from issued-reward totals in dashboard reporting.
Built withdrawal flows, approval queues, and CSV export for finance operations.
Phone Verification & OTP
Normalised phone numbers to E.164 with Thai-specific validation, handling unique-constraint violations rather than surfacing them as 500 errors.
Made OTP verification atomic against concurrent changes to the same phone number.
Un-consumed the OTP on a phone-uniqueness race, so a losing request does not burn the user's code.
Wrapped phone verification and the reward it triggers in a single transaction, with safe rollback and original-error preservation across a migration that replaced the uniqueness constraint.
Admin Console & Operations Reporting
Built 59 admin screens and a reusable
AdminListShellwith selectable toggles, applied across every list surface.Replaced late-order banners with clickable KPI tiles that open the queue pre-filtered — late-to-dispatch, late-in-transit, pending withdrawals, and return/refund backlog.
Built reporting with period comparison, refund rate, and server-side CSV export.
Added server-side catalogue CSV export, member and item counts in order exports, and member names in withdrawal exports.
Implemented admin password management, member deletion, and legal-document CRUD on a single-live-version publish workflow.
Fixed a member delete button that deleted on the first click, with no way back.
Catalogue, Cart & Storefront
Built cart, checkout, shop, and search across the consumer app — including persisted line selection so checkout takes only ticked, orderable lines.
Moved inventory to per-variant stock and fixed catalogue query alias collisions.
Displayed published reviews on product detail, and auto-filled Thai banner headlines from product translations.
Added a
pageMetautility and per-page SEO meta tags.
Auth, Notifications & Platform
Distinguished social-account conflicts on email signup, added duplicate-email checks, account lockout handling, and linked suspended social sign-in to support.
Invalidated refresh tokens on account withdrawal and rejected refresh for withdrawn accounts.
Fixed the operator notification feed that stopped at row 50 of 305, and made mark-all-read a single server-side update.
Emailed suspended members when support replies to their appeal.
Wrote migrations that auto-resolve duplicate reward-policy rows instead of failing the deploy.
Localised across English, Korean, and Thai, including the i18n string generator and CI copy-checking allowlists.
Wrote unit and integration tests across balance adjustment, tier evaluation, shipment cancellation, member service, and profile management.
1. Three Payment Rails That Fail Differently
Challenge: Card, PromptPay/mobile banking, and Cash on Delivery are not three options on one flow — they are three different lifecycles. Card confirms synchronously. PromptPay confirms out of band, minutes later, through a webhook. COD confirms days later, in cash, in a courier's hand. The original checkout created the order first and hoped payment followed, which left orphaned orders whenever it did not.
Solution: Inverted the flow so PromptPay and mobile-banking orders are created only after payment succeeds, and built asynchronous reconciliation for the confirmations that arrive later. COD got its own status track with a remittance panel, so finance reconciles collected cash against dispatched parcels rather than trusting the order status.
Result: Each rail settles on its own timeline without the other two inheriting its failure modes.
2. Reward Balances Are Money
Challenge: Rewards on this platform are withdrawable as real currency, so the ledger is a financial system. Adjusting a balance also triggers tier evaluation — and a failure between the two leaves a member half-applied. Separately, a member with a withdrawal pending could still spend the same balance, because reads did not account for money already in flight.
Solution: Made balance adjustment atomic with retry handling around tier evaluation, and held pending-withdrawal amounts across every balance read rather than only the withdrawal screen. Referral rates moved out of hardcoded constants into the reward policy engine, so a rate change is a policy edit rather than a deploy.
3. Races You Cannot See in a Browser
Challenge: Phone verification looks like a form with a code in it. Underneath, the same number can arrive twice at once — a retried request, a double-tapped button, two devices. The naive implementation either burns the user's OTP, awards the signup reward twice, or throws a database constraint error at them.
Solution: Normalised every number to one canonical E.164 format, made verification atomic against concurrent changes, un-consumed the OTP when a race is lost, and wrapped verification and its reward in one transaction. The same reasoning applied to shipment cancellation, which is now guarded against a booking confirmation landing mid-write.
4. A Migration That Cannot Fail the Deploy
Challenge: Two schema changes had to run against live production data that already violated the constraints being added — duplicate reward-policy country rows, and a phone uniqueness constraint over unnormalised numbers. A migration that simply asserts the constraint takes the deploy down.
Solution: Wrote migrations that resolve the conflicting data themselves before applying the constraint, and made the tier rename deterministic — renaming only the ladders wearing the known tier names, so re-running it is safe.
5. Refunds the Gateway Cannot Send
Challenge: Only card charges are reversible through Omise. PromptPay can be neither voided nor refunded, and mobile banking settles off-us — outside the gateway's chain entirely, with nothing for a refund call to act on. An operator carrying over card habits would promise a member a refund the console physically could not send.
Solution: Encoded each rail's refundability in one module the console reads everywhere, so the order list and the detail page cannot drift apart on the answer, and routed non-reversible rails to manual settlement instead of a gateway call that was always going to fail.
Result: The console never offers a refund it cannot complete.
6. An Unfamiliar Regional Stack
Challenge: SHIPPOP for multi-carrier shipping, Omise for payments, Thai SMS delivery — integrations whose English documentation is sparse and whose error codes are undocumented.
Solution: Verified behaviour against the live sandbox rather than trusting the documentation, and built the failure path before the success path — classifying provider errors like SHIPPOP_MEMBER_NOT_CONFIGURED into actionable messages rather than letting them surface as generic 500s.