From 14c305b6bf483c988279ea1ef8f7bc117b737202 Mon Sep 17 00:00:00 2001 From: TengkuAchmad Date: Mon, 3 Aug 2026 22:49:38 +0700 Subject: [PATCH] feat: document full API surface in Postman collection Reorganize into folders (Health & Config, Payment Links, Payment Operations, Webhook, Logs & Docs, Dev/Test) and add every route that was missing: GET/POST /api/config, POST /createtransaksi (ERP shape), Charge (Core API), Create Snap Token, GET /api/logs, GET /api/logs/files, GET /api/logs/view, GET /openapi.json, GET /docs, and the LOG_EXPOSE_API-only echo/echo2/ test-notify-erp endpoints. The Logs & Docs folder carries collection-level Basic Auth (log_user/log_pass variables) shared by all its requests. Co-Authored-By: Claude Sonnet 5 --- ...idtrans-Middleware.postman_collection.json | 362 ++++++++++++++---- 1 file changed, 277 insertions(+), 85 deletions(-) diff --git a/postman/Midtrans-Middleware.postman_collection.json b/postman/Midtrans-Middleware.postman_collection.json index 2f57ae4..3c7178e 100644 --- a/postman/Midtrans-Middleware.postman_collection.json +++ b/postman/Midtrans-Middleware.postman_collection.json @@ -1,7 +1,7 @@ { "info": { - "name": "Midtrans Middleware - Payment Link", - "description": "Generate & manage shareable Midtrans Snap payment links via the CIFO Midtrans middleware.\n\nFlow: 1) Create Payment Link -> 2) share the returned `data.url` with the customer -> 3) (optional) Resolve Payment Link / Check Status to track it.", + "name": "Midtrans Middleware", + "description": "Full API surface of the CIFO Midtrans middleware: payment link generation, direct Core/Snap checkout, the Midtrans webhook, and internal log/docs tooling.\n\nMain flow: 1) Create Payment Link -> 2) share the returned `data.url` with the customer -> 3) (optional) Resolve Payment Link / Check Status to track it.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ @@ -9,104 +9,296 @@ { "key": "api_key", "value": "dev-key", "type": "string" }, { "key": "order_id", "value": "ERPSKRIP-2608030000000627:TKG-260803000063", "type": "string" }, { "key": "payment_token", "value": "", "type": "string" }, - { "key": "midtrans_order_id", "value": "", "type": "string" } + { "key": "midtrans_order_id", "value": "", "type": "string" }, + { "key": "log_user", "value": "", "type": "string" }, + { "key": "log_pass", "value": "", "type": "string" } ], "item": [ { - "name": "Create Payment Link", - "request": { - "method": "POST", - "header": [ - { "key": "X-API-KEY", "value": "{{api_key}}" }, - { "key": "Content-Type", "value": "application/json" } - ], - "url": { - "raw": "{{base_url}}/api/payment-links", - "host": ["{{base_url}}"], - "path": ["api", "payment-links"] - }, - "description": "Creates a signed, shareable Midtrans Snap payment link directly from the given payload.\n\norder_id is used as-is for tracking, and sanitized (':' -> '.') for the value actually sent to Midtrans, since Midtrans order_id only allows alphanumeric and - _ ~ .\n\nexpire_at is honored if it's a future epoch-ms timestamp; otherwise it falls back to PAYMENT_LINK_TTL_MINUTES.\n\nResponse: `{ status, messages, data: { url, order_id, midtrans_order_id, expire_at } }` - `data.url` is the link to share with the customer.", - "body": { - "mode": "raw", - "raw": "{\n \"order_id\": \"ERPSKRIP-2608030000000627:TKG-260803000063\",\n \"nominal\": 179000,\n \"customer\": {\n \"name\": \"Yusnika Nur Faidah\",\n \"phone\": \"0881022144656\",\n \"email\": \"yusnika_nur_faidah@example.com\"\n },\n \"expire_at\": 1785852063058\n}", - "options": { "raw": { "language": "json" } } - } - }, - "event": [ + "name": "Health & Config", + "item": [ { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (pm.response.code === 200) {", - " const data = pm.response.json()?.data || {};", - " const token = (data.url || '').split('/').pop();", - " if (token) pm.collectionVariables.set('payment_token', token);", - " if (data.midtrans_order_id) pm.collectionVariables.set('midtrans_order_id', data.midtrans_order_id);", - "}" - ] + "name": "Health Check", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "{{base_url}}/api/health", "host": ["{{base_url}}"], "path": ["api", "health"] }, + "description": "Server + Midtrans key configuration health check." + } + }, + { + "name": "Get Runtime Config", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "{{base_url}}/api/config", "host": ["{{base_url}}"], "path": ["api", "config"] }, + "description": "Returns payment-method toggles, Midtrans env (sandbox/production), and the public client key." + } + }, + { + "name": "Update Runtime Config (dev only)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/config", "host": ["{{base_url}}"], "path": ["api", "config"] }, + "description": "Toggle payment methods at runtime. Returns 403 when NODE_ENV=production.", + "body": { + "mode": "raw", + "raw": "{\n \"paymentToggles\": {\n \"bank_transfer\": true,\n \"credit_card\": true,\n \"gopay\": true,\n \"cstore\": true\n }\n}", + "options": { "raw": { "language": "json" } } + } } } ] }, { - "name": "Resolve Payment Link", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/payment-links/{{payment_token}}", - "host": ["{{base_url}}"], - "path": ["api", "payment-links", "{{payment_token}}"] + "name": "Payment Links", + "item": [ + { + "name": "Create Payment Link (direct payload)", + "request": { + "method": "POST", + "header": [ + { "key": "X-API-KEY", "value": "{{api_key}}" }, + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{base_url}}/api/payment-links", "host": ["{{base_url}}"], "path": ["api", "payment-links"] }, + "description": "Creates a signed, shareable Midtrans Snap payment link directly from the given payload.\n\norder_id is used as-is for tracking, and sanitized (':' -> '.') for the value actually sent to Midtrans, since Midtrans order_id only allows alphanumeric and - _ ~ .\n\nexpire_at is honored if it's a future epoch-ms timestamp; otherwise it falls back to PAYMENT_LINK_TTL_MINUTES.\n\nResponse: `{ status, messages, data: { url, order_id, midtrans_order_id, expire_at } }` - `data.url` is the link to share with the customer.\n\nRate-limited (RATE_LIMIT_MAX per RATE_LIMIT_WINDOW_MS per IP).", + "body": { + "mode": "raw", + "raw": "{\n \"order_id\": \"ERPSKRIP-2608030000000627:TKG-260803000063\",\n \"nominal\": 179000,\n \"customer\": {\n \"name\": \"Yusnika Nur Faidah\",\n \"phone\": \"0881022144656\",\n \"email\": \"yusnika_nur_faidah@example.com\"\n },\n \"expire_at\": 1785852063058\n}", + "options": { "raw": { "language": "json" } } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.response.code === 200) {", + " const data = pm.response.json()?.data || {};", + " const token = (data.url || '').split('/').pop();", + " if (token) pm.collectionVariables.set('payment_token', token);", + " if (data.midtrans_order_id) pm.collectionVariables.set('midtrans_order_id', data.midtrans_order_id);", + "}" + ] + } + } + ] }, - "description": "Resolves a payment link token into the underlying order details. Returns `{ order_id, nominal, customer, expire_at, allowed_methods }`. `payment_token` is auto-filled from the \"Create Payment Link\" response." - } - }, - { - "name": "Check Payment Status", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/payments/{{midtrans_order_id}}/status", - "host": ["{{base_url}}"], - "path": ["api", "payments", "{{midtrans_order_id}}", "status"] + { + "name": "Create Payment Link (ERP mercant_id/item shape)", + "request": { + "method": "POST", + "header": [ + { "key": "X-API-KEY", "value": "{{api_key}}" }, + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{base_url}}/createtransaksi", "host": ["{{base_url}}"], "path": ["createtransaksi"] }, + "description": "Legacy/ERP-facing endpoint. order_id is derived server-side as `mercant_id.item[0].item_id` (dot-joined; Midtrans order_id only allows alphanumeric and - _ ~ .).\n\nexpire_at is NOT client-supplied — computed server-side from PAYMENT_LINK_TTL_MINUTES (default 1440 min).\n\nResponse: `{ status, messages, data: { url } }`. Rate-limited.", + "body": { + "mode": "raw", + "raw": "{\n \"mercant_id\": \"ERPSKRIP-2608030000000627\",\n \"nominal\": 179000,\n \"nama\": \"Yusnika Nur Faidah\",\n \"no_telepon\": \"0881022144656\",\n \"email\": \"yusnika_nur_faidah@example.com\",\n \"item\": [\n { \"item_id\": \"TKG-260803000063\" }\n ]\n}", + "options": { "raw": { "language": "json" } } + } + } }, - "description": "Checks the current Midtrans transaction status. Uses `midtrans_order_id` (the Midtrans-safe id, ':' replaced with '.'), auto-filled from the \"Create Payment Link\" response." - } - }, - { - "name": "Simulate Midtrans Notification (Local)", - "request": { - "method": "POST", - "header": [ - { "key": "Content-Type", "value": "application/json" } - ], - "url": { - "raw": "{{base_url}}/api/payments/notification", - "host": ["{{base_url}}"], - "path": ["api", "payments", "notification"] - }, - "description": "Replays a REAL webhook payload (copied from Midtrans Dashboard's notification history) straight into the local server, so you can debug the ERP-notify flow without waiting for Midtrans to reach a public URL.\n\nIMPORTANT: `signature_key` in the body must have been generated with the SAME `MIDTRANS_SERVER_KEY` as this local server's `.env` (production key vs sandbox key mismatch = signature invalid). Paste the exact JSON from Dashboard as-is; do not modify order_id/status_code/gross_amount or the signature will no longer match.", - "body": { - "mode": "raw", - "raw": "{\n \"transaction_type\": \"off-us\",\n \"transaction_time\": \"2026-08-03 21:26:58\",\n \"transaction_status\": \"settlement\",\n \"transaction_id\": \"5654e1a9-0d00-4e93-8629-026e6282e4d5\",\n \"status_message\": \"midtrans payment notification\",\n \"status_code\": \"200\",\n \"signature_key\": \"27bad17aac3ca8e2d22096d747d4ea6630237db179df4d3576c93a4300c6bcb72b69c9ca62f90b7af2a005100373b584051406df4f47ec51af4b55bd84de6898\",\n \"settlement_time\": \"2026-08-03 21:31:20\",\n \"pop_id\": \"7c27736b-deb2-483f-a152-2ff50803a3ba\",\n \"payment_type\": \"qris\",\n \"order_id\": \"ERPSKRIP-2608030000000637.TKG-260801001361\",\n \"merchant_id\": \"G277033254\",\n \"merchant_cross_reference_id\": \"d65f691f-4ba1-4dc6-a6eb-bd3f3303416a\",\n \"issuer\": \"BJB\",\n \"gross_amount\": \"159000.00\",\n \"fraud_status\": \"accept\",\n \"expiry_time\": \"2026-08-04 21:26:58\",\n \"customer_details\": {\n \"phone\": \"+6281952988381\",\n \"full_name\": \"Saepudin \",\n \"email\": \"saepudin@example.com\"\n },\n \"currency\": \"IDR\",\n \"acquirer\": \"gopay\"\n}", - "options": { "raw": { "language": "json" } } + { + "name": "Resolve Payment Link", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/payment-links/{{payment_token}}", + "host": ["{{base_url}}"], + "path": ["api", "payment-links", "{{payment_token}}"] + }, + "description": "Resolves a payment link token into the underlying order details. Returns `{ order_id, nominal, customer, expire_at, allowed_methods }`. `payment_token` is auto-filled from \"Create Payment Link (direct payload)\"." + } } - } + ] }, { - "name": "Health Check", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/health", - "host": ["{{base_url}}"], - "path": ["api", "health"] + "name": "Payment Operations", + "item": [ + { + "name": "Charge (Core API)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/payments/charge", "host": ["{{base_url}}"], "path": ["api", "payments", "charge"] }, + "description": "Creates a Core API transaction (bank_transfer, credit_card, gopay/qris, cstore). Called directly from the browser checkout — no pre-registration required. Rate-limited to reduce bot abuse / card-testing.", + "body": { + "mode": "raw", + "raw": "{\n \"payment_type\": \"bank_transfer\",\n \"transaction_details\": {\n \"order_id\": \"order-{{$timestamp}}\",\n \"gross_amount\": 150000\n },\n \"bank_transfer\": {\n \"bank\": \"bca\"\n }\n}", + "options": { "raw": { "language": "json" } } + } + } }, - "description": "Server + Midtrans key configuration health check." - } + { + "name": "Create Snap Token", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/payments/snap/token", "host": ["{{base_url}}"], "path": ["api", "payments", "snap", "token"] }, + "description": "Creates a Midtrans Snap token for the hosted payment popup (window.snap.pay). Called directly from the browser (PayPage / CheckoutPage). Rate-limited.", + "body": { + "mode": "raw", + "raw": "{\n \"transaction_details\": {\n \"order_id\": \"order-{{$timestamp}}\",\n \"gross_amount\": 150000\n },\n \"customer_details\": {\n \"first_name\": \"John\",\n \"email\": \"john@example.com\",\n \"phone\": \"081234567890\"\n },\n \"item_details\": [\n { \"id\": \"order-{{$timestamp}}\", \"name\": \"Payment\", \"price\": 150000, \"quantity\": 1 }\n ]\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "Check Payment Status", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/payments/{{midtrans_order_id}}/status", + "host": ["{{base_url}}"], + "path": ["api", "payments", "{{midtrans_order_id}}", "status"] + }, + "description": "Passthrough of Midtrans core.transaction.status(). Uses `midtrans_order_id` (the Midtrans-safe id, ':' replaced with '.'), auto-filled from \"Create Payment Link (direct payload)\". Also fires an ERP-notify fallback if already successful and not yet notified." + } + } + ] + }, + { + "name": "Webhook", + "item": [ + { + "name": "Simulate Midtrans Notification (Local)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/payments/notification", "host": ["{{base_url}}"], "path": ["api", "payments", "notification"] }, + "description": "Replays a REAL webhook payload (copied from Midtrans Dashboard's notification history) straight into the local server, so you can debug the ERP-notify flow without waiting for Midtrans to reach a public URL.\n\nIMPORTANT: `signature_key` in the body must have been generated with the SAME `MIDTRANS_SERVER_KEY` as this local server's `.env` (production key vs sandbox key mismatch = signature invalid). Paste the exact JSON from Dashboard as-is; do not modify order_id/status_code/gross_amount or the signature will no longer match.", + "body": { + "mode": "raw", + "raw": "{\n \"transaction_type\": \"off-us\",\n \"transaction_time\": \"2026-08-03 21:26:58\",\n \"transaction_status\": \"settlement\",\n \"transaction_id\": \"5654e1a9-0d00-4e93-8629-026e6282e4d5\",\n \"status_message\": \"midtrans payment notification\",\n \"status_code\": \"200\",\n \"signature_key\": \"27bad17aac3ca8e2d22096d747d4ea6630237db179df4d3576c93a4300c6bcb72b69c9ca62f90b7af2a005100373b584051406df4f47ec51af4b55bd84de6898\",\n \"settlement_time\": \"2026-08-03 21:31:20\",\n \"pop_id\": \"7c27736b-deb2-483f-a152-2ff50803a3ba\",\n \"payment_type\": \"qris\",\n \"order_id\": \"ERPSKRIP-2608030000000637.TKG-260801001361\",\n \"merchant_id\": \"G277033254\",\n \"merchant_cross_reference_id\": \"d65f691f-4ba1-4dc6-a6eb-bd3f3303416a\",\n \"issuer\": \"BJB\",\n \"gross_amount\": \"159000.00\",\n \"fraud_status\": \"accept\",\n \"expiry_time\": \"2026-08-04 21:26:58\",\n \"customer_details\": {\n \"phone\": \"+6281952988381\",\n \"full_name\": \"Saepudin \",\n \"email\": \"saepudin@example.com\"\n },\n \"currency\": \"IDR\",\n \"acquirer\": \"gopay\"\n}", + "options": { "raw": { "language": "json" } } + } + } + } + ] + }, + { + "name": "Logs & Docs (internal)", + "auth": { + "type": "basic", + "basic": [ + { "key": "username", "value": "{{log_user}}", "type": "string" }, + { "key": "password", "value": "{{log_pass}}", "type": "string" } + ] + }, + "item": [ + { + "name": "Recent Logs (in-memory)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/logs?limit=100", + "host": ["{{base_url}}"], + "path": ["api", "logs"], + "query": [ + { "key": "limit", "value": "100" }, + { "key": "level", "value": "info", "disabled": true }, + { "key": "q", "value": "", "disabled": true } + ] + }, + "description": "Protected by HTTP Basic Auth (LOG_BASIC_AUTH_USER/PASS) — fill `log_user`/`log_pass` collection variables." + } + }, + { + "name": "List Log Files", + "request": { + "method": "GET", + "header": [{ "key": "Accept", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/logs/files", "host": ["{{base_url}}"], "path": ["api", "logs", "files"] } + } + }, + { + "name": "Read Log File (JSON)", + "request": { + "method": "GET", + "header": [{ "key": "Accept", "value": "application/json" }], + "url": { + "raw": "{{base_url}}/api/logs/files/LOGS_03082026.log", + "host": ["{{base_url}}"], + "path": ["api", "logs", "files", "LOGS_03082026.log"] + }, + "description": "Set Accept: application/json to get raw JSON — omit it (browser navigation) and the server redirects to the readable HTML viewer instead." + } + }, + { + "name": "Open Log Viewer (HTML)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/logs/view?file=LOGS_03082026.log", + "host": ["{{base_url}}"], + "path": ["api", "logs", "view"], + "query": [{ "key": "file", "value": "LOGS_03082026.log" }] + }, + "description": "Best opened directly in a browser (search, level filter, click-to-trace) rather than via Postman's response viewer." + } + }, + { + "name": "OpenAPI Spec (JSON)", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "{{base_url}}/openapi.json", "host": ["{{base_url}}"], "path": ["openapi.json"] } + } + }, + { + "name": "Open API Docs (Scalar)", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "{{base_url}}/docs", "host": ["{{base_url}}"], "path": ["docs"] }, + "description": "Best opened directly in a browser." + } + } + ] + }, + { + "name": "Dev/Test (LOG_EXPOSE_API only)", + "item": [ + { + "name": "Echo", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/echo", "host": ["{{base_url}}"], "path": ["api", "echo"] }, + "description": "Mock endpoint for testing ERP-style callbacks — just echoes back what it receives.", + "body": { "mode": "raw", "raw": "{\n \"mercant_id\": \"TEST-MERCHANT\",\n \"status_code\": \"200\",\n \"nominal\": \"150000\",\n \"signature\": \"test\"\n}", "options": { "raw": { "language": "json" } } } + } + }, + { + "name": "Echo 2 (multi-URL testing)", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/echo2", "host": ["{{base_url}}"], "path": ["api", "echo2"] }, + "body": { "mode": "raw", "raw": "{\n \"mercant_id\": \"TEST-MERCHANT\",\n \"status_code\": \"200\",\n \"nominal\": \"150000\",\n \"signature\": \"test\"\n}", "options": { "raw": { "language": "json" } } } + } + }, + { + "name": "Manually Trigger ERP Notify", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "url": { "raw": "{{base_url}}/api/test/notify-erp", "host": ["{{base_url}}"], "path": ["api", "test", "notify-erp"] }, + "description": "Bypasses the webhook entirely and calls notifyERP() directly — useful for testing ERP_NOTIFICATION_URL(S)/ERP_CLIENT_ID wiring in isolation.", + "body": { "mode": "raw", "raw": "{\n \"orderId\": \"ERPSKRIP-2608030000000627.TKG-260803000063\",\n \"nominal\": \"179000\",\n \"mercant_id\": \"ERPSKRIP-2608030000000627\"\n}", "options": { "raw": { "language": "json" } } } + } + } + ] } ] }