const express = require('express') const bodyParser = require('body-parser') const fetch = require('node-fetch') const crypto = require('crypto') const PORT = process.env.PORT || 7201 const SUPA_URL = (process.env.SUPA_URL || process.env.SUPA_URL_OVERRIDE || '').replace(/\/$/, '') const SUPA_KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || '' const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '' // optional HMAC secret function supaFetch(path, opts = {}) { const url = `${SUPA_URL}/rest/v1/${path}` const headers = Object.assign({}, opts.headers || {}, { apikey: SUPA_KEY, Authorization: `Bearer ${SUPA_KEY}`, Accept: 'application/json' }) return fetch(url, Object.assign({}, opts, { headers })) } function computeSignature(bodyText, ts) { if (!WEBHOOK_SECRET) return '' const h = crypto.createHmac('sha256', WEBHOOK_SECRET) h.update(bodyText + (ts || '')) return h.digest('hex') } async function upsertRaw(payload, tracking_no, carrier, signature_valid) { try { const body = { carrier: carrier || null, tracking_no: tracking_no || null, body: payload, received_at: new Date().toISOString(), signature_valid: signature_valid } const resp = await supaFetch('platform_express_event_raw', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) return resp } catch (e) { console.warn('upsertRaw error', e) return null } } async function findWaybillId(tracking_no, order_no) { try { if (tracking_no) { const r = await supaFetch(`platform_express_waybills?tracking_no=eq.${encodeURIComponent(tracking_no)}`) if (r.ok) { const data = await r.json() if (data && data.length > 0) return data[0].id } } if (order_no) { const r2 = await supaFetch(`platform_express_waybills?order_no=eq.${encodeURIComponent(order_no)}`) if (r2.ok) { const data2 = await r2.json() if (data2 && data2.length > 0) return data2[0].id } } return null } catch (e) { console.warn('findWaybillId error', e) return null } } function mapStatus(inStatus) { let s = 'IN_TRANSIT' if (!inStatus) return s const v = String(inStatus).toUpperCase() if (['GOT','SEND','TRANSIT'].indexOf(v) > -1) s = 'IN_TRANSIT' else if (v === 'SENT') s = 'OUT_FOR_DELIVERY' else if (v === 'PICKUP') s = 'READY_FOR_PICKUP' else if (v === 'SIGNED' || v === 'DELIVERED') s = 'DELIVERED' else if (v === 'FAILED' || v === 'EXCEPTION') s = 'EXCEPTION' else if (v === 'RETURNED') s = 'RETURNED' else { const valid = ['ORDER_PLACED','SHIPPED','IN_TRANSIT','OUT_FOR_DELIVERY','READY_FOR_PICKUP','DELIVERED','EXCEPTION','RETURNED'] if (valid.indexOf(v) > -1) s = v } return s } async function updateWaybill(id, status_code, text) { try { const now = new Date().toISOString() await supaFetch(`platform_express_waybills?id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, body: JSON.stringify({ current_status_code: status_code, current_status_text: text, last_synced_at: now }) }) } catch (e) { console.warn('updateWaybill error', e) } } async function insertEvent(event) { try { await supaFetch('platform_express_tracking_events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }) } catch (e) { console.warn('insertEvent error', e) } } async function start() { if (!SUPA_URL || !SUPA_KEY) { console.error('SUPA_URL and SUPA_KEY must be set in env') process.exit(1) } const app = express() app.use(bodyParser.json({ limit: '1mb' })) app.post('/webhook/express/status', async (req, res) => { const ts = req.headers['x-timestamp'] || req.headers['X-TIMESTAMP'] || '' const sig = req.headers['x-signature'] || req.headers['X-SIGNATURE'] || '' const cid = req.headers['x-client-id'] || req.headers['X-CLIENT-ID'] || '' const bodyText = JSON.stringify(req.body || {}) let sigValid = true if (WEBHOOK_SECRET) { const calc = computeSignature(bodyText, ts) sigValid = calc === String(sig) } // persist raw await upsertRaw(req.body || {}, req.body && (req.body.mailNo || req.body.tracking_no), req.body && req.body.carrier, sigValid) // find waybill const tracking_no = req.body && (req.body.mailNo || req.body.tracking_no) const order_no = req.body && (req.body.txLogisticId || req.body.order_no) const carrier = req.body && req.body.carrier ? req.body.carrier : (req.body && req.body.company || null) const event_code = req.body && (req.body.infoContent || req.body.status_code || req.body.event_code) const event_text = req.body && (req.body.remark || req.body.event_text || '') const waybillId = await findWaybillId(tracking_no, order_no) if (!waybillId) { // Waybill not found — respond 200 but inform caller in body. return res.status(200).json({ ok: false, message: 'waybill not found' }) } const status_code = mapStatus(event_code) // update waybill await updateWaybill(waybillId, status_code, event_text) // parse event_time let event_time = new Date().toISOString() if (req.body && req.body.acceptTime) { try { const t = req.body.acceptTime.indexOf('T') > -1 ? req.body.acceptTime : req.body.acceptTime.replace(' ', 'T') const dt = new Date(t) if (!isNaN(dt.getTime())) event_time = dt.toISOString() } catch (e) {} } // insert event const eventPayload = { waybill_id: waybillId, carrier: carrier, tracking_no: tracking_no || null, event_time: event_time, event_code: event_code || 'UNKNOWN', event_text: event_text || '', status_code: status_code, raw_payload: req.body || {}, dedupe_key: 'WEBHOOK_' + Date.now() } await insertEvent(eventPayload) return res.json({ ok: true }) }) app.get('/health', (req, res) => res.json({ ok: true })) app.listen(PORT, '0.0.0.0', () => console.log(`Webhook receiver listening on http://0.0.0.0:${PORT}`)) } start().catch(e => { console.error('start failed', e); process.exit(1) })