const DEFAULT_BASE = 'https://prime-iq.com/myp/webapi'; const CRED = { login: null, password: null, initialToken: null }; let cachedToken = null; let tokenExpiresAt = 0; function allowedOrigins(env) { return String(env.APP_ALLOWED_ORIGINS || '*').split(',').map(x => x.trim()).filter(Boolean); } function headersFor(request, env, contentType = 'application/json; charset=UTF-8') { const origin = request.headers.get('Origin'); const allowed = allowedOrigins(env); const headers = { 'Content-Type': contentType, 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Worker-Key, X-Confirm-Write, X-Request-Id', 'Vary': 'Origin' }; if (allowed.includes('*')) headers['Access-Control-Allow-Origin'] = '*'; else if (origin && allowed.includes(origin)) headers['Access-Control-Allow-Origin'] = origin; return headers; } function json(request, env, body, status = 200) { return new Response(JSON.stringify(body), { status, headers: headersFor(request, env) }); } function baseUrl(env) { return env.PRIME_BASE_URL || DEFAULT_BASE; } function today() { return new Date().toISOString().slice(0, 10); } function daysAgo(n) { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString().slice(0, 10); } function lockedSenderId(env) { const n = Number(env?.PAGE_LOCK_SENDER_ID || 0); return Number.isInteger(n) && n > 0 ? n : 0; } function employeePathAllowed(path, method) { if (method === 'GET') return new Set(['/','/health','/states','/districts','/shipments','/latest','/history','/steps','/reasons','/reasons/list','/print']).has(path); return method === 'POST' && path === '/create'; } function isDate(value) { return /^\d{4}-\d{2}-\d{2}$/.test(String(value || '')); } function isNumericId(value) { return /^\d+$/.test(String(value || '')); } function validDateRange(from, to, maxDays = 90) { if (!isDate(from) || !isDate(to)) return false; const start = new Date(`${from}T00:00:00Z`).getTime(); const end = new Date(`${to}T00:00:00Z`).getTime(); return Number.isFinite(start) && Number.isFinite(end) && end >= start && end - start <= maxDays * 86400000; } function pageParams(query) { const start = Number(query.get('start') || 0); const offset = Number(query.get('offset') || 20); if (!Number.isInteger(start) || start < 0 || !Number.isInteger(offset) || offset < 1 || offset > 50) return null; return { start, offset }; } function safeSegment(value, max = 120) { const text = String(value || ''); return text && text.length <= max && !text.includes('/') && !text.includes('..') ? text : null; } function writeEnabled(request, env) { return env.ENABLE_TICKET_WRITES === 'true' && request.headers.get('X-Confirm-Write') === 'yes'; } function primeWriteEnabled(request, env) { return env.ENABLE_PRIME_WRITES === 'true' && request.headers.get('X-Confirm-Write') === 'yes'; } function audit(request, action, detail = {}) { console.info({ audit: true, action, method: request.method, detail, at: new Date().toISOString() }); } function requestId(request) { const incoming = String(request.headers.get('X-Request-Id') || '').trim(); return /^[A-Za-z0-9._:-]{1,80}$/.test(incoming) ? incoming : crypto.randomUUID(); } function actionDetails(payload) { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return {}; const allowed = ['caseid','caseId','action','systemStageCode','systemStepCode','caseFixId','subjectCode','ticketId','senderId']; return Object.fromEntries(allowed.filter(k => payload[k] !== undefined).map(k => [k, payload[k]])); } function logPrimeAction({ id, action, path, method, caseid, status, ok, startedAt, error }) { const record = { type: 'prime_action', requestId: id, action, path, method, caseid: caseid || undefined, status, ok: Boolean(ok), durationMs: Date.now() - startedAt, at: new Date().toISOString() }; if (error) record.error = String(error).slice(0, 240); (ok ? console.info : console.error)(record); } function writeResult(request, env, body, status, id) { const response = json(request, env, { ...body, requestId: id }, status); response.headers.set('X-Request-Id', id); return response; } async function primeAction(request, env, { action, path, method, payload, caseid }) { const id = requestId(request); const startedAt = Date.now(); const safeDetails = Array.isArray(payload) ? { count: payload.length, senderId: payload[0]?.senderId } : actionDetails(payload); console.info({ type: 'prime_action_started', requestId: id, action, path, method, ...safeDetails, at: new Date().toISOString() }); try { const result = await primeApi(path, method, payload, env); const ok = result.status >= 200 && result.status < 300; logPrimeAction({ id, action, path, method, caseid, status: result.status, ok, startedAt, error: ok ? null : (result.data?.errorMessage || result.data?.message || result.data?.error) }); return { ...result, requestId: id }; } catch (error) { logPrimeAction({ id, action, path, method, caseid, status: 502, ok: false, startedAt, error: error?.message || error }); return { status: 502, data: { error: error?.message || 'Prime request failed' }, requestId: id }; } } function pickArray(value) { if (Array.isArray(value)) return value; if (!value || typeof value !== 'object') return []; for (const key of Object.keys(value)) { if (Array.isArray(value[key])) return value[key]; if (value[key] && typeof value[key] === 'object') { for (const nested of Object.keys(value[key])) { if (Array.isArray(value[key][nested])) return value[key][nested]; } } } return []; } async function responseData(response) { const text = await response.text(); try { return text ? JSON.parse(text) : null; } catch { return text; } } async function getToken(env) { if (!env?.PRIME_LOGIN || !env?.PRIME_PASSWORD || !env?.PRIME_INITIAL_TOKEN) { throw new Error('Missing Prime Secrets: PRIME_LOGIN, PRIME_PASSWORD, PRIME_INITIAL_TOKEN'); } if (cachedToken && Date.now() < tokenExpiresAt) return cachedToken; CRED.login = env.PRIME_LOGIN; CRED.password = env.PRIME_PASSWORD; CRED.initialToken = env.PRIME_INITIAL_TOKEN; const response = await fetch(`${baseUrl(env)}/auth/external-system-login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(CRED) }); const body = await responseData(response); if (!response.ok || !body?.accessToken) throw new Error(`Prime login failed: ${response.status}`); cachedToken = body.accessToken; tokenExpiresAt = Date.now() + 25 * 60 * 1000; return cachedToken; } async function primeApi(path, method = 'GET', body = null, env, retry = true) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 30000); try { const token = await getToken(env); const response = await fetch(`${baseUrl(env)}${path}`, { method, signal: controller.signal, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: body == null ? undefined : JSON.stringify(body) }); if (response.status === 401 && retry) { cachedToken = null; tokenExpiresAt = 0; return primeApi(path, method, body, env, false); } return { status: response.status, data: await responseData(response) }; } finally { clearTimeout(timer); } } async function primeMultipart(path, form, env) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 60000); try { const token = await getToken(env); const response = await fetch(`${baseUrl(env)}${path}`, { method: 'POST', signal: controller.signal, headers: { Authorization: `Bearer ${token}` }, body: form }); return { status: response.status, data: await responseData(response) }; } finally { clearTimeout(timer); } } function formWithinLimit(form, maxBytes = 10 * 1024 * 1024) { for (const [, value] of form.entries()) if (typeof value !== 'string' && value.size > maxBytes) return false; return true; } function primeCaseIdOf(value) { const n = Number(value?.caseid); return Number.isSafeInteger(n) && n > 0 ? n : null; } async function tryPaths(paths, method, body, env) { const tried = []; for (const path of paths) { const result = await primeApi(path, method, body, env); tried.push({ path, status: result.status }); if (result.status !== 404) { return { ok: result.status >= 200 && result.status < 300, status: result.status, data: result.data, usedPath: path, tried }; } } return { ok: false, status: 404, error: 'no matching Prime path', tried }; } async function searchShipments(from, to, env) { const all = []; for (let page = 1; page <= 20; page++) { const result = await primeApi(`/external/search-shipments?page=${page}`, 'POST', { fromDate: from, toDate: to }, env); if (result.status < 200 || result.status >= 300) return { ok: false, status: result.status, data: result.data }; const rows = pickArray(result.data); if (!rows.length) break; all.push(...rows); if (rows.length < 10) break; } const summary = all.map(s => ({ caseid: s.caseid, primeCaseId: primeCaseIdOf(s), senderId: s.senderId, receiverName: s.receiverName, receiverHp1: s.receiverHp1, amount: s.receiptAmtIqd, district: s.district, state: s.state, stepCode: s.stepCode, stepName: s.stepName || '', stageName: s.stageName || '', statusLabel: s.statusLabel || '', returnReason: s.rtnReason || s.rtnReasonCode || '', snag: s.snagName || s.snagCode || '', snagFixes: (s.snagPossibleFixes || []).map(f => f.description).filter(Boolean), postponedReason: s.postponedReasonCode || '', postponedTo: s.postponedTo || '', agentNote: s.agentRmk || '', productInfo: s.productInfo || '', locationDetails: s.locationDetails || '' })); const lock = lockedSenderId(env); const shipments = (lock ? all.filter(x => Number(x.senderId) === lock) : all).map(s => ({ ...s, primeCaseId: primeCaseIdOf(s) })); const lockedSummary = lock ? summary.filter(x => Number(x.senderId) === lock) : summary; return { ok: true, count: shipments.length, shipments, summary: lockedSummary, updatedAt: new Date().toISOString() }; } function cacheKey(env) { return `https://prime-worker-cache.local/latest/${lockedSenderId(env) || 'all'}`; } async function saveCache(payload, env) { await caches.default.put(new Request(cacheKey(env)), new Response(JSON.stringify(payload), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=600' } })); } async function readCache(env) { const hit = await caches.default.match(new Request(cacheKey(env))); return hit ? hit.json().catch(() => null) : null; } function apiKeyRequired(request, env, path) { const publicPaths = new Set(['/', '/health']); if (publicPaths.has(path)) return false; return Boolean(env.WORKER_API_KEY) && request.headers.get('X-Worker-Key') !== env.WORKER_API_KEY; } function allowedApiPath(path, method) { const read = method === 'GET' && ( /^\/general\/states$/.test(path) || /^\/general\/state-districts\/[A-Za-z0-9_-]+$/.test(path) || /^\/general\/branches$/.test(path) || /^\/external\/merchants$/.test(path) || /^\/external\/list-of-system-steps$/.test(path) || /^\/external\/(list-of-system-rtn-reasons|list-of-snag-reasons|snag-reasons|list-snag-reasons)$/.test(path) || /^\/agent-process\/rtn-reasons$/.test(path) || /^\/external\/case-history\/\d+$/.test(path) || /^\/external\/merchant-shops\/[^/]+$/.test(path) || /^\/external\/ticket-subjects$/.test(path) || /^\/external\/list-tickets\/[^/]+\/\d+\/\d+$/.test(path) || /^\/external\/single-ticket-header\/\d+$/.test(path) || /^\/external\/ticket-chat\/\d+$/.test(path) || /^\/notifications\/unseen-tickets$/.test(path) || /^\/external\/merchant-payment-receipts\/[^/]+\/\d+\/\d+$/.test(path) || /^\/external\/merchant-cases-in-payment-receipt\/[^/]+\/\d+$/.test(path) || /^\/external\/merchant-shipments-in-payment-receipt\/[^/]+\/\d+$/.test(path) || /^\/logistics\/print\/case\/\d+\/[A-Za-z0-9_-]+$/.test(path) ); const search = method === 'POST' && /^\/external\/search-shipments\?page=\d+$/.test(path); return read || search; } export default { async scheduled(event, env, ctx) { ctx.waitUntil(searchShipments(daysAgo(3), today(), env).then(data => data.ok ? saveCache(data, env) : null).catch(() => null)); }, async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname.replace(/\/+$/, '') || '/'; const method = request.method.toUpperCase(); if (method === 'OPTIONS') return new Response(null, { headers: headersFor(request, env) }); if (apiKeyRequired(request, env, path)) return json(request, env, { ok: false, error: 'unauthorized' }, 401); if (env.EMPLOYEE_MODE === 'true' && !employeePathAllowed(path, method)) return json(request, env, { ok: false, error: 'employee route is not allowed' }, 403); const query = url.searchParams; const body = async () => request.json().catch(() => null); try { if (path === '/health') return json(request, env, { ok: true, service: 'prime-worker-final', time: new Date().toISOString() }); if (path === '/') return json(request, env, { ok: true, msg: 'proxy running', routes: ['/login','/states','/districts?state=BGD','/branches','/merchants','/shops?login=','/create','/shipments?from=&to=','/shipments/resend','/shipments/update','/latest','/history?id=','/steps','/reasons','/print?id=&size=A4','/refresh','/tickets','/tickets/subjects','/tickets/:id','/tickets/:id/chat','/tickets/unseen','/finance/account','/finance/receipts?login='] }); if (path === '/login' && method === 'GET') { const token = await getToken(env); return json(request, env, { ok: true, tokenLength: token.length }); } if (path === '/states' && method === 'GET') { const r = await primeApi('/general/states', 'GET', null, env); return json(request, env, { ok: r.status === 200, states: r.data }, r.status); } if (path === '/districts' && method === 'GET') { const state = query.get('state') || 'BGD'; if (!/^[A-Za-z0-9_-]+$/.test(state)) return json(request, env, { ok: false, error: 'invalid state' }, 400); const r = await primeApi(`/general/state-districts/${state}`, 'GET', null, env); return json(request, env, { ok: r.status === 200, state, districts: pickArray(r.data).map(x => ({ id: x.districtId, name: x.districtName })) }, r.status); } if (path === '/branches' && method === 'GET') { const r = await primeApi('/general/branches', 'GET', null, env); return json(request, env, { ok: r.status === 200, branches: r.data }, r.status); } if (path === '/merchants' && method === 'GET') { const r = await primeApi('/external/merchants', 'GET', null, env); return json(request, env, { ok: r.status === 200, merchants: r.data }, r.status); } if (path === '/shops' && method === 'GET') { const login = query.get('login'); if (!login || login.length > 120) return json(request, env, { ok: false, error: 'missing or invalid login' }, 400); const r = await primeApi(`/external/merchant-shops/${encodeURIComponent(login)}`, 'GET', null, env); return json(request, env, { ok: r.status === 200, shops: r.data }, r.status); } if (path === '/tickets/subjects' && method === 'GET') { const r = await primeApi('/external/ticket-subjects', 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, subjects: r.data }, r.status); } if (path === '/tickets/unseen' && method === 'GET') { const r = await primeApi('/notifications/unseen-tickets', 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, unseen: r.data }, r.status); } if (path === '/tickets' && method === 'GET') { const status = safeSegment(query.get('status') || 'open', 40); const paging = pageParams(query); if (!status || !paging) return json(request, env, { ok: false, error: 'invalid ticket status or pagination' }, 400); const r = await primeApi(`/external/list-tickets/${encodeURIComponent(status)}/${paging.start}/${paging.offset}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, tickets: r.data, page: paging }, r.status); } const ticketMatch = path.match(/^\/tickets\/(\d+)$/); if (ticketMatch && method === 'GET') { const id = ticketMatch[1]; const r = await primeApi(`/external/single-ticket-header/${id}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, ticket: r.data }, r.status); } const ticketChatMatch = path.match(/^\/tickets\/(\d+)\/chat$/); if (ticketChatMatch && method === 'GET') { const id = ticketChatMatch[1]; const r = await primeApi(`/external/ticket-chat/${id}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, chat: r.data }, r.status); } if (path === '/finance/account' && method === 'GET') { const r = await primeApi('/financials/customer-financials', 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, account: r.data, source: '/financials/customer-financials' }, r.status); } if (path === '/finance/receipts' && method === 'GET') { const login = safeSegment(query.get('login'), 120); const paging = pageParams(query); if (!login || !paging) return json(request, env, { ok: false, error: 'login and valid pagination are required' }, 400); const r = await primeApi(`/external/merchant-payment-receipts/${encodeURIComponent(login)}/${paging.start}/${paging.offset}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, receipts: r.data, page: paging }, r.status); } const receiptCases = path.match(/^\/finance\/receipts\/(\d+)\/cases$/); if (receiptCases && method === 'GET') { const login = safeSegment(query.get('login'), 120); if (!login) return json(request, env, { ok: false, error: 'login is required' }, 400); const r = await primeApi(`/external/merchant-cases-in-payment-receipt/${encodeURIComponent(login)}/${receiptCases[1]}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, cases: r.data }, r.status); } const receiptShipments = path.match(/^\/finance\/receipts\/(\d+)\/shipments$/); if (receiptShipments && method === 'GET') { const login = safeSegment(query.get('login'), 120); if (!login) return json(request, env, { ok: false, error: 'login is required' }, 400); const r = await primeApi(`/external/merchant-shipments-in-payment-receipt/${encodeURIComponent(login)}/${receiptShipments[1]}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, shipments: r.data }, r.status); } if (path === '/tickets/open' && method === 'POST') { if (!writeEnabled(request, env)) return json(request, env, { ok: false, error: 'ticket writes are disabled' }, 403); const payload = await body(); if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return json(request, env, { ok: false, error: 'valid JSON object required' }, 400); const r = await primeAction(request, env, { action: 'ticket.open', path: '/external/open-ticket', method: 'POST', payload, caseid: payload.caseId || payload.ticket?.caseid }); return writeResult(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status, r.requestId); } if (path === '/shipments/resend' && method === 'POST') { if (!primeWriteEnabled(request, env)) return json(request, env, { ok: false, error: 'Prime write operations are disabled or not confirmed' }, 403); const payload = await body(); if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !isNumericId(payload.caseid) || payload.action !== 'REQUEST_RESEND' || !payload.systemStageCode || !payload.systemStepCode) { return json(request, env, { ok: false, error: 'caseid, action REQUEST_RESEND, systemStageCode and systemStepCode are required' }, 400); } const r = await primeAction(request, env, { action: 'shipment.resend', path: '/external/update-case-status', method: 'POST', payload, caseid: Number(payload.caseid) }); return writeResult(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status, r.requestId); } const ticketMessage = path.match(/^\/tickets\/(\d+)\/message$/); if (ticketMessage && method === 'POST') { if (!writeEnabled(request, env)) return json(request, env, { ok: false, error: 'ticket writes are disabled' }, 403); const payload = await body(); if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return json(request, env, { ok: false, error: 'valid JSON object required' }, 400); audit(request, 'ticket.message', { ticketId: ticketMessage[1] }); const r = await primeApi(`/external/ticket-chat/write-msg/${ticketMessage[1]}`, 'POST', payload, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status); } const ticketMedia = path.match(/^\/tickets\/(\d+)\/media$/); if (ticketMedia && method === 'POST') { if (!writeEnabled(request, env)) return json(request, env, { ok: false, error: 'ticket writes are disabled' }, 403); const form = await request.formData(); if (!formWithinLimit(form)) return json(request, env, { ok: false, error: 'media file exceeds 10 MB' }, 413); audit(request, 'ticket.media', { ticketId: ticketMedia[1] }); const r = await primeMultipart('/external/ticket-chat/upload-media', form, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status); } if (path === '/shipments' && method === 'GET') { const from = query.get('from') || daysAgo(1); const to = query.get('to') || today(); if (!validDateRange(from, to)) return json(request, env, { ok: false, error: 'invalid date range; use YYYY-MM-DD and max 90 days' }, 400); return json(request, env, await searchShipments(from, to, env)); } if (path === '/latest' && method === 'GET') { const cached = await readCache(env); if (cached) return json(request, env, { ...cached, fromCache: true }); const fresh = await searchShipments(daysAgo(3), today(), env); if (fresh.ok) ctx.waitUntil(saveCache(fresh, env)); return json(request, env, { ...fresh, fromCache: false }); } if (path === '/refresh' && method === 'GET') { const fresh = await searchShipments(daysAgo(3), today(), env); if (fresh.ok) await saveCache(fresh, env); return json(request, env, { ok: fresh.ok, count: fresh.count || 0, updatedAt: fresh.updatedAt }, fresh.ok ? 200 : (fresh.status || 502)); } if (path === '/history' && method === 'GET') { const id = query.get('id'); if (!isNumericId(id)) return json(request, env, { ok: false, error: 'id must be numeric' }, 400); const r = await primeApi(`/external/case-history/${id}`, 'GET', null, env); if (r.status !== 200) return json(request, env, { ok: false, status: r.status, data: r.data }, r.status); const raw = Array.isArray(r.data) ? r.data : []; const steps = raw.map(x => ({ at: x.queueEnterDate || '', stepCode: x.stepCode || '', status: x.shipmentStatus || '', snagCode: x.snagCode || '' })); return json(request, env, { ok: true, caseid: Number(id), currentStep: steps[0]?.stepCode || '', currentStatus: steps[0]?.status || '', problemCode: steps.map(x => x.snagCode).filter(Boolean).pop() || '', createdAt: steps.at(-1)?.at || '', lastUpdate: steps[0]?.at || '', stepCount: steps.length, steps }); } if (path === '/steps' && method === 'GET') { const r = await primeApi('/external/list-of-system-steps', 'GET', null, env); return json(request, env, { ok: r.status === 200, steps: r.data }, r.status); } if (path === '/reasons/list' && method === 'GET') { const r = await tryPaths(['/external/list-of-system-rtn-reasons','/agent-process/rtn-reasons','/external/list-of-snag-reasons','/external/snag-reasons','/external/list-snag-reasons','/general/snag-reasons'], 'GET', null, env); return json(request, env, r, r.status || 502); } if (path === '/reasons' && method === 'GET') { const from = query.get('from') || daysAgo(1); const to = query.get('to') || today(); if (!validDateRange(from, to)) return json(request, env, { ok: false, error: 'invalid date range; use YYYY-MM-DD and max 90 days' }, 400); const result = await searchShipments(from, to, env); if (!result.ok) return json(request, env, result, result.status || 502); const problems = result.summary.filter(x => x.returnReason || x.snag || x.postponedReason); return json(request, env, { ok: true, from, to, count: problems.length, problems }); } if (path === '/print' && method === 'GET') { const id = query.get('id'); const size = query.get('size') || 'A4'; if (!isNumericId(id) || !/^[A-Za-z0-9_-]{1,20}$/.test(size)) return json(request, env, { ok: false, error: 'invalid id or size' }, 400); const r = await primeApi(`/logistics/print/case/${id}/${encodeURIComponent(size)}`, 'GET', null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, caseid: Number(id), size, printUrl: r.data }, r.status); } if (path === '/create' && method === 'POST') { const payload = await body(); if (!Array.isArray(payload) || payload.length < 1 || payload.length > 50) return json(request, env, { ok: false, error: 'body must be an array of 1 to 50 shipments' }, 400); const lock = lockedSenderId(env); if (lock && payload.some(x => Number(x?.senderId) !== lock)) return json(request, env, { ok: false, error: 'senderId is not allowed for this employee page' }, 403); const r = await primeAction(request, env, { action: 'shipment.create', path: '/external/create-shipments', method: 'POST', payload }); return writeResult(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status, r.requestId); } if (path === '/merchants/create' && method === 'POST') return json(request, env, await tryPaths(['/external/create-merchant','/external/merchants'], 'POST', await body(), env)); if (path === '/merchants/update' && method === 'PUT') return json(request, env, await tryPaths(['/external/update-merchant','/external/merchants'], 'PUT', await body(), env)); if (path === '/shops/create' && method === 'POST') return json(request, env, await tryPaths(['/external/create-shop','/external/merchant-shops'], 'POST', await body(), env)); if (path === '/shops/update' && method === 'PUT') return json(request, env, await tryPaths(['/external/update-shop','/external/merchant-shops'], 'PUT', await body(), env)); if (path === '/shipments/update' && method === 'PUT') { if (!primeWriteEnabled(request, env)) return json(request, env, { ok: false, error: 'Prime write operations are disabled or not confirmed' }, 403); const payload = await body(); if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !isNumericId(payload.caseid || payload.caseId)) return json(request, env, { ok: false, error: 'caseid or caseId is required' }, 400); const r = await primeAction(request, env, { action: 'shipment.update', path: '/external/update-shipment', method: 'PUT', payload, caseid: Number(payload.caseid || payload.caseId) }); return writeResult(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data }, r.status, r.requestId); } if (path === '/shipments/delete' && method === 'DELETE') { const id = query.get('id'); if (!isNumericId(id)) return json(request, env, { ok: false, error: 'numeric id required' }, 400); return json(request, env, await tryPaths([`/external/delete-case/${id}`,`/external/case/${id}`,`/external/delete-shipment/${id}`,`/external/shipments/${id}`], 'DELETE', null, env)); } if (path === '/shipment-info' && method === 'POST') return json(request, env, await tryPaths(['/external/cases-information','/external/case-information','/external/shipments-information','/external/shipment-information'], 'POST', await body(), env)); if (path === '/charges' && method === 'POST') return json(request, env, await tryPaths(['/external/case-charges-calculations','/external/shipment-charges-calculations','/external/case-charges'], 'POST', await body(), env)); if (path === '/api' && (method === 'GET' || method === 'POST')) { const requestedPath = query.get('path'); const requestedMethod = (query.get('method') || method).toUpperCase(); if (!requestedPath || !requestedPath.startsWith('/') || requestedPath.includes('..') || !allowedApiPath(requestedPath, requestedMethod)) { return json(request, env, { ok: false, error: 'path or method is not allowed' }, 403); } const r = await primeApi(requestedPath, requestedMethod, requestedMethod === 'POST' ? await body() : null, env); return json(request, env, { ok: r.status >= 200 && r.status < 300, status: r.status, data: r.data, path: requestedPath }, r.status); } return json(request, env, { ok: false, error: `unknown path: ${path}` }, 404); } catch (error) { const message = error?.name === 'AbortError' ? 'PRIME_TIMEOUT' : String(error?.message || error); return json(request, env, { ok: false, error: message }, 502); } } };