first commit

This commit is contained in:
2026-02-18 16:03:43 +07:00
commit c52ecd6514
54 changed files with 8270 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
const bankTransferService = require('../services/bank-transfer.service');
const responseHelper = require('../../../helpers/response.helper');
class BankTransferController {
static async charge(req, res) {
try {
const { bank } = req.params;
const payload = req.body;
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await bankTransferService.createTransaction(payload, bank.toLowerCase());
return responseHelper.success(res, `Transaksi Virtual Account ${bank.toUpperCase()} berhasil dibuat`, result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = BankTransferController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const bankTransferController = require('../controllers/bank-transfer.controller');
router.post('/:bank', bankTransferController.charge);
module.exports = router;

View File

@@ -0,0 +1,56 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class BankTransferService {
async createTransaction(payload, bank) {
let { order_id, external_id, amount, customer_details } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
payment_type: "bank_transfer",
transaction_details: {
order_id: external_id,
gross_amount: amount
},
bank_transfer: {
bank: bank
},
customer_details: customer_details,
custom_expiry: {
expiry_duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
if (bank === 'mandiri') {
parameter.payment_type = "echannel";
parameter.echannel = {
bill_info1: "Payment For:",
bill_info2: "Invoice " + (external_id || order_id)
};
delete parameter.bank_transfer;
}
try {
const response = await midtransCore.getCoreApi().charge(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans Error (${bank}): ${error.message}`);
}
}
}
module.exports = new BankTransferService();

View File

@@ -0,0 +1,27 @@
const creditCardService = require('../services/credit-card.service');
const responseHelper = require('../../../helpers/response.helper');
class CreditCardController {
static async charge(req, res) {
try {
const payload = req.body;
if (!payload.credit_card_token) {
return responseHelper.error(res, "credit_card_token wajib disertakan (dapatkan dari frontend Midtrans JS)", 400);
}
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await creditCardService.createTransaction(payload);
return responseHelper.success(res, "Transaksi Credit Card berhasil diinisialisasi", result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = CreditCardController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const creditCardController = require('../controllers/credit-card.controller');
router.post('/charge', creditCardController.charge);
module.exports = router;

View File

@@ -0,0 +1,48 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class CreditCardService {
async createTransaction(payload) {
let { order_id, external_id, amount, customer_details, credit_card_token } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
payment_type: "credit_card",
transaction_details: {
order_id: external_id,
gross_amount: amount
},
credit_card: {
token_id: credit_card_token,
authentication: true
},
customer_details: customer_details,
custom_expiry: {
expiry_duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
try {
const response = await midtransCore.getCoreApi().charge(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans Credit Card Error: ${error.message}`);
}
}
}
module.exports = new CreditCardService();

View File

@@ -0,0 +1,24 @@
const cstoreService = require('../services/cstore.service');
const responseHelper = require('../../../helpers/response.helper');
class CStoreController {
static async charge(req, res) {
try {
const { store } = req.params;
const payload = req.body;
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await cstoreService.createTransaction(payload, store.toLowerCase());
return responseHelper.success(res, `Transaksi ${store.toUpperCase()} berhasil dibuat`, result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = CStoreController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const cstoreController = require('../controllers/cstore.controller');
router.post('/:store', cstoreController.charge);
module.exports = router;

View File

@@ -0,0 +1,57 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class CStoreService {
async createTransaction(payload, store) {
let { order_id, external_id, amount, customer_details } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
payment_type: store,
transaction_details: {
order_id: external_id,
gross_amount: amount
},
customer_details: customer_details,
custom_expiry: {
expiry_duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
if (store === 'indomaret') {
parameter.indomaret = {
message: "Pembayaran untuk Invoice " + (external_id || order_id)
};
}
if (store === 'alfamart') {
parameter.alfamart = {
alfamart_free_text_1: "Terima kasih telah berbelanja",
alfamart_free_text_2: "Simpan struk Anda"
};
}
try {
const response = await midtransCore.getCoreApi().charge(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans Error (${store}): ${error.message}`);
}
}
}
module.exports = new CStoreService();

View File

@@ -0,0 +1,24 @@
const eWalletService = require('../services/e-wallet.service');
const responseHelper = require('../../../helpers/response.helper');
class EWalletController {
static async charge(req, res) {
try {
const { type } = req.params;
const payload = req.body;
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await eWalletService.createTransaction(payload, type.toLowerCase());
return responseHelper.success(res, `Transaksi ${type.toUpperCase()} berhasil dibuat`, result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = EWalletController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const eWalletController = require('../controllers/e-wallet.controller');
router.post('/:type', eWalletController.charge);
module.exports = router;

View File

@@ -0,0 +1,57 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class EWalletService {
async createTransaction(payload, type) {
let { order_id, external_id, amount, customer_details } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
payment_type: type,
transaction_details: {
order_id: external_id,
gross_amount: amount
},
customer_details: customer_details,
custom_expiry: {
expiry_duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
if (type === 'gopay') {
parameter.gopay = {
enable_callback: true,
callback_url: "https://your-app.com/"
};
}
if (type === 'shopeepay') {
parameter.shopeepay = {
callback_url: "https://your-app.com/"
};
}
try {
const response = await midtransCore.getCoreApi().charge(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans Error (${type}): ${error.message}`);
}
}
}
module.exports = new EWalletService();

View File

@@ -0,0 +1,19 @@
class InvoiceController {
static async getAll(req, res) {
try {
res.json({ message: "Mengambil semua data invoice" });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
static async getById(req, res) {
try {
res.json({ message: `Mengambil data invoice dengan ID: ${req.params.id}` });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
}
module.exports = InvoiceController;

View File

@@ -0,0 +1,24 @@
const notificationService = require('../services/notification.service');
const responseHelper = require('../../../helpers/response.helper');
class NotificationController {
static async handle(req, res) {
const payload = req.body;
try {
const isValid = notificationService.verifySignature(payload);
if (!isValid) {
return responseHelper.error(res, "Invalid Signature Key", 403);
}
await notificationService.processNotification(payload);
return responseHelper.success(res, "Notification Handled");
} catch (error) {
console.error("Webhook Error:", error.message);
return responseHelper.error(res, error.message);
}
}
}
module.exports = NotificationController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const notificationController = require('../controllers/notification.controller');
router.post('/handling', notificationController.handle);
module.exports = router;

View File

@@ -0,0 +1,93 @@
const crypto = require('crypto');
const db = require('../../../../models/migration');
require('dotenv').config();
class NotificationService {
verifySignature(payload) {
const { order_id, status_code, gross_amount, signature_key } = payload;
const serverKey = process.env.MIDTRANS_SERVER_KEY;
const data = order_id + status_code + gross_amount + serverKey;
const hash = crypto.createHash('sha512').update(data).digest('hex');
return hash === signature_key;
}
async processNotification(payload) {
const { order_id, transaction_status, fraud_status } = payload;
console.log(`[Notification] Processing Order ${order_id}: status=${transaction_status}, fraud=${fraud_status}`);
try {
let payment = await db.Payment.findOne({
where: { external_id: order_id },
order: [['created_at', 'DESC']]
});
let order = null;
if (payment) {
order = await db.Order.findByPk(payment.order_id);
} else {
order = await db.Order.findOne({ where: { orderID: order_id } });
if (!order) {
order = await db.Order.findByPk(order_id);
}
}
if (!order) {
console.warn(`[Notification] Data Order/Payment untuk ID ${order_id} tidak ditemukan.`);
return;
}
let orderPaymentStatus = 'PENDING';
let orderStatus = order.status;
let paymentRecordStatus = 'PENDING';
if (transaction_status === 'settlement' || (transaction_status === 'capture' && fraud_status === 'accept')) {
orderPaymentStatus = 'PAID';
orderStatus = 'COMPLETED';
paymentRecordStatus = 'SUCCESS';
} else if (['deny', 'cancel', 'expire'].includes(transaction_status)) {
orderPaymentStatus = 'REJECT';
orderStatus = 'FAILED';
paymentRecordStatus = 'FAILED';
} else if (transaction_status === 'pending') {
orderPaymentStatus = 'PENDING';
paymentRecordStatus = 'PENDING';
}
if (order.payment_status === orderPaymentStatus && order.status === orderStatus) {
console.log(`[Notification] Order ${order.id} sudah sinkron. Skip update.`);
return;
}
await order.update({
payment_status: orderPaymentStatus,
status: orderStatus
});
if (!payment) {
payment = await db.Payment.findOne({
where: { order_id: order.id },
order: [['created_at', 'DESC']]
});
}
if (payment) {
await payment.update({
status: paymentRecordStatus,
response_data: JSON.stringify(payload)
});
}
console.log(`[Notification] SUCCESS: Order ${order.id} updated to ${orderStatus}, Payment updated to ${paymentRecordStatus}`);
} catch (error) {
console.error(`[Notification] Error: ${error.message}`);
throw error;
}
}
}
module.exports = new NotificationService();

View File

@@ -0,0 +1,24 @@
const paylaterService = require('../services/paylater.service');
const responseHelper = require('../../../helpers/response.helper');
class PayLaterController {
static async charge(req, res) {
try {
const { type } = req.params;
const payload = req.body;
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await paylaterService.createTransaction(payload, type.toLowerCase());
return responseHelper.success(res, `Transaksi ${type.toUpperCase()} berhasil dibuat`, result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = PayLaterController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const paylaterController = require('../controllers/paylater.controller');
router.post('/:type', paylaterController.charge);
module.exports = router;

View File

@@ -0,0 +1,45 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class PayLaterService {
async createTransaction(payload, type) {
let { order_id, external_id, amount, customer_details, item_details } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
payment_type: type,
transaction_details: {
order_id: external_id,
gross_amount: amount
},
customer_details: customer_details,
item_details: item_details,
custom_expiry: {
expiry_duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
try {
const response = await midtransCore.getCoreApi().charge(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans PayLater Error (${type}): ${error.message}`);
}
}
}
module.exports = new PayLaterService();

View File

@@ -0,0 +1,23 @@
const snapService = require('../services/snap.service');
const responseHelper = require('../../../helpers/response.helper');
class SnapController {
static async createToken(req, res) {
try {
const payload = req.body;
if (!payload.order_id || !payload.external_id) {
return responseHelper.error(res, "order_id dan external_id wajib diisi", 400);
}
const result = await snapService.createTransaction(payload);
return responseHelper.success(res, "Snap Token berhasil dibuat", result);
} catch (error) {
console.error(error);
return responseHelper.error(res, error.message);
}
}
}
module.exports = SnapController;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const snapController = require('../controllers/snap.controller');
router.post('/token', snapController.createToken);
module.exports = router;

View File

@@ -0,0 +1,45 @@
const midtransCore = require('../../../core/midtrans.service');
const paymentLogger = require('../../../helpers/payment_logger.helper');
const db = require('../../../../models/migration');
class SnapService {
async createTransaction(payload) {
let { order_id, external_id, amount, customer_details, item_details } = payload;
if (!amount) {
let order = null;
if (order_id) order = await db.Order.findByPk(order_id);
if (!order && external_id) order = await db.Order.findOne({ where: { orderID: external_id } });
if (!order) throw new Error("Amount wajib diisi atau order_id/external_id tidak valid");
amount = order.total;
payload.amount = amount;
}
const parameter = {
transaction_details: {
order_id: external_id,
gross_amount: amount
},
customer_details: customer_details,
item_details: item_details,
usage_limit: 1,
expiry: {
duration: parseInt(process.env.MIDTRANS_EXPIRY_DURATION || 24),
unit: process.env.MIDTRANS_EXPIRY_UNIT || 'hour'
}
};
try {
const response = await midtransCore.getSnap().createTransaction(parameter);
await paymentLogger.log(response, payload);
return response;
} catch (error) {
throw new Error(`Midtrans Snap Error: ${error.message}`);
}
}
}
module.exports = new SnapService();

View File

@@ -0,0 +1,39 @@
const transactionService = require('../services/transaction.service');
const responseHelper = require('../../../helpers/response.helper');
class TransactionController {
static async getStatus(req, res) {
try {
const { order_id } = req.params;
const result = await transactionService.getStatus(order_id);
return responseHelper.success(res, "Status transaksi berhasil diambil", result);
} catch (error) {
return responseHelper.error(res, error.message);
}
}
static async refund(req, res) {
try {
const payload = req.body;
if (!payload.order_id || !payload.amount) {
return responseHelper.error(res, "order_id dan amount wajib diisi untuk refund", 400);
}
const result = await transactionService.refund(payload);
return responseHelper.success(res, "Permintaan refund berhasil diproses", result);
} catch (error) {
return responseHelper.error(res, error.message);
}
}
static async cancel(req, res) {
try {
const { order_id } = req.params;
const result = await transactionService.cancel(order_id);
return responseHelper.success(res, "Transaksi berhasil dibatalkan", result);
} catch (error) {
return responseHelper.error(res, error.message);
}
}
}
module.exports = TransactionController;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const transactionController = require('../controllers/transaction.controller');
router.get('/status/:order_id', transactionController.getStatus);
router.post('/refund', transactionController.refund);
router.post('/cancel/:order_id', transactionController.cancel);
module.exports = router;

View File

@@ -0,0 +1,40 @@
const midtransCore = require('../../../core/midtrans.service');
class TransactionService {
async getStatus(orderId) {
try {
const response = await midtransCore.getCoreApi().transaction.status(orderId);
return response;
} catch (error) {
throw new Error(`Gagal mengambil status transaksi: ${error.message}`);
}
}
async refund(payload) {
const { order_id, amount, reason, refund_key } = payload;
const parameter = {
refund_key: refund_key || `ref-${order_id}-${Date.now()}`,
amount: amount,
reason: reason || "Customer request"
};
try {
const response = await midtransCore.getCoreApi().transaction.refund(order_id, parameter);
return response;
} catch (error) {
throw new Error(`Gagal melakukan refund: ${error.message}`);
}
}
async cancel(orderId) {
try {
const response = await midtransCore.getCoreApi().transaction.cancel(orderId);
return response;
} catch (error) {
throw new Error(`Gagal membatalkan transaksi: ${error.message}`);
}
}
}
module.exports = new TransactionService();