299 lines
12 KiB
Markdown
299 lines
12 KiB
Markdown
# Simaya Midtrans Payment Server
|
|
|
|
Backend Express.js server untuk integrasi pembayaran Midtrans dengan sistem ERP.
|
|
|
|
> Dokumentasi endpoint yang selalu sinkron dengan kode: buka `/docs` (Scalar, HTTP Basic Auth) atau `/openapi.json`. Untuk testing manual, import `postman/Midtrans-Middleware.postman_collection.json` — mencakup seluruh route di bawah ini.
|
|
|
|
## Daftar Isi
|
|
|
|
- [Fitur Utama](#fitur-utama)
|
|
- [Konfigurasi Environment](#konfigurasi-environment)
|
|
- [API Endpoints](#api-endpoints)
|
|
- [Payment Flow](#payment-flow)
|
|
- [Testing](#testing)
|
|
- [Logging](#logging)
|
|
- [Keamanan](#keamanan)
|
|
|
|
## Fitur Utama
|
|
|
|
### 1. **Dual Mode Payment**
|
|
- **CORE API**: Bank Transfer, Credit Card, GoPay/QRIS, Convenience Store
|
|
- **SNAP**: Hosted payment interface dengan UI Midtrans
|
|
|
|
### 2. **Payment Link Generation**
|
|
- Dua bentuk payload: langsung (`order_id`/`nominal`/`customer`/`expire_at`) atau ERP (`mercant_id`/`item[]`)
|
|
- Signature HMAC-SHA256 pada token, TTL configurable
|
|
- `order_id` yang mengandung karakter selain `A-Za-z0-9-_~.` (mis. `:`) otomatis disanitasi jadi `.` sebelum dikirim ke Midtrans
|
|
|
|
### 3. **ERP Integration**
|
|
- Notifikasi otomatis ke sistem ERP setelah pembayaran sukses
|
|
- Multi-endpoint support (comma-separated URLs)
|
|
- Signature verification untuk keamanan
|
|
|
|
### 4. **Webhook Handler**
|
|
- Unified webhook untuk CORE dan SNAP
|
|
- Signature verification
|
|
- Idempotent notification handling
|
|
|
|
### 5. **State Persisten**
|
|
- `activeOrders`, `notifiedOrders`, `orderRetryCount`, `orderMerchantId` disimpan ke `server/data/state.json` (auto-save), jadi tidak hilang saat server restart
|
|
|
|
### 6. **Advanced Logging**
|
|
- Level-based logging (debug, info, warn, error), file harian (`LOGS_DDMMYYYY.log`)
|
|
- Auto-delete log lebih tua dari `LOG_RETENTION_DAYS`
|
|
- Log viewer interaktif di `/api/logs/view` (search, filter level, click-to-trace)
|
|
- Payload masking untuk sensitive data, Jakarta timezone (WIB/UTC+7)
|
|
|
|
## Konfigurasi Environment
|
|
|
|
Referensi lengkap ada di `.env.example` di root project. Ringkasan per kategori:
|
|
|
|
### Midtrans Configuration
|
|
```env
|
|
MIDTRANS_SERVER_KEY=your-server-key
|
|
MIDTRANS_CLIENT_KEY=your-client-key
|
|
MIDTRANS_IS_PRODUCTION=false
|
|
|
|
ENABLE_BANK_TRANSFER=true
|
|
ENABLE_CREDIT_CARD=true
|
|
ENABLE_GOPAY=true
|
|
ENABLE_CSTORE=true
|
|
```
|
|
|
|
### Payment Link & External API
|
|
```env
|
|
EXTERNAL_API_KEY=your-api-key # X-API-KEY untuk /createtransaksi & POST /api/payment-links
|
|
PAYMENT_LINK_SECRET=your-signing-secret # HMAC secret untuk token link
|
|
PAYMENT_LINK_TTL_MINUTES=1440
|
|
PAYMENT_LINK_BASE=http://localhost:5174/pay
|
|
```
|
|
|
|
### ERP Integration
|
|
```env
|
|
ERP_NOTIFICATION_URL=https://your-erp.com/api/payment-notification
|
|
ERP_NOTIFICATION_URLS=https://erp1.com/api/notif,https://erp2.com/api/notif # multi-URL, opsional
|
|
ERP_CLIENT_ID=your-erp-client-id # juga dipakai sebagai secret HMAC (fallback ERP_CLIENT_SECRET)
|
|
ERP_ENABLE_NOTIF=true
|
|
```
|
|
|
|
### Logging & Admin Access
|
|
```env
|
|
LOG_LEVEL=info
|
|
LOG_EXPOSE_API=false # default sudah tertutup (false) kalau tidak di-set
|
|
LOG_BASIC_AUTH_USER= # wajib diisi di production untuk lindungi /api/logs* dan /docs
|
|
LOG_BASIC_AUTH_PASS=
|
|
LOG_RETENTION_DAYS=30
|
|
LOG_BUFFER_SIZE=1000
|
|
```
|
|
|
|
### CORS & Rate Limiting
|
|
```env
|
|
CORS_ALLOWED_ORIGINS=https://your-frontend.example.com # kosong = izinkan semua origin (dev default)
|
|
RATE_LIMIT_WINDOW_MS=60000
|
|
RATE_LIMIT_MAX=20
|
|
```
|
|
|
|
### Server
|
|
```env
|
|
PORT=8000
|
|
NODE_ENV=production # HARUS "production" di server production — banyak gerbang keamanan bergantung pada ini
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
### Health & Config
|
|
|
|
#### `GET /api/health`
|
|
Health check endpoint.
|
|
|
|
#### `GET /api/config`
|
|
Ambil payment toggles & Midtrans client key saat ini.
|
|
|
|
#### `POST /api/config` (non-production only)
|
|
Update payment toggles saat runtime. Return 403 kalau `NODE_ENV=production`.
|
|
|
|
### Payment Operations
|
|
|
|
#### `POST /api/payments/charge`
|
|
Buat transaksi Core API (bank transfer, card, GoPay/QRIS, cstore). Dipanggil langsung dari browser checkout, tanpa perlu pra-registrasi order. **Rate-limited.**
|
|
|
|
#### `POST /api/payments/snap/token`
|
|
Generate Snap token untuk hosted payment popup. Dipanggil langsung dari browser (`PayPage`/`CheckoutPage`). **Rate-limited.**
|
|
|
|
#### `GET /api/payments/:orderId/status`
|
|
Cek status transaksi (passthrough Midtrans). Juga memicu fallback notifikasi ERP kalau status sudah sukses tapi belum ter-notify.
|
|
|
|
### Payment Link
|
|
|
|
#### `POST /api/payment-links` — payload langsung
|
|
Requires `X-API-KEY`. **Rate-limited.**
|
|
```json
|
|
{
|
|
"order_id": "DEMO-MERCHANT-0001:ITEM-0001",
|
|
"nominal": 179000,
|
|
"customer": { "name": "Budi Santoso", "phone": "081234567890", "email": "budi.santoso@example.com" },
|
|
"expire_at": 1785852063058
|
|
}
|
|
```
|
|
`order_id` dipakai apa adanya untuk tracking internal; versi yang dikirim ke Midtrans disanitasi (`:` dan karakter lain → `.`). `expire_at` dipakai kalau valid & di masa depan, kalau tidak fallback ke `PAYMENT_LINK_TTL_MINUTES`.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "status": "200", "messages": "SUCCESS", "data": { "url": "...", "order_id": "...", "midtrans_order_id": "...", "expire_at": 1785852063058 } }
|
|
```
|
|
|
|
#### `POST /createtransaksi` — payload ERP (mercant_id/item)
|
|
Requires `X-API-KEY`. **Rate-limited.**
|
|
```json
|
|
{
|
|
"mercant_id": "merchant-001",
|
|
"nominal": 150000,
|
|
"nama": "John Doe",
|
|
"email": "john@example.com",
|
|
"no_telepon": "081234567890",
|
|
"item": [{ "item_id": "product-123", "nama": "Product Name", "harga": 150000, "qty": 1 }],
|
|
"allowed_methods": ["bank_transfer", "gopay"]
|
|
}
|
|
```
|
|
`order_id` diturunkan sebagai `mercant_id.item[0].item_id` (dot-joined). `expire_at` TIDAK bisa di-supply client — dihitung server dari `PAYMENT_LINK_TTL_MINUTES`.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "status": "200", "messages": "SUCCESS", "data": { "url": "http://localhost:5174/pay/eyJ2Ijox..." } }
|
|
```
|
|
|
|
#### `GET /api/payment-links/:token`
|
|
Resolve token jadi detail order: `{ order_id, nominal, customer, expire_at, allowed_methods }`.
|
|
|
|
### Webhook
|
|
|
|
#### `POST /api/payments/notification`
|
|
Handler webhook Midtrans (unified CORE & SNAP). Verifikasi `signature_key` (`SHA512(order_id+status_code+gross_amount+MIDTRANS_SERVER_KEY)`), lalu meneruskan notifikasi ke ERP. Return non-2xx kalau notifikasi ERP gagal supaya Midtrans retry.
|
|
|
|
### Logs & Docs (internal — HTTP Basic Auth di production)
|
|
|
|
| Endpoint | Keterangan |
|
|
|---|---|
|
|
| `GET /api/logs` | Log in-memory terbaru (`?limit`, `?level`, `?q`) |
|
|
| `GET /api/logs/files` | Daftar file log harian |
|
|
| `GET /api/logs/files/:filename` | Isi satu file log (JSON), auto-redirect ke viewer HTML kalau diakses dari browser |
|
|
| `GET /api/logs/view?file=...` | Viewer interaktif: filter level, cari teks, klik nilai apa pun untuk trace |
|
|
| `GET /openapi.json` | Spec OpenAPI mentah |
|
|
| `GET /docs` | Dokumentasi API interaktif (Scalar) |
|
|
|
|
### Dev/Test (aktif hanya kalau `LOG_EXPOSE_API=true`)
|
|
|
|
- `POST /api/echo`, `POST /api/echo2` — mock endpoint untuk testing callback ERP
|
|
- `POST /api/test/notify-erp` — trigger `notifyERP()` langsung, bypass webhook
|
|
|
|
## Payment Flow
|
|
|
|
### 1. Payment Link Creation Flow
|
|
```
|
|
ERP System → POST /createtransaksi atau POST /api/payment-links → Server generate token → Payment URL
|
|
```
|
|
|
|
### 2. Payment Execution Flow
|
|
```
|
|
Customer → Payment URL → Frontend resolve token →
|
|
Pilih metode → POST /api/payments/charge atau /api/payments/snap/token → Midtrans
|
|
```
|
|
|
|
### 3. Payment Completion Flow
|
|
```
|
|
Midtrans → Webhook → Verifikasi signature → Update ledger →
|
|
Notify ERP → Tandai order selesai
|
|
```
|
|
|
|
### 4. Format Notifikasi ke ERP
|
|
```json
|
|
{
|
|
"mercant_id": "merchant-001",
|
|
"status_code": "200",
|
|
"nominal": "150000",
|
|
"signature": "sha512-hash"
|
|
}
|
|
```
|
|
**Perhitungan Signature:** `SHA512(mercant_id + status_code + nominal + ERP_CLIENT_ID)`
|
|
|
|
> Catatan: payload ke ERP **tidak** membawa `order_id` — pencocokan transaksi murni lewat `mercant_id`. Kalau satu `mercant_id` bisa punya lebih dari satu order aktif bersamaan, ERP perlu strategi lain untuk membedakannya.
|
|
|
|
## Testing
|
|
|
|
Lihat folder `tests/` untuk file-file testing:
|
|
|
|
```bash
|
|
node tests/test-create-payment-link.cjs
|
|
node tests/test-frontend-payload.cjs
|
|
node tests/test-snap-token.cjs
|
|
```
|
|
|
|
Atau pakai `postman/Midtrans-Middleware.postman_collection.json` — mencakup seluruh endpoint di atas, dikelompokkan per folder, termasuk request untuk mensimulasikan webhook Midtrans dari payload asli.
|
|
|
|
## Logging
|
|
|
|
### Log Levels
|
|
- **debug**: Detail untuk diagnosa masalah
|
|
- **info**: Informasi umum
|
|
- **warn**: Peringatan
|
|
- **error**: Error yang masih memungkinkan aplikasi jalan
|
|
|
|
### Format Log (file, satu baris per event)
|
|
```
|
|
[2026-08-03T21:35:54.746+07:00] [INFO ] webhook.notifying_erp | {"order_id":"DEMO-MERCHANT-0002.ITEM-0002","mercant_id":"DEMO-MERCHANT-0002"}
|
|
```
|
|
|
|
Paling nyaman dibaca lewat `GET /api/logs/view` (filter level, cari teks, klik nilai untuk trace) daripada membaca file mentah.
|
|
|
|
### Event Penting
|
|
|
|
**Payment Lifecycle:** `charge.request`, `charge.success`, `charge.error`, `status.request`, `webhook.received`
|
|
|
|
**Payment Link:** `payment-links.create.issued`, `createtransaksi.issued`, `paymentlink.resolve.success`
|
|
|
|
**ERP Integration:** `erp.notify.start`, `erp.notify.success`, `erp.notify.error`, `erp.notify.skip`
|
|
|
|
**Security:** `webhook.signature.invalid`, `createtransaksi.unauthorized`, `payment-links.create.unauthorized`, `admin.auth.failed`, `rate_limit.blocked`
|
|
|
|
## Keamanan
|
|
|
|
1. **Signature Verification** — webhook, token payment link, notifikasi ERP semuanya ditandatangani/diverifikasi.
|
|
2. **Idempotency** — cegah duplikasi order/notifikasi ERP; state disimpan konsisten pakai `order_id` yang sudah disanitasi (Midtrans-safe) di semua map internal (`activeOrders`, `notifiedOrders`, `orderRetryCount`, `orderMerchantId`), sehingga tidak ada mismatch walau `order_id` asli mengandung karakter seperti `:`.
|
|
3. **API Key Authentication** — `X-API-KEY` untuk `/createtransaksi` dan `POST /api/payment-links`. Dev mode fallback (bypass) hanya aktif kalau `NODE_ENV !== 'production'` **dan** key belum di-set.
|
|
4. **HTTP Basic Auth untuk admin tooling** — `/api/logs*`, `/openapi.json`, `/docs` semuanya dilindungi `requireAdminAuth` (`LOG_BASIC_AUTH_USER`/`PASS`). **Wajib di-set di production** — gerbang ini bergantung pada `NODE_ENV=production` untuk benar-benar dipaksa (lihat poin 8). Terpisah dari itu, `LOG_EXPOSE_API=false` (default) mematikan `/api/logs*` dengan 403 sekalipun auth-nya lolos, dan membuat endpoint dev/test (`/api/echo`, `/api/echo2`, `/api/test/notify-erp`) tidak terdaftar sama sekali — flag ini **tidak** memengaruhi `/openapi.json` atau `/docs`.
|
|
5. **Rate Limiting** — `/createtransaksi`, `/api/payment-links`, `/api/payments/charge`, `/api/payments/snap/token` dibatasi per-IP (`RATE_LIMIT_MAX` request per `RATE_LIMIT_WINDOW_MS`) untuk mencegah bot abuse / card-testing pada endpoint yang memang harus tetap bisa diakses langsung dari browser tanpa API key.
|
|
6. **CORS** — dibatasi lewat `CORS_ALLOWED_ORIGINS` (comma-separated). Kosong = izinkan semua origin (default dev).
|
|
7. **Payload Masking** — field sensitif (card number, CVV, token, server key) otomatis disamarkan di log.
|
|
8. **`NODE_ENV` production** — banyak gerbang keamanan (API key, Basic Auth) bergantung pada `isDevEnv()`. Pastikan proses production (`ecosystem.config.cjs`) benar-benar set `NODE_ENV=production`, lalu `pm2 restart <app> --update-env` setelah mengubahnya.
|
|
|
|
## Running the Server
|
|
|
|
```bash
|
|
npm install
|
|
npm run server # atau: node server/index.cjs
|
|
# Server jalan di http://localhost:8000
|
|
```
|
|
|
|
## Common Issues
|
|
|
|
### "Transaction already pending"
|
|
**Penyebab**: Order ID sudah punya transaksi pending di Midtrans.
|
|
**Solusi**: Pakai instruksi pembayaran yang ada, atau buat order baru dengan ID berbeda.
|
|
|
|
### "ERP notification failed"
|
|
**Penyebab**: Endpoint ERP tidak bisa diakses, atau signature mismatch (`ERP_CLIENT_ID` beda dengan yang dipakai ERP).
|
|
**Solusi**: Cek `ERP_NOTIFICATION_URL(S)` dan `ERP_CLIENT_ID`, replay payload webhook asli lewat request "Simulate Midtrans Notification (Local)" di Postman collection untuk debug.
|
|
|
|
### "Invalid signature on webhook"
|
|
**Penyebab**: `MIDTRANS_SERVER_KEY` salah, atau webhook dari sumber tak sah.
|
|
**Solusi**: Pastikan `MIDTRANS_SERVER_KEY` sama dengan akun Midtrans yang memproses transaksi tersebut (sandbox vs production beda key).
|
|
|
|
### Log/`/docs` mengembalikan 401 atau 503
|
|
**Penyebab**: `LOG_BASIC_AUTH_USER`/`PASS` belum di-set di production (`NODE_ENV=production`).
|
|
**Solusi**: Set kedua env var tersebut, restart server.
|
|
|
|
## Support
|
|
|
|
- [Midtrans API Documentation](https://docs.midtrans.com/)
|
|
- [Midtrans Node.js Library](https://github.com/Midtrans/midtrans-nodejs-client)
|