first commit
This commit is contained in:
18
app/config/database.js
Normal file
18
app/config/database.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
require('dotenv').config();
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
process.env.DB_NAME,
|
||||
process.env.DB_USER,
|
||||
process.env.DB_PASS,
|
||||
{
|
||||
host: process.env.DB_HOST,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||
define: {
|
||||
timestamps: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = sequelize;
|
||||
22
app/config/midtrans.js
Normal file
22
app/config/midtrans.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const midtransClient = require('midtrans-client');
|
||||
require('dotenv').config();
|
||||
|
||||
class MidtransConfig {
|
||||
static getSnapClient() {
|
||||
return new midtransClient.Snap({
|
||||
isProduction: process.env.MIDTRANS_IS_PRODUCTION === 'true',
|
||||
serverKey: process.env.MIDTRANS_SERVER_KEY,
|
||||
clientKey: process.env.MIDTRANS_CLIENT_KEY
|
||||
});
|
||||
}
|
||||
|
||||
static getCoreApiClient() {
|
||||
return new midtransClient.CoreApi({
|
||||
isProduction: process.env.MIDTRANS_IS_PRODUCTION === 'true',
|
||||
serverKey: process.env.MIDTRANS_SERVER_KEY,
|
||||
clientKey: process.env.MIDTRANS_CLIENT_KEY
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MidtransConfig;
|
||||
32
app/core/midtrans.service.js
Normal file
32
app/core/midtrans.service.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const midtransClient = require('midtrans-client');
|
||||
require('dotenv').config();
|
||||
|
||||
class MidtransCoreService {
|
||||
constructor() {
|
||||
this.isProduction = process.env.MIDTRANS_IS_PRODUCTION === 'true';
|
||||
this.serverKey = process.env.MIDTRANS_SERVER_KEY;
|
||||
this.clientKey = process.env.MIDTRANS_CLIENT_KEY;
|
||||
|
||||
this.coreApi = new midtransClient.CoreApi({
|
||||
isProduction: this.isProduction,
|
||||
serverKey: this.serverKey,
|
||||
clientKey: this.clientKey
|
||||
});
|
||||
|
||||
this.snap = new midtransClient.Snap({
|
||||
isProduction: this.isProduction,
|
||||
serverKey: this.serverKey,
|
||||
clientKey: this.clientKey
|
||||
});
|
||||
}
|
||||
|
||||
getCoreApi() {
|
||||
return this.coreApi;
|
||||
}
|
||||
|
||||
getSnap() {
|
||||
return this.snap;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MidtransCoreService();
|
||||
24
app/core/payment.service.js
Normal file
24
app/core/payment.service.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const MidtransConfig = require('../../config/midtrans');
|
||||
const coreApi = MidtransConfig.getCoreApiClient();
|
||||
|
||||
class BasePaymentService {
|
||||
static async charge(payload) {
|
||||
try {
|
||||
const response = await coreApi.charge(payload);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Midtrans Charge Error:', error.ApiResponse || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getStatus(orderId) {
|
||||
try {
|
||||
return await coreApi.transaction.status(orderId);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BasePaymentService;
|
||||
76
app/helpers/payment_logger.helper.js
Normal file
76
app/helpers/payment_logger.helper.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const db = require('../../models/migration');
|
||||
|
||||
class PaymentLoggerHelper {
|
||||
async log(midtransResponse, requestPayload = null, type = 'PAYMENT') {
|
||||
const orderIdInternal = requestPayload ? requestPayload.order_id : null;
|
||||
const orderIdExternal = midtransResponse.order_id || (requestPayload ? requestPayload.external_id : null);
|
||||
|
||||
const grossAmount = midtransResponse.gross_amount || (requestPayload ? requestPayload.amount : null);
|
||||
const transactionStatus = midtransResponse.transaction_status || 'pending';
|
||||
const paymentType = midtransResponse.payment_type || (requestPayload ? 'snap' : null);
|
||||
const transactionId = midtransResponse.transaction_id || null;
|
||||
|
||||
if (!orderIdInternal && !orderIdExternal) {
|
||||
console.warn(`[PaymentLogger] Tidak ada ID (internal/external) ditemukan. Lewati pencatatan.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let order = null;
|
||||
|
||||
if (orderIdInternal) {
|
||||
order = await db.Order.findByPk(orderIdInternal);
|
||||
}
|
||||
|
||||
if (!order && orderIdExternal) {
|
||||
order = await db.Order.findOne({
|
||||
where: { orderID: orderIdExternal }
|
||||
});
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
console.warn(`[PaymentLogger] Order tidak ditemukan untuk Internal ID: ${orderIdInternal} atau External ID: ${orderIdExternal}.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let mappedStatus = 'PENDING';
|
||||
if (['settlement', 'capture'].includes(transactionStatus)) {
|
||||
mappedStatus = 'SUCCESS';
|
||||
} else if (['deny', 'cancel', 'expire'].includes(transactionStatus)) {
|
||||
mappedStatus = 'FAILED';
|
||||
}
|
||||
|
||||
const payment = await db.Payment.create({
|
||||
order_id: order.id,
|
||||
user_id: order.user_id || null,
|
||||
amount: order.total || grossAmount,
|
||||
response_data: JSON.stringify(midtransResponse),
|
||||
currency: 'IDR',
|
||||
description: midtransResponse.status_message || `Payment for order ${orderIdExternal || order.id}`,
|
||||
type: type,
|
||||
channel_code: paymentType,
|
||||
channel_name: paymentType,
|
||||
channel_type: this._getChannelType(paymentType),
|
||||
status: mappedStatus,
|
||||
third_party: 'MIDTRANS',
|
||||
external_id: orderIdExternal || transactionId
|
||||
});
|
||||
|
||||
console.log(`[PaymentLogger] Berhasil mencatat pembayaran untuk Order: ${order.id} (External: ${orderIdExternal})`);
|
||||
return payment;
|
||||
} catch (error) {
|
||||
console.error(`[PaymentLogger] Gagal mencatat pembayaran: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_getChannelType(paymentType) {
|
||||
if (!paymentType) return null;
|
||||
if (paymentType.includes('va') || paymentType.includes('bank') || paymentType.includes('echannel')) return 'BANK';
|
||||
if (['gopay', 'shopeepay', 'qris'].includes(paymentType)) return 'E_WALLET';
|
||||
if (paymentType.includes('credit_card')) return 'CREDIT_CARD';
|
||||
return 'QRIS';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PaymentLoggerHelper();
|
||||
24
app/helpers/response.helper.js
Normal file
24
app/helpers/response.helper.js
Normal file
@@ -0,0 +1,24 @@
|
||||
class ResponseHelper {
|
||||
static success(res, message, data = {}, status = 200) {
|
||||
return res.status(status).json({
|
||||
success: true,
|
||||
message,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
static error(res, message, status = 500, errors = null) {
|
||||
const response = {
|
||||
success: false,
|
||||
message
|
||||
};
|
||||
|
||||
if (errors) {
|
||||
response.errors = errors;
|
||||
}
|
||||
|
||||
return res.status(status).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResponseHelper;
|
||||
@@ -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;
|
||||
8
app/modules/bank-transfer/routes/bank-transfer.route.js
Normal file
8
app/modules/bank-transfer/routes/bank-transfer.route.js
Normal 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;
|
||||
56
app/modules/bank-transfer/services/bank-transfer.service.js
Normal file
56
app/modules/bank-transfer/services/bank-transfer.service.js
Normal 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();
|
||||
@@ -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;
|
||||
8
app/modules/credit-card/routes/credit-card.route.js
Normal file
8
app/modules/credit-card/routes/credit-card.route.js
Normal 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;
|
||||
48
app/modules/credit-card/services/credit-card.service.js
Normal file
48
app/modules/credit-card/services/credit-card.service.js
Normal 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();
|
||||
24
app/modules/cstore/controllers/cstore.controller.js
Normal file
24
app/modules/cstore/controllers/cstore.controller.js
Normal 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;
|
||||
8
app/modules/cstore/routes/cstore.route.js
Normal file
8
app/modules/cstore/routes/cstore.route.js
Normal 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;
|
||||
57
app/modules/cstore/services/cstore.service.js
Normal file
57
app/modules/cstore/services/cstore.service.js
Normal 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();
|
||||
24
app/modules/e-wallet/controllers/e-wallet.controller.js
Normal file
24
app/modules/e-wallet/controllers/e-wallet.controller.js
Normal 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;
|
||||
8
app/modules/e-wallet/routes/e-wallet.route.js
Normal file
8
app/modules/e-wallet/routes/e-wallet.route.js
Normal 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;
|
||||
57
app/modules/e-wallet/services/e-wallet.service.js
Normal file
57
app/modules/e-wallet/services/e-wallet.service.js
Normal 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();
|
||||
19
app/modules/invoice/controllers/invoice.controller.js
Normal file
19
app/modules/invoice/controllers/invoice.controller.js
Normal 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;
|
||||
@@ -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;
|
||||
8
app/modules/notification/routes/notification.route.js
Normal file
8
app/modules/notification/routes/notification.route.js
Normal 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;
|
||||
93
app/modules/notification/services/notification.service.js
Normal file
93
app/modules/notification/services/notification.service.js
Normal 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();
|
||||
24
app/modules/paylater/controllers/paylater.controller.js
Normal file
24
app/modules/paylater/controllers/paylater.controller.js
Normal 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;
|
||||
8
app/modules/paylater/routes/paylater.route.js
Normal file
8
app/modules/paylater/routes/paylater.route.js
Normal 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;
|
||||
45
app/modules/paylater/services/paylater.service.js
Normal file
45
app/modules/paylater/services/paylater.service.js
Normal 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();
|
||||
23
app/modules/snap/controllers/snap.controller.js
Normal file
23
app/modules/snap/controllers/snap.controller.js
Normal 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;
|
||||
8
app/modules/snap/routes/snap.route.js
Normal file
8
app/modules/snap/routes/snap.route.js
Normal 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;
|
||||
45
app/modules/snap/services/snap.service.js
Normal file
45
app/modules/snap/services/snap.service.js
Normal 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();
|
||||
@@ -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;
|
||||
10
app/modules/transaction/routes/transaction.route.js
Normal file
10
app/modules/transaction/routes/transaction.route.js
Normal 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;
|
||||
40
app/modules/transaction/services/transaction.service.js
Normal file
40
app/modules/transaction/services/transaction.service.js
Normal 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();
|
||||
Reference in New Issue
Block a user