diff --git a/.env.example b/.env.example index d6e1c8e..74948b9 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ -```dotenv PORT= NODE_ENV= @@ -16,4 +15,7 @@ DB_USER= DB_PASS= DB_NAME= DB_DIALECT= -``` \ No newline at end of file + +# Router Configuration +ROUTER_CALLBACK_TOKEN= +PROJECT_ID= \ No newline at end of file diff --git a/app/modules/bank-transfer/services/bank-transfer.service.js b/app/modules/bank-transfer/services/bank-transfer.service.js index 5cc5915..f84fe19 100644 --- a/app/modules/bank-transfer/services/bank-transfer.service.js +++ b/app/modules/bank-transfer/services/bank-transfer.service.js @@ -16,10 +16,13 @@ class BankTransferService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { payment_type: "bank_transfer", transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, bank_transfer: { diff --git a/app/modules/credit-card/services/credit-card.service.js b/app/modules/credit-card/services/credit-card.service.js index 05b82ac..8a5d0c9 100644 --- a/app/modules/credit-card/services/credit-card.service.js +++ b/app/modules/credit-card/services/credit-card.service.js @@ -16,10 +16,13 @@ class CreditCardService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { payment_type: "credit_card", transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, credit_card: { diff --git a/app/modules/cstore/services/cstore.service.js b/app/modules/cstore/services/cstore.service.js index f304e2d..36df9b0 100644 --- a/app/modules/cstore/services/cstore.service.js +++ b/app/modules/cstore/services/cstore.service.js @@ -16,10 +16,13 @@ class CStoreService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { payment_type: store, transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, customer_details: customer_details, diff --git a/app/modules/e-wallet/services/e-wallet.service.js b/app/modules/e-wallet/services/e-wallet.service.js index cd966a0..2b99a18 100644 --- a/app/modules/e-wallet/services/e-wallet.service.js +++ b/app/modules/e-wallet/services/e-wallet.service.js @@ -16,10 +16,13 @@ class EWalletService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { payment_type: type, transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, customer_details: customer_details, diff --git a/app/modules/notification/controllers/notification.controller.js b/app/modules/notification/controllers/notification.controller.js index ed77eaf..20dafb4 100644 --- a/app/modules/notification/controllers/notification.controller.js +++ b/app/modules/notification/controllers/notification.controller.js @@ -6,6 +6,11 @@ class NotificationController { const payload = req.body; try { + const callbackToken = req.headers['x-callback-token']; + if (callbackToken !== process.env.ROUTER_CALLBACK_TOKEN) { + return responseHelper.error(res, "Unauthorized token router gateway", 401); + } + const isValid = notificationService.verifySignature(payload); if (!isValid) { return responseHelper.error(res, "Invalid Signature Key", 403); diff --git a/app/modules/paylater/services/paylater.service.js b/app/modules/paylater/services/paylater.service.js index 499e968..9b5eaa4 100644 --- a/app/modules/paylater/services/paylater.service.js +++ b/app/modules/paylater/services/paylater.service.js @@ -16,10 +16,13 @@ class PayLaterService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { payment_type: type, transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, customer_details: customer_details, diff --git a/app/modules/snap/services/snap.service.js b/app/modules/snap/services/snap.service.js index 215dd37..1a91194 100644 --- a/app/modules/snap/services/snap.service.js +++ b/app/modules/snap/services/snap.service.js @@ -16,9 +16,12 @@ class SnapService { payload.amount = amount; } + const projectId = process.env.PROJECT_ID; + const midtransOrderId = projectId ? `${external_id}-${projectId}` : external_id; + const parameter = { transaction_details: { - order_id: external_id, + order_id: midtransOrderId, gross_amount: amount }, customer_details: customer_details, diff --git a/tests/unit/bank-transfer.service.test.js b/tests/unit/bank-transfer.service.test.js index 4e4d656..0378e37 100644 --- a/tests/unit/bank-transfer.service.test.js +++ b/tests/unit/bank-transfer.service.test.js @@ -21,13 +21,18 @@ describe('BankTransferService', () => { jest.clearAllMocks(); process.env.MIDTRANS_EXPIRY_DURATION = '1'; process.env.MIDTRANS_EXPIRY_UNIT = 'hour'; + process.env.PROJECT_ID = 'CBT'; }); it('should create a bank transfer transaction (BCA) successfully', async () => { const payload = { order_id: 'u-123', external_id: 'INV-001', amount: 10000 }; mockCharge.mockResolvedValue({ status_code: '201' }); const result = await bankTransferService.createTransaction(payload, 'bca'); - expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ payment_type: 'bank_transfer' })); + expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-001-CBT' + }) + })); expect(result.status_code).toBe('201'); }); diff --git a/tests/unit/credit-card.service.test.js b/tests/unit/credit-card.service.test.js index 922eb9c..b8443c8 100644 --- a/tests/unit/credit-card.service.test.js +++ b/tests/unit/credit-card.service.test.js @@ -18,13 +18,18 @@ const creditCardService = require('../../app/modules/credit-card/services/credit describe('CreditCardService', () => { beforeEach(() => { jest.clearAllMocks(); + process.env.PROJECT_ID = 'CBT'; }); it('should create a credit card transaction successfully', async () => { const payload = { order_id: 'u-cc', external_id: 'INV-CC-001', amount: 500000, credit_card_token: 't-123' }; mockCharge.mockResolvedValue({ status_code: '201', transaction_status: 'capture' }); const result = await creditCardService.createTransaction(payload); - expect(mockCharge).toHaveBeenCalled(); + expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-CC-001-CBT' + }) + })); expect(result.status_code).toBe('201'); }); diff --git a/tests/unit/cstore.service.test.js b/tests/unit/cstore.service.test.js index d19e0a5..5166935 100644 --- a/tests/unit/cstore.service.test.js +++ b/tests/unit/cstore.service.test.js @@ -17,13 +17,18 @@ const cstoreService = require('../../app/modules/cstore/services/cstore.service' describe('CStoreService', () => { beforeEach(() => { jest.clearAllMocks(); + process.env.PROJECT_ID = 'CBT'; }); it('should create an indomaret transaction successfully', async () => { const payload = { order_id: 'u-123', external_id: 'INV-I-001', amount: 50000 }; mockCharge.mockResolvedValue({ status_code: '201' }); const result = await cstoreService.createTransaction(payload, 'indomaret'); - expect(mockCharge).toHaveBeenCalled(); + expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-I-001-CBT' + }) + })); }); it('should create an alfamart transaction successfully', async () => { diff --git a/tests/unit/e-wallet.service.test.js b/tests/unit/e-wallet.service.test.js index 9bc99e8..130cbfa 100644 --- a/tests/unit/e-wallet.service.test.js +++ b/tests/unit/e-wallet.service.test.js @@ -17,13 +17,18 @@ const eWalletService = require('../../app/modules/e-wallet/services/e-wallet.ser describe('EWalletService', () => { beforeEach(() => { jest.clearAllMocks(); + process.env.PROJECT_ID = 'CBT'; }); it('should create a gopay transaction successfully', async () => { const payload = { order_id: 'u-123', external_id: 'INV-G-001', amount: 25000 }; mockCharge.mockResolvedValue({ status_code: '201' }); const result = await eWalletService.createTransaction(payload, 'gopay'); - expect(mockCharge).toHaveBeenCalled(); + expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-G-001-CBT' + }) + })); expect(result.status_code).toBe('201'); }); diff --git a/tests/unit/notification.controller.test.js b/tests/unit/notification.controller.test.js index aaf2701..833a202 100644 --- a/tests/unit/notification.controller.test.js +++ b/tests/unit/notification.controller.test.js @@ -8,17 +8,20 @@ const NotificationController = require('../../app/modules/notification/controlle describe('NotificationController', () => { let mockReq, mockRes; + const routerToken = 'TEST_ROUTER_TOKEN'; beforeEach(() => { jest.clearAllMocks(); + process.env.ROUTER_CALLBACK_TOKEN = routerToken; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; }); - it('should handle notification successfully', async () => { + it('should handle notification successfully when token and signature are valid', async () => { mockReq = { + headers: { 'x-callback-token': routerToken }, body: { order_id: 'I-123', transaction_status: 'settlement' } }; @@ -32,8 +35,22 @@ describe('NotificationController', () => { expect(responseHelper.success).toHaveBeenCalledWith(mockRes, expect.stringContaining('Handled')); }); + it('should return 401 if x-callback-token is invalid', async () => { + mockReq = { + headers: { 'x-callback-token': 'wrong-token' }, + body: {} + }; + + await NotificationController.handle(mockReq, mockRes); + + expect(responseHelper.error).toHaveBeenCalledWith(mockRes, expect.stringContaining('Unauthorized token'), 401); + }); + it('should return 403 if signature invalid', async () => { - mockReq = { body: { signature_key: 'invalid' } }; + mockReq = { + headers: { 'x-callback-token': routerToken }, + body: { signature_key: 'invalid' } + }; notificationService.verifySignature.mockReturnValue(false); await NotificationController.handle(mockReq, mockRes); @@ -42,7 +59,10 @@ describe('NotificationController', () => { }); it('should return error on service error', async () => { - mockReq = { body: { order_id: 'I-123' } }; + mockReq = { + headers: { 'x-callback-token': routerToken }, + body: { order_id: 'I-123' } + }; notificationService.verifySignature.mockReturnValue(true); notificationService.processNotification.mockRejectedValue(new Error('DB Error')); diff --git a/tests/unit/paylater.service.test.js b/tests/unit/paylater.service.test.js index d23a905..a891c8f 100644 --- a/tests/unit/paylater.service.test.js +++ b/tests/unit/paylater.service.test.js @@ -18,6 +18,7 @@ const paylaterService = require('../../app/modules/paylater/services/paylater.se describe('PayLaterService', () => { beforeEach(() => { jest.clearAllMocks(); + process.env.PROJECT_ID = 'CBT'; }); it('should create an akulaku transaction successfully', async () => { @@ -25,6 +26,9 @@ describe('PayLaterService', () => { mockCharge.mockResolvedValue({ status_code: '201' }); await paylaterService.createTransaction(payload, 'akulaku'); expect(mockCharge).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-AK-001-CBT' + }), payment_type: 'akulaku' })); }); diff --git a/tests/unit/snap.service.test.js b/tests/unit/snap.service.test.js index 3973f76..4a0f3d9 100644 --- a/tests/unit/snap.service.test.js +++ b/tests/unit/snap.service.test.js @@ -16,13 +16,18 @@ const snapService = require('../../app/modules/snap/services/snap.service'); describe('SnapService', () => { beforeEach(() => { jest.clearAllMocks(); + process.env.PROJECT_ID = 'CBT'; }); it('should create a snap transaction successfully', async () => { const payload = { order_id: 'u-snap', external_id: 'INV-S-001', amount: 100000 }; mockCreateTransaction.mockResolvedValue({ token: 't-123', redirect_url: 'http://m.com' }); const result = await snapService.createTransaction(payload); - expect(mockCreateTransaction).toHaveBeenCalled(); + expect(mockCreateTransaction).toHaveBeenCalledWith(expect.objectContaining({ + transaction_details: expect.objectContaining({ + order_id: 'INV-S-001-CBT' + }) + })); expect(result.token).toBe('t-123'); }); });