Yimaru-BackEnd/postman/Chapa-Subscription-Payments.postman_collection.json
Yared Yemane 1f7b38861e Integrate Chapa for learner subscription payments
Add Chapa checkout, verify, webhook, and callback flows so subscriptions activate only after confirmed payment. Route subscription checkout through Chapa while keeping ArifPay for direct payments. Include integration docs and a Postman collection.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 03:35:57 -07:00

544 lines
18 KiB
JSON

{
"info": {
"_postman_id": "c4a8f2e1-9b3d-4c7a-a1e6-chapa-payments-01",
"name": "Chapa Subscription Payments",
"description": "Postman collection for Yimaru LMS Chapa subscription payment flow.\n\n## Setup\n1. Set `base_url` (default `http://localhost:8080`).\n2. Set `learner_email`, `learner_password`, and `learner_phone`.\n3. Set `chapa_webhook_secret` (same as `CHAPA_WEBHOOK_SECRET` in `.env`).\n4. Run **Customer Login** to populate `access_token`.\n5. Run **List Subscription Plans** to populate `plan_id`.\n6. Run **Subscribe with Payment** — open `payment_url` in a browser and complete Chapa test checkout.\n7. Run **Verify Payment** (uses `tx_ref` saved as `session_id`).\n\n## Notes\n- `session_id` in verify/cancel paths is Chapa `tx_ref` (UUID returned at checkout).\n- Webhook request includes a pre-request script that signs the body with HMAC-SHA256.\n- See `docs/CHAPA_INTEGRATION.md` for dashboard webhook URL configuration.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{access_token}}",
"type": "string"
}
]
},
"variable": [
{
"key": "base_url",
"value": "http://localhost:8080"
},
{
"key": "access_token",
"value": ""
},
{
"key": "learner_email",
"value": "learner@example.com"
},
{
"key": "learner_password",
"value": "your-password"
},
{
"key": "learner_phone",
"value": "0912345678"
},
{
"key": "plan_id",
"value": "1"
},
{
"key": "payment_id",
"value": ""
},
{
"key": "tx_ref",
"value": ""
},
{
"key": "payment_url",
"value": ""
},
{
"key": "chapa_webhook_secret",
"value": ""
},
{
"key": "chapa_ref_id",
"value": "APqDvYw1okk2"
}
],
"item": [
{
"name": "00 - Auth",
"item": [
{
"name": "Customer Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const body = pm.response.json();",
"if (body.data && body.data.access_token) {",
" pm.collectionVariables.set('access_token', body.data.access_token);",
" pm.test('Access token saved', function () {",
" pm.expect(body.data.access_token).to.be.a('string').and.not.empty;",
" });",
"}"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"{{learner_email}}\",\n \"password\": \"{{learner_password}}\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/auth/customer-login",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "customer-login"]
},
"description": "Authenticates a learner and saves `access_token` for subsequent requests."
},
"response": []
}
]
},
{
"name": "01 - Subscription Plans",
"item": [
{
"name": "List Subscription Plans",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const body = pm.response.json();",
"if (Array.isArray(body.data) && body.data.length > 0) {",
" pm.collectionVariables.set('plan_id', String(body.data[0].id));",
"}"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/subscription-plans?active_only=true",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscription-plans"],
"query": [
{
"key": "active_only",
"value": "true"
}
]
},
"description": "Public list of active plans. Saves first plan `id` to `plan_id`."
},
"response": []
},
{
"name": "Get Subscription Plan by ID",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/subscription-plans/{{plan_id}}",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscription-plans", "{{plan_id}}"]
}
},
"response": []
}
]
},
{
"name": "02 - Chapa Payment Flow",
"item": [
{
"name": "Subscribe with Payment (Checkout)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const body = pm.response.json();",
"if (body.data) {",
" if (body.data.payment_id) {",
" pm.collectionVariables.set('payment_id', String(body.data.payment_id));",
" }",
" if (body.data.session_id) {",
" pm.collectionVariables.set('tx_ref', body.data.session_id);",
" }",
" if (body.data.payment_url) {",
" pm.collectionVariables.set('payment_url', body.data.payment_url);",
" }",
" pm.test('Payment URL returned', function () {",
" pm.expect(body.data.payment_url).to.be.a('string').and.not.empty;",
" });",
"}"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"plan_id\": {{plan_id}},\n \"phone\": \"{{learner_phone}}\",\n \"email\": \"{{learner_email}}\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/subscriptions/checkout",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscriptions", "checkout"]
},
"description": "Primary learner endpoint. Returns Chapa `payment_url`. Open it in a browser to complete payment. `session_id` in the response is the Chapa `tx_ref`."
},
"response": []
},
{
"name": "Initiate Subscription Payment",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const body = pm.response.json();",
"if (body.data) {",
" if (body.data.payment_id) pm.collectionVariables.set('payment_id', String(body.data.payment_id));",
" if (body.data.session_id) pm.collectionVariables.set('tx_ref', body.data.session_id);",
" if (body.data.payment_url) pm.collectionVariables.set('payment_url', body.data.payment_url);",
"}"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"plan_id\": {{plan_id}},\n \"phone\": \"{{learner_phone}}\",\n \"email\": \"{{learner_email}}\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/payments/subscribe",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "subscribe"]
},
"description": "Alias of checkout — same Chapa initialize flow."
},
"response": []
},
{
"name": "Verify Payment",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/verify/{{tx_ref}}",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "verify", "{{tx_ref}}"]
},
"description": "Verifies payment with Chapa using `tx_ref` (path param named `session_id` in the API). Run after completing checkout."
},
"response": []
},
{
"name": "Get My Payments",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments?limit=20&offset=0",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments"],
"query": [
{
"key": "limit",
"value": "20"
},
{
"key": "offset",
"value": "0"
}
]
}
},
"response": []
},
{
"name": "Get Payment by ID",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/{{payment_id}}",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "{{payment_id}}"]
}
},
"response": []
},
{
"name": "Cancel Pending Payment",
"request": {
"method": "POST",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/{{payment_id}}/cancel",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "{{payment_id}}", "cancel"]
},
"description": "Only works while payment status is PENDING."
},
"response": []
},
{
"name": "Get Chapa Payment Methods",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/methods",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "methods"]
}
},
"response": []
}
]
},
{
"name": "03 - Subscription Status",
"item": [
{
"name": "Get My Subscription",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/subscriptions/me",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscriptions", "me"]
},
"description": "Returns active subscription after successful payment."
},
"response": []
},
{
"name": "Check Subscription Status",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/subscriptions/status",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscriptions", "status"]
}
},
"response": []
},
{
"name": "Get Subscription History",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/subscriptions/history",
"host": ["{{base_url}}"],
"path": ["api", "v1", "subscriptions", "history"]
}
},
"response": []
}
]
},
{
"name": "04 - Chapa Webhooks (no auth)",
"item": [
{
"name": "Chapa Webhook (charge.success)",
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
"const secret = pm.collectionVariables.get('chapa_webhook_secret') || '';",
"const body = pm.request.body.raw || '';",
"if (!secret) {",
" console.warn('Set chapa_webhook_secret collection variable to sign the webhook');",
"}",
"const signature = CryptoJS.HmacSHA256(body, secret).toString(CryptoJS.enc.Hex);",
"pm.request.headers.upsert({ key: 'x-chapa-signature', value: signature });"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"event\": \"charge.success\",\n \"type\": \"API\",\n \"tx_ref\": \"{{tx_ref}}\",\n \"reference\": \"{{chapa_ref_id}}\",\n \"status\": \"success\",\n \"amount\": \"500.00\",\n \"currency\": \"ETB\",\n \"payment_method\": \"telebirr\",\n \"mode\": \"test\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/payments/webhook",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "webhook"]
},
"description": "Simulates Chapa webhook. Requires valid `tx_ref` from a real initialize call. Backend re-verifies with Chapa API before activating subscription. Set `chapa_webhook_secret` to match dashboard / `.env`."
},
"response": []
},
{
"name": "Chapa Callback (GET)",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/chapa/callback?trx_ref={{tx_ref}}&ref_id={{chapa_ref_id}}&status=success",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "chapa", "callback"],
"query": [
{
"key": "trx_ref",
"value": "{{tx_ref}}"
},
{
"key": "ref_id",
"value": "{{chapa_ref_id}}"
},
{
"key": "status",
"value": "success"
}
]
},
"description": "Simulates Chapa redirect to `CHAPA_CALLBACK_URL`. Uses same verify flow as webhook."
},
"response": []
}
]
},
{
"name": "05 - ArifPay Direct (legacy)",
"description": "OTP/direct payment flows still use ArifPay. Subscription checkout uses Chapa (folder 02).",
"item": [
{
"name": "Get Direct Payment Methods",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/v1/payments/direct/methods",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "direct", "methods"]
}
},
"response": []
},
{
"name": "Initiate Direct Payment",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"plan_id\": {{plan_id}},\n \"phone\": \"{{learner_phone}}\",\n \"email\": \"{{learner_email}}\",\n \"payment_method\": \"TELEBIRR\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/payments/direct",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "direct"]
}
},
"response": []
},
{
"name": "Verify Direct Payment OTP",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"session_id\": \"{{tx_ref}}\",\n \"otp\": \"123456\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/payments/direct/verify-otp",
"host": ["{{base_url}}"],
"path": ["api", "v1", "payments", "direct", "verify-otp"]
}
},
"response": []
}
]
}
]
}