Skip to content

Phase 3: Shopee Slice (Inbound) — Findings

Status: Complete ✅ (end-to-end cascade re-validated against fresh staging on 2026-05-07; see §5 "Day-3 cascade re-validation") Predecessor: Phase 3 Kickoff Plan · Implementation Plan Exit criterion target: "A real Shopee order lands in Medusa via the connector."

1. Purpose + method

Validate the inbound Shopee connector end-to-end against the merchant's real Shopee shop credentials.

Method:

  1. OAuth-bootstrap the Shopee in-house seller app against staging.
  2. Place a low-value test order on the merchant's Shopee shop (sleeves single, ~$1).
  3. Observe webhook → raw event → Medusa order creation within 5 min.
  4. Drive status updates (ship, complete) and verify Medusa state mirrors.
  5. Confirm escrow row populated post-COMPLETED.
  6. Negative test: temporarily rename the SKU on Medusa side → trigger another order → verify exception + Telegram alert.

2. Connector primitives leaned on

  • @congminh1254/shopee-sdk v1.5.5+ — order, payment, push managers
  • PostgresTokenStorage — survives container restart, no Docker volume needed
  • Shopee Open Platform v2 — webhook signature (HMAC-SHA256 of body), order detail, escrow detail, escrow list, lost push message recovery

3. Custom extensions added

  • src/modules/connectors/shopee/ — full module (service + 4 models + normalizers + mappers + 4 jobs + webhook + 1 subscriber)
  • src/modules/connectors/shared/ConnectorException + Telegram alerting
  • src/modules/tcg/ extended with TcgChannelListing (Phase 2 deferral)
  • 3 Module Links: ShopeeOrderSync ↔ Medusa order, TCGChannelListing → variant, TCGChannelListing → serialized item
  • 6 migrations (5 auto + 1 hand-written CHECK on tcg_channel_listing)

4. Verified scenarios

Validated 2026-04-23 against SG sandbox endpoint openplatform.sandbox.test-stable.shopee.sg. Sandbox does NOT auto-push real test orders, only the manual "Push Test Data" button — operator-driven scenarios use synthesized signed webhooks targeting real ordersn fetched via SDK.

  • ☑ OAuth bootstrap script succeeds; shopee_auth_token row appears (manual exchange via curl — bootstrap script blocked by stale-cache issue at the time, since unblocked by tcg-platform#37)
  • ☑ Webhook signature verification: good signature returns 200 + signature_ok=true
  • ☑ Webhook signature verification: tampered body returns 200 + signature_ok=false + ConnectorException (Telegram alert dropped — SHOPEE_ALERT_WEBHOOK_URL not set in sandbox)
  • ☑ Real test order: synthesized webhook for ordersn 260424KAMFVKAG → Medusa order order_01KPXTPRWJSG59Q7B6XWK9ZRNX created with SKU pokemon-pikachu-paldean-fates-rc01-en-NM-NF-N, sales_channel = "Shopee"
  • ☑ PROCESSED → Medusa fulfillment created with packed_at, inventory level decremented, reservation consumed (re-validated 2026-05-07 on fresh staging — order 260507S3QC5R2V → fulfillment ful_01KR1G5F0EA2QDCJ7WZG6SG5GS packed_at = 15:16:11, inventory 100→99, reservation consumed cleanly. No manual SQL hacks.)
  • ☑ SHIPPED → fulfillment shipped_at updated (re-validated 2026-05-07 — same fulfillment shipped_at = 15:16:42. Previously a silent no-op; closed by tcg-platform#46 which adds shipping_methods to the createOrderWorkflow input.)
  • ☑ TO_CONFIRM_RECEIVE → fulfillment delivered_at updated (re-validated 2026-05-07 — delivered_at = 15:17:16, zero exceptions across the entire cascade.)
  • ☑ Cancellation: cancel on Shopee → Medusa order canceled (validated 2026-05-08 — fresh order 260507S815Y15P/order/cancel_order with cancel_reason: OUT_OF_STOCK + item_list → synthesized CANCELLED push → Medusa order_01KR1HJEY8EP98BKAX0WYERWBJ.canceled_at = 2026-05-08 15:42:01, zero exceptions. See "Day-3 follow-up" below for sandbox quirks discovered.)
  • ⚠ Escrow: completed order produces a ShopeeEscrow row with non-zero escrow_amount (API + auth validated — /payment/get_escrow_detail returned full payload with escrow_amount: 97.77; E2E persistence (COMPLETED-push → ShopeeEscrow row) gated on Shopee auto-release timer which sandbox can't trigger on demand. Production validation backlog item.)
  • ⚠ Lost-push recovery: simulate dropped webhook → orders recovered on next hourly run (code path validated, but /push/get_lost_push_message rejects with "App has not open push" until the Shopee app status flips Live/Published — sandbox apps in "Developing" status can't exercise this. Production validation backlog item.)
  • ⚠ Daily reconciliation: orders dropped from BOTH webhook and lost-push (e.g. >3-day-old) recovered overnight (API + auth validated — /order/get_order_list returned recent orders correctly; job's dynamic import("./sdk.js") resolves cleanly only against the compiled .medusa/server/ build, not against TS source via medusa exec. Will run automatically on its daily cron in production. Production validation backlog item.)
  • ☑ Negative SKU test: unmatched SKU produces exception + alert payload (validated 2026-05-08 — fresh order 260507S8AP64A8 with item 801989015 ("Taipey", item_sku abc123 — no Medusa variant match) → connector_exception cex_01KR1HW853CD6SDPM5GS0SGV78 class sku_not_found, raw_event left processed=false, no Medusa order leaked. Alert payload generated correctly with all expected fields, dropped to stderr because SHOPEE_ALERT_WEBHOOK_URL unset on staging — known.)
  • ☑ Token expiry alert (push code 12): synthesized code-12 webhook → connector_exception row class token_expired with shop_id annotation
  • ☑ Idempotent: 6+ webhook replays for same ordersn → exactly 1 Medusa order in DB
  • (bonus) Real Shopee-signed push verification (validated 2026-05-08 — enabled Push Mechanism on the "Sand Box Test" app in Open Platform → "Verify and Save" the callback URL https://tcg-staging.exzentcg.com/connectors/shopee/webhook (Shopee's verification call returned 200 + signature_ok=true) → "Push Test Data" for code 3 produced raw_event srev_01KR1KH3F9E1X5K3Z4VBDS4WVD signature_ok=true for an actually-Shopee-signed payload. Validates the signature verifier against real Shopee signing, complementing prior synthesized-push validation.)

5. Gaps / concerns surfaced

Fill in during validation. Anticipated candidates:

  • Stub workflow dispatches (fulfill, create_shipment, cancel) need wiring against real Medusa core-flows — initial implementation may surface missing input fields.
  • Shopee's actual rate limits unknown until measured under real load.
  • Escrow available date varies — real test order may take 5-7 days to settle.
  • Customer matching policy on buyer_user_id — verify across orders from the same buyer.

Notable additions discovered during PRs A–E:

  • SDK is ESM-only despite the audit's claim of CJS-only. Discovered during PR B; required dynamic await import("./sdk.js") in any module that exposes business logic to Medusa's CJS build pipeline. Static imports of the SDK from service.ts break medusa build.
  • Jest needs __mocks__/@congminh1254/shopee-sdk.js + moduleNameMapper to stub the ESM-only SDK in its CJS test environment. Pattern lives in jest.config.js and __mocks__/.
  • medusaIntegrationTestRunner env infrastructure is broken locally and in CI: tax_provider/payment_provider tables not created after runModulesMigrations() in the test DB. All HTTP integration tests across PRs B–E are committed but cannot validate locally; CI doesn't run them at all. Phase 9 hardening item to investigate root cause and either fix the runner or add CI integration test job.
  • Module Link from TcgChannelListing → TcgSerializedItem skipped (Phase 3) due to Medusa RemoteJoiner.buildReferences() fieldAlias collision with the existing product-variant-serialized-item.ts link. Relationship captured at model level via serialized_item_id column. Phase 6 outbound code can re-add with custom alias config if needed.
  • Workflow dispatches now wiredcreate_fulfillment / create_shipment / cancel / mark_delivered dispatch real Medusa core-flows. PROCESSED creates a fulfillment; SHIPPED updates shipped_at; TO_CONFIRM_RECEIVE marks delivered; CANCELLED cancels the order (cancelling any non-cancelled fulfillment first). RETRY_SHIP / IN_CANCEL / TO_RETURN remain log_only — Shopee-specific transient states with no Medusa equivalent (sync row's shopee_status still tracks). On-create inventory reservation also added so PROCESSED's updateReservationsStep finds something to consume. (#41, #42, #43.) See "Day-2 lifecycle-wiring discoveries" below for two follow-up gaps surfaced during validation that block SHIPPED end-to-end.
  • The audit's claim of region: 'sg' was wrong — SDK source has only 5 regions; SG uses ShopeeRegion.GLOBAL.
  • medusa db:generate <module> requires a fully-migrated dev DB — otherwise the generator emits a destructive full-schema-sync migration that drops all tables it doesn't see (including other modules' core entity tables). Surfaced when Phase 2's Migration20260423015400.ts for the new tcg_channel_listing model came out as a 1131-line up() that dropped cart/order/customer/tax_provider/etc. on every fresh-DB boot — undetected until the staging Postgres volume was wiped post-Phase-3 deployment, at which point the crashloop with relation "tax_provider" does not exist exposed it. (Fixed in tcg-platform#24; workflow note added to tcg-staging troubleshooting.)

Day-1 sandbox bootstrap discoveries (2026-04-23 validation):

A cluster of issues only surfaced once we tried to actually push real Shopee data through the connector end-to-end. Each was fixed in code so future fresh-DB deploys won't repeat the manual debugging:

  • SHOPEE_PUSH_PARTNER_KEY is a separate secret from SHOPEE_PARTNER_KEY — Shopee Partner Center exposes them under different sections (Push Mechanism vs App Detail). The webhook verifier needs the push key, not the API key. (PR #29; .env now has both.)
  • Webhook signature scheme is HMAC-SHA256(push_key, push_url + "|" + body) — note the literal | separator — and the Authorization header is bare hex (no SHA256 prefix). Both audit and the reference receiver xKeNcHii/shopee-webhook-receiver had the wrong scheme; reverse-engineered via runtime diagnostic. (PR #33.)
  • SHOPEE_PUSH_URL env var must be pinned to the exact URL string Shopee was configured with. Behind Cloudflare Tunnel + NPM, deriving from req.path/req.host headers produces a different URL than what Shopee signed. (Pinning is the operationally correct fix; PR #20 in tasks tracks fixing the derivation fallback as a follow-up.)
  • Medusa bodyParser: { preserveRawBody: true } is required for webhook routes — custom express.raw() middleware loses to Medusa's default JSON parser order, leaving req.rawBody undefined and HMAC verification always failing silently. (PR #30.)
  • Webhook route must dispatch processEvent inline after res.end() — the file-based subscriber under src/subscribers/shopee-raw-event-created.ts doesn't fire on Medusa v2.13.6 (auto-emit event-name mismatch suspected). Inline dispatch in route is more reliable, same fast-ack behaviour. (PR #36; subscriber file kept for future polling-job emit paths.)
  • shop_id must be passed as plain Number, not BigInt — Medusa's bigNumber field setter throws "Cannot set value " on BigInt(...). (PR #26.)
  • extractOrdersn must handle data as nested object, not just JSON-stringified string — real Shopee push code-3 payloads send data as object form. (PR #35.)
  • SKU normalizer must fall back to item_sku when model_sku is empty — listings without variations come back from Shopee with empty model_sku and the real SKU on item_sku (model_id=0). All Phase 2 seed products are single-variant. (PR #35.)
  • SDK is ESM-only and must be loaded via dynamic import() inside an async function — static import at module top transpiles to CJS require() which fails at runtime with No "exports" main defined. Dynamic import() of literal string also gets hoisted by some toolchains; works as written here because TypeScript's CJS emit preserves it inside the async function body. (PR #37.)
  • SG sandbox base URL is https://openplatform.sandbox.test-stable.shopee.sg/api/v2 — NOT the SDK's TEST_GLOBAL constant which points at partner.test-stable.shopeemobile.com. SHOPEE_API_BASE_URL env override per the SDK's setup guide. Filed SDK issue #157 about the README documenting 13 region constants but only 5 shipping. (PR #34.)
  • OAuth auth_partner URL must use the correct region-specific host even for OAuth. Same TEST_GLOBAL vs SG-sandbox URL gotcha applies — got Wrong sign on the global host because the sandbox partner_id doesn't exist there.
  • Cloudflare Access must have a Bypass policy for /connectors/*. Otherwise webhook POSTs get bounced to Cloudflare login and never reach our origin. (Solved via dashboard — added separate Application above the catch-all admin app with Bypass policy for path /connectors/*. Documented in tcg-staging.md follow-up.)
  • SG region with SGD currency must exist before any order workflow runs. Medusa's data-migration scripts seed currencies + countries but no regions. Without one, addToCartWorkflow fails with Cannot read properties of null (reading 'id'). (PR #38 — folded into seed-phase-2.)
  • Every variant needs at least one inventory_level row before order workflow can fulfill. Phase 2 product creation makes inventory_items but not inventory_levels. (PR #38 — seed-phase-2 now creates 100 units at Warehouse for every variant.)
  • Auto-created sales channels need explicit linking to stock locations + products — Shopee SC isn't linked to any location/product on creation. PR #38 auto-links Shopee SC to all stock locations on first creation; product linkage stays operator-driven (since not every Medusa product belongs on Shopee). Follow-up gotcha: PR #38's auto-link runs only on first creation. On staging, the "Shopee" SC already existed from a pre-#38 deploy with zero location links, so the auto-link never ran — Medusa rejected the first real inbound order with "Sales channel ... is not associated with any stock location for variant ...". Unblocked tactically via admin UI (Settings → Sales Channels → Shopee → Locations → link Warehouse + Store + Event-A). PR #40 makes the check defensive: if an existing SC has zero location links, auto-link anyway. (tcg-platform#40.)

Day-2 lifecycle-wiring discoveries (2026-04-25 validation):

After #41 wired PROCESSED/SHIPPED/CANCELLED/TO_CONFIRM_RECEIVE workflows, #42 added on-create inventory reservation, and #43 fixed the items.detail.quantity graph traversal, a second bootstrap-style cluster surfaced when running PROCESSED → SHIPPED → … against staging order #1:

  • Medusa v2 splits line-item quantity onto a junction. order.items.quantity is undefined at the GraphQL graph level — the actual integer lives on order.items.detail.quantity (the order_item entity that joins order_line_item to order). All connector graph queries that derive fulfillment / shipment / reservation quantities had to traverse items.detail.quantity and read Number(it.detail?.quantity ?? 0). Symptom before fix: createOrderFulfillmentWorkflow errored with "FulfillmentItem.quantity is required, 'undefined' found". (tcg-platform#43.)
  • createOrderFulfillmentWorkflow requires a pre-existing reservation_item row for every line item. Its internal updateReservationsStep consumes the reservation rather than creating one, so any order created before #42 reserved on create gets "No stock reservation found for item ordli_…" on PROCESSED. Code-side fix is in #42; pre-existing orders need a one-shot reservation_item backfill. Follow-up gap: ensureMedusaFulfillment does not defensively re-create a reservation if one is missing — operator runbook needs to call createReservationsWorkflow (or equivalent SQL) for any inflight order that predates the deploy.
  • Reservation location mismatch. getDefaultShopeeLocationId picks the first stock-location linked to the Shopee SC, ordered by id. On staging that resolves to "Store" (id sloc_01KPWSWNZC4F1ETN1NSJFYWNC5) — but the Phase-2 seed only stocks "Warehouse". Reservation lands at a location with no inventory_level row, then createOrderFulfillmentWorkflow fails with "Inventory level for item … and location … not found". Two viable fixes (need decision): (a) extend seed-phase-2 to stock every Shopee-linked location, or (b) add an env var SHOPEE_DEFAULT_LOCATION_ID (with a fallback that prefers locations holding inventory for the item). (b) is more product-correct for multi-location operations later. Tracked as TBD in Phase 3 follow-ups.
  • createOrderWorkflow input has no shipping_methods → silent SHIPPED no-op. When service.ts calls createOrderWorkflow, the input only includes currency_code / email / sales_channel_id / shipping_address / billing_address / items / metadata. With no shipping_methods, no order_shipping row is created. Later, on a SHIPPED push, createOrderShipmentWorkflow → registerOrderShipmentStep → orderModule.registerShipment(...) runs but quietly produces no fulfillment update — fulfillment.shipped_at stays NULL, and no exception is recorded (raw event is marked processed=true). Fix needed: before calling createOrderWorkflow, resolve a shipping option for the chosen Shopee location (Phase 2 seed creates a free "Standard Shipping" SO), then pass shipping_methods: [{ shipping_option_id, name, amount: 0, data: {} }] in the workflow input. Without this, the lifecycle stops at PROCESSED.
  • Workflow alert names ride on connector_exception not on logs. During this run we relied on select … from connector_exception to find what dispatchToMedusa swallowed; the docker-compose logs only emit [connector-alert] SHOPEE_ALERT_WEBHOOK_URL not set — alert dropped for the thrown path. The "ran clean but did nothing useful" path (e.g. SHIPPED above) leaves no trace at all. Operability note: any silent no-op from a Medusa core-flow needs an explicit post-condition assertion in our wrapper (e.g. re-query the fulfillment after createOrderShipmentWorkflow and throw if shipped_at is still null). Tracked as a Phase-9 hardening item.

Day-3 cascade re-validation (2026-05-07):

The two day-2 SHIPPED-blocking gaps are closed and the full cascade now runs end-to-end on the natural code path with no manual SQL hacks. PRs that landed between day-2 and day-3:

  • tcg-platform#46 — env-driven location pick (SHOPEE_DEFAULT_LOCATION_ID), shipping-option resolver (SHOPEE_DEFAULT_SHIPPING_OPTION_ID with location-scoped fallback), shipping_methods injected into createOrderWorkflow input, plus seed-phase-2 extension to bootstrap a "Shopee Domestic" fulfillment set + Singapore service zone + free "Shopee Order" SO.
  • tcg-platform#48 — fix: Medusa enforces stock_location ↔ fulfillment_set as 1:1; seed link FS to one location only (Warehouse), not loop over all.
  • tcg-platform#49 — fix: enable manual_manual fulfillment provider on the Shopee location (separate STOCK_LOCATION ↔ fulfillment_provider_id link). Without it, createShippingOptionsWorkflow rejects the SO at validation.
  • tcg-platform#50 — fix: wire the new env vars into docker-compose.yml's environment: block (PR #46 added them to .env.example only).

Validation method: clean staging rebuild (docker compose down -v && up -d --build), fresh seed via yarn medusa exec ./src/scripts/seed-phase-2.ts, fresh OAuth bootstrap, fresh sandbox order created via Open Platform Test Order tool (ordersn 260507S3QC5R2V), Shopee state advanced via /order/ship_order API call (with address_id + pickup_time_id resolved from /order/get_shipping_parameter first) and Test Order tool's Pickup/Deliver buttons, synthesized webhook pushes (code 3) at each transition.

End state on ful_01KR1G5F0EA2QDCJ7WZG6SG5GS:

Gate Timestamp
packed_at 2026-05-07 15:16:11
shipped_at 2026-05-07 15:16:42
delivered_at 2026-05-07 15:17:16
connector_exception rows produced 0

SHOPEE_DEFAULT_LOCATION_ID is pinned to Warehouse on staging. SHOPEE_DEFAULT_SHIPPING_OPTION_ID left unset — the location-scoped fallback resolves correctly via the FS link the seed creates.

Operational notes from this run:

  • Shopee's sandbox ship_order requires pickup.address_id and pickup.pickup_time_id from get_shipping_parameter's info_needed.pickup array — not the address_id: 0, pickup_time_id: "" defaults. Generic error_server if you skip the discovery call.
  • After ship_order succeeds, the sandbox order stays at READY_TO_SHIP for ~30s before auto-advancing to PROCESSED. The Test Order tool's Pickup button stays disabled until that auto-advance happens; clicking it then transitions to SHIPPED. Deliver transitions SHIPPED → TO_CONFIRM_RECEIVE.
  • Webhook URL on staging: https://tcg-staging.exzentcg.com/connectors/shopee/webhook — Cloudflare Access bypass on /connectors/* (configured during phase 3 day-1) is essential.

Day-3 follow-up validations (2026-05-08 evening):

After flipping status to "Complete", noticed §4 still had ☐ items beyond the cascade. Powered through the remaining checklist:

  • Cancellation (full E2E pass) — Created fresh order 260507S815Y15P. Sandbox quirks discovered: cancel_reason: CUSTOMER_REQUEST rejects with "cancel reason is invalid"; cancel_reason: OUT_OF_STOCK without item_list rejects with "no item ids are given". Working call: { order_sn, cancel_reason: "OUT_OF_STOCK", item_list: [{ item_id, model_id: 0 }] } → success → synthesized CANCELLED push → Medusa order_01KR1HJEY8EP98BKAX0WYERWBJ.canceled_at populated, zero exceptions.
  • Negative SKU + alert payload (full E2E pass) — Fresh order 260507S8AP64A8 with item 801989015 ("Taipey", item_sku=abc123 — no Medusa match) → connector_exception cex_01KR1HW853CD6SDPM5GS0SGV78 class sku_not_found with descriptive message including ordersn; raw_event processed=false; no Medusa order leaked. Alert payload generated with all expected fields (connector, error_class, message, exception_id, raw_event_id, occurred_at), dropped to stderr because SHOPEE_ALERT_WEBHOOK_URL unset on staging — known.
  • Real Shopee-signed push verification (bonus) — Enabled Push Mechanism on the "Sand Box Test" app in Open Platform → set callback URL to https://tcg-staging.exzentcg.com/connectors/shopee/webhook → "Verify and Save" succeeded (Shopee's verification call came back signature_ok=true on our side). Then "Push Test Data" for code 3 (order_status_push) produced raw_event srev_01KR1KH3F9E1X5K3Z4VBDS4WVD with signature_ok=true for an actually-Shopee-signed payload. Confirms the signature verifier works for real Shopee signing, not just our synthesized pushes. Connector also recorded cex_01KR1KH3MMHHGHK693KVQ2KP2V (class unknown, "getOrdersDetail failed: error_not_found") for the fake test-data ordersn — error path exercised correctly, though the unknown class is a phase-9 hardening candidate (see below).
  • Escrow API path (partial — API validated, E2E persistence deferred) — /payment/get_escrow_detail against shop 226349641 with order 260507S3QC5R2V returned full payload with escrow_amount: 97.77, commission_fee: 2.18, item match (item_sku: pokemon-pikachu-paldean-fates-rc01-en-NM-NF-N). Auth + endpoint + parsing path are working. Full E2E (COMPLETED-push → ShopeeEscrow row persistence) requires Shopee state to advance TO_CONFIRM_RECEIVE → COMPLETED, which is gated on Shopee's auto-release timer (~7 days in real Shopee, no manual trigger in sandbox).
  • Lost-push recovery API (blocked by sandbox app status) — /push/get_lost_push_message rejects with "App has not open push" even after Test Push URL verification. Requires the app to be Live/Published on Shopee (currently "Developing" status), which needs Shopee's app review process. Code path is correct; environmental block, not a code defect.
  • Daily reconciliation API (partial — API validated, job invocation gated on build) — /order/get_order_list against the 7-day window returned the 3 orders we created today, validating auth + endpoint + parsing. Direct invocation of the job via medusa exec ./src/jobs/... failed with "Cannot find module '/server/src/modules/connectors/shopee/sdk.js'" — the dynamic import("./sdk.js") resolves only against the compiled .medusa/server/ build, not against TS source via medusa exec. In production, Medusa's scheduled-job runner uses the compiled output and the import resolves correctly. The job will fire on its daily cron in production.

Phase-9 hardening candidates surfaced today:

  • Connector classifies error_not_found (Shopee returned "the order is not found." from get_order_detail) as error_class: unknown. Should get a dedicated class (shopee_order_not_found or similar) so operators can filter for it cleanly.
  • Post-condition assertions on Medusa core-flow calls (already noted in day-2 findings; still standing).

Production validation backlog (non-blocking):

Item Trigger to revalidate
Escrow E2E persistence (COMPLETED → ShopeeEscrow row) First production order that auto-completes (real buyers confirm receipt or 7-day auto-release fires) — happens organically once live traffic begins.
Lost-push recovery hourly job After "Sand Box Test" app status flips Live/Published on Shopee Open Platform — OR validate operationally in production once the app is live there.
Daily reconciliation cron After the first daily cron tick on production deploy — observe the job runs, no exceptions, log output sane.

6. Phase 3 decisions recorded

  • Inbound only. Outbound (price / stock push) deferred to Phase 6 alongside Lazada per project plan.
  • Push code matrix. Codes 3 (order status), 4 (tracking), 12 (auth expiry) actioned; codes 1/2/5–11/13 ack + log only. Code 99 observed in the wild as Shopee's webhook connectivity self-test / liveness probeshop_id: 999999 with {selftest: true|2} from Shopee's side plus an empty-data variant from the real shop. extractOrdersn correctly returns null; connector stores raw + returns 200, nothing else to do.
  • SKU mapping by direct match (model_sku === variant.sku). No TCGChannelListing lookup for inbound.
  • TCGChannelListing populated during inbound (cheap upsert), used for outbound in Phase 6.
  • All-failures-alert policy. Operator can filter at n8n later if noise warrants.
  • PostgresTokenStorage instead of SDK file storage (avoids sync I/O blocking event loop).
  • getLostPushMessage instead of generic order-list polling for the hourly backstop.
  • Audit-driven corrections captured in implementation plan: region GLOBAL not 'sg', HMAC of body only not url+body, escrow field renames, INVOICE_PENDING removed, etc.

7. Open questions for Phase 4+

  • ~~getDefaultShopeeLocationId policy~~ — Resolved 2026-05-07 via PR #46: SHOPEE_DEFAULT_LOCATION_ID env var with a fallback to "first SC-linked location" if unset. Multi-warehouse operations later set the env var explicitly per deploy.
  • ~~Shipping-method assignment policy~~ — Resolved 2026-05-07 via PR #46: SHOPEE_DEFAULT_SHIPPING_OPTION_ID env override + location-scoped lookup fallback (via fulfillment_set → service_zone → shipping_options). Phase 6 outbound work can revisit if per-channel SO mapping becomes a requirement.
  • Defensive reservation in ensureMedusaFulfillment — should it re-create a reservation if one is missing (belt-and-braces with PR #42 on-create reservation), or trust the on-create path and surface a clear error otherwise? Trade-off: silent self-heal vs. surfacing a real bug.
  • Post-condition assertions on Medusa workflow calls — multiple core-flows return successfully without performing the expected mutation when input is incomplete (SHIPPED / register-shipment was the smoking gun). Add wrapper-level "did it actually happen" checks? (Phase-9 hardening candidate.)
  • Phase 4 (DCA) — does escrow_amount alone capture everything DCA needs, or are sub-fields (final_shipping_fee, seller_return_refund, coins, etc.) also required?
  • TCGChannelListing outbound shape — confirm in Phase 6 design.