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:
- OAuth-bootstrap the Shopee in-house seller app against staging.
- Place a low-value test order on the merchant's Shopee shop (sleeves single, ~$1).
- Observe webhook → raw event → Medusa order creation within 5 min.
- Drive status updates (ship, complete) and verify Medusa state mirrors.
- Confirm escrow row populated post-COMPLETED.
- Negative test: temporarily rename the SKU on Medusa side → trigger another order → verify exception + Telegram alert.
2. Connector primitives leaned on¶
@congminh1254/shopee-sdkv1.5.5+ — order, payment, push managersPostgresTokenStorage— 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 alertingsrc/modules/tcg/extended withTcgChannelListing(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_tokenrow 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_URLnot set in sandbox) - ☑ Real test order: synthesized webhook for ordersn
260424KAMFVKAG→ Medusa orderorder_01KPXTPRWJSG59Q7B6XWK9ZRNXcreated with SKUpokemon-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 — order260507S3QC5R2V→ fulfillmentful_01KR1G5F0EA2QDCJ7WZG6SG5GSpacked_at = 15:16:11, inventory 100→99, reservation consumed cleanly. No manual SQL hacks.) - ☑ SHIPPED → fulfillment
shipped_atupdated (re-validated 2026-05-07 — same fulfillmentshipped_at = 15:16:42. Previously a silent no-op; closed by tcg-platform#46 which addsshipping_methodsto thecreateOrderWorkflowinput.) - ☑ TO_CONFIRM_RECEIVE → fulfillment
delivered_atupdated (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_orderwithcancel_reason: OUT_OF_STOCK+item_list→ synthesized CANCELLED push → Medusaorder_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
ShopeeEscrowrow with non-zeroescrow_amount(API + auth validated —/payment/get_escrow_detailreturned full payload withescrow_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_messagerejects 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_listreturned recent orders correctly; job's dynamicimport("./sdk.js")resolves cleanly only against the compiled.medusa/server/build, not against TS source viamedusa 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
260507S8AP64A8with item 801989015 ("Taipey", item_skuabc123— no Medusa variant match) →connector_exceptioncex_01KR1HW853CD6SDPM5GS0SGV78classsku_not_found, raw_event leftprocessed=false, no Medusa order leaked. Alert payload generated correctly with all expected fields, dropped to stderr becauseSHOPEE_ALERT_WEBHOOK_URLunset on staging — known.) - ☑ Token expiry alert (push code 12): synthesized code-12 webhook →
connector_exceptionrow classtoken_expiredwith 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_eventsrev_01KR1KH3F9E1X5K3Z4VBDS4WVDsignature_ok=truefor 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 fromservice.tsbreakmedusa build. - Jest needs
__mocks__/@congminh1254/shopee-sdk.js+moduleNameMapperto stub the ESM-only SDK in its CJS test environment. Pattern lives injest.config.jsand__mocks__/. medusaIntegrationTestRunnerenv infrastructure is broken locally and in CI:tax_provider/payment_providertables not created afterrunModulesMigrations()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 → TcgSerializedItemskipped (Phase 3) due to MedusaRemoteJoiner.buildReferences()fieldAlias collision with the existingproduct-variant-serialized-item.tslink. Relationship captured at model level viaserialized_item_idcolumn. Phase 6 outbound code can re-add with custom alias config if needed. - Workflow dispatches now wired —
create_fulfillment/create_shipment/cancel/mark_delivereddispatch real Medusa core-flows. PROCESSED creates a fulfillment; SHIPPED updatesshipped_at; TO_CONFIRM_RECEIVE marks delivered; CANCELLED cancels the order (cancelling any non-cancelled fulfillment first). RETRY_SHIP / IN_CANCEL / TO_RETURN remainlog_only— Shopee-specific transient states with no Medusa equivalent (sync row'sshopee_statusstill tracks). On-create inventory reservation also added so PROCESSED'supdateReservationsStepfinds 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 usesShopeeRegion.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'sMigration20260423015400.tsfor the newtcg_channel_listingmodel came out as a 1131-lineup()that droppedcart/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 withrelation "tax_provider" does not existexposed 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_KEYis a separate secret fromSHOPEE_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 (noSHA256prefix). Both audit and the reference receiverxKeNcHii/shopee-webhook-receiverhad the wrong scheme; reverse-engineered via runtime diagnostic. (PR #33.) SHOPEE_PUSH_URLenv var must be pinned to the exact URL string Shopee was configured with. Behind Cloudflare Tunnel + NPM, deriving fromreq.path/req.hostheaders 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 — customexpress.raw()middleware loses to Medusa's default JSON parser order, leavingreq.rawBodyundefined and HMAC verification always failing silently. (PR #30.) - Webhook route must dispatch
processEventinline afterres.end()— the file-based subscriber undersrc/subscribers/shopee-raw-event-created.tsdoesn'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_idmust be passed as plain Number, not BigInt — Medusa'sbigNumberfield setter throws "Cannot set value" on BigInt(...). (PR #26.)extractOrdersnmust handledataas nested object, not just JSON-stringified string — real Shopee push code-3 payloads senddataas object form. (PR #35.)- SKU normalizer must fall back to
item_skuwhenmodel_skuis empty — listings without variations come back from Shopee with emptymodel_skuand the real SKU onitem_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 — staticimportat module top transpiles to CJSrequire()which fails at runtime withNo "exports" main defined. Dynamicimport()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'sTEST_GLOBALconstant which points atpartner.test-stable.shopeemobile.com.SHOPEE_API_BASE_URLenv 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_partnerURL must use the correct region-specific host even for OAuth. Same TEST_GLOBAL vs SG-sandbox URL gotcha applies — gotWrong signon 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,
addToCartWorkflowfails withCannot read properties of null (reading 'id'). (PR #38 — folded into seed-phase-2.) - Every variant needs at least one
inventory_levelrow 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.quantityisundefinedat the GraphQL graph level — the actual integer lives onorder.items.detail.quantity(theorder_itementity that joinsorder_line_itemtoorder). All connector graph queries that derive fulfillment / shipment / reservation quantities had to traverseitems.detail.quantityand readNumber(it.detail?.quantity ?? 0). Symptom before fix:createOrderFulfillmentWorkflowerrored with "FulfillmentItem.quantity is required, 'undefined' found". (tcg-platform#43.) createOrderFulfillmentWorkflowrequires a pre-existingreservation_itemrow for every line item. Its internalupdateReservationsStepconsumes 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-shotreservation_itembackfill. Follow-up gap:ensureMedusaFulfillmentdoes not defensively re-create a reservation if one is missing — operator runbook needs to callcreateReservationsWorkflow(or equivalent SQL) for any inflight order that predates the deploy.- Reservation location mismatch.
getDefaultShopeeLocationIdpicks the first stock-location linked to the Shopee SC, ordered by id. On staging that resolves to "Store" (idsloc_01KPWSWNZC4F1ETN1NSJFYWNC5) — but the Phase-2 seed only stocks "Warehouse". Reservation lands at a location with noinventory_levelrow, thencreateOrderFulfillmentWorkflowfails with "Inventory level for item … and location … not found". Two viable fixes (need decision): (a) extendseed-phase-2to stock every Shopee-linked location, or (b) add an env varSHOPEE_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. createOrderWorkflowinput has noshipping_methods→ silent SHIPPED no-op. Whenservice.tscallscreateOrderWorkflow, the input only includescurrency_code / email / sales_channel_id / shipping_address / billing_address / items / metadata. With noshipping_methods, noorder_shippingrow 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 markedprocessed=true). Fix needed: before callingcreateOrderWorkflow, resolve a shipping option for the chosen Shopee location (Phase 2 seed creates a free "Standard Shipping" SO), then passshipping_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_exceptionnot on logs. During this run we relied onselect … from connector_exceptionto find whatdispatchToMedusaswallowed; the docker-compose logs only emit[connector-alert] SHOPEE_ALERT_WEBHOOK_URL not set — alert droppedfor 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 aftercreateOrderShipmentWorkflowand throw ifshipped_atis 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_IDwith location-scoped fallback),shipping_methodsinjected intocreateOrderWorkflowinput, plusseed-phase-2extension to bootstrap a "Shopee Domestic" fulfillment set + Singapore service zone + free "Shopee Order" SO. - tcg-platform#48 — fix: Medusa enforces
stock_location ↔ fulfillment_setas 1:1; seed link FS to one location only (Warehouse), not loop over all. - tcg-platform#49 — fix: enable
manual_manualfulfillment provider on the Shopee location (separateSTOCK_LOCATION ↔ fulfillment_provider_idlink). Without it,createShippingOptionsWorkflowrejects the SO at validation. - tcg-platform#50 — fix: wire the new env vars into
docker-compose.yml'senvironment:block (PR #46 added them to.env.exampleonly).
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_orderrequirespickup.address_idandpickup.pickup_time_idfromget_shipping_parameter'sinfo_needed.pickuparray — not theaddress_id: 0, pickup_time_id: ""defaults. Genericerror_serverif you skip the discovery call. - After
ship_ordersucceeds, the sandbox order stays atREADY_TO_SHIPfor ~30s before auto-advancing toPROCESSED. The Test Order tool'sPickupbutton stays disabled until that auto-advance happens; clicking it then transitions toSHIPPED.DelivertransitionsSHIPPED → 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_REQUESTrejects with "cancel reason is invalid";cancel_reason: OUT_OF_STOCKwithoutitem_listrejects 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 → Medusaorder_01KR1HJEY8EP98BKAX0WYERWBJ.canceled_atpopulated, zero exceptions. - Negative SKU + alert payload (full E2E pass) — Fresh order
260507S8AP64A8with item 801989015 ("Taipey",item_sku=abc123— no Medusa match) →connector_exceptioncex_01KR1HW853CD6SDPM5GS0SGV78classsku_not_foundwith descriptive message including ordersn; raw_eventprocessed=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 becauseSHOPEE_ALERT_WEBHOOK_URLunset 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 backsignature_ok=trueon our side). Then "Push Test Data" for code 3 (order_status_push) produced raw_eventsrev_01KR1KH3F9E1X5K3Z4VBDS4WVDwithsignature_ok=truefor an actually-Shopee-signed payload. Confirms the signature verifier works for real Shopee signing, not just our synthesized pushes. Connector also recordedcex_01KR1KH3MMHHGHK693KVQ2KP2V(classunknown, "getOrdersDetail failed: error_not_found") for the fake test-data ordersn — error path exercised correctly, though theunknownclass is a phase-9 hardening candidate (see below). - Escrow API path (partial — API validated, E2E persistence deferred) —
/payment/get_escrow_detailagainst shop 226349641 with order260507S3QC5R2Vreturned full payload withescrow_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_messagerejects 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_listagainst the 7-day window returned the 3 orders we created today, validating auth + endpoint + parsing. Direct invocation of the job viamedusa exec ./src/jobs/...failed with "Cannot find module '/server/src/modules/connectors/shopee/sdk.js'" — the dynamicimport("./sdk.js")resolves only against the compiled.medusa/server/build, not against TS source viamedusa 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." fromget_order_detail) aserror_class: unknown. Should get a dedicated class (shopee_order_not_foundor 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 probe —
shop_id: 999999with{selftest: true|2}from Shopee's side plus an empty-datavariant from the real shop.extractOrdersncorrectly returns null; connector stores raw + returns 200, nothing else to do. - SKU mapping by direct match (
model_sku === variant.sku). NoTCGChannelListinglookup for inbound. TCGChannelListingpopulated 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).
getLostPushMessageinstead 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+¶
- ~~
getDefaultShopeeLocationIdpolicy~~ — Resolved 2026-05-07 via PR #46:SHOPEE_DEFAULT_LOCATION_IDenv 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_IDenv override + location-scoped lookup fallback (viafulfillment_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_amountalone 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.
Links¶
- Phase 3 Kickoff Plan
- Phase 3 Implementation Plan
tcg-stagingrunbook- Audit report (internal — not a public artefact)
@congminh1254/shopee-sdk