feat: Implement Mandiri payment processing and signature generation endpoints
This commit is contained in:
parent
b26494d147
commit
75b38bfa0d
|
|
@ -5,6 +5,7 @@ import crypto from "crypto";
|
|||
import db from "../db";
|
||||
import { get, set } from "lodash";
|
||||
import { hashToken } from "../middleware/auth";
|
||||
import generateSignatureService from "../lib/bank/mandiri/generateSignatureService";
|
||||
|
||||
const KEY_PATH =
|
||||
process.env.PRIVATE_KEY_PATH || path.join(__dirname, "../../secrets/key.pem");
|
||||
|
|
@ -309,3 +310,32 @@ export async function verifySignature(req: Request, res: Response) {
|
|||
return res.status(500).json({ error: err.message || "internal error" });
|
||||
}
|
||||
}
|
||||
export async function generateSignatureBank(req: Request, res: Response) {
|
||||
try {
|
||||
const data = req.body || {};
|
||||
const clientId = get(data, "client_id") || get(data, "clientId");
|
||||
const user = await db.users.findFirst({
|
||||
where: { clientbank_id: clientId },
|
||||
include: { database: true, parameters: true, tenants: true, banks: true },
|
||||
});
|
||||
const signatureService = await generateSignatureService({
|
||||
data: {
|
||||
isGenerateNewToken: true,
|
||||
client_secret: get(data, "client_secret") || "",
|
||||
endpointurl: get(data, "endpointurl") || "",
|
||||
body: get(data, "requestBody", {}),
|
||||
},
|
||||
user: user as any,
|
||||
});
|
||||
return res.json({
|
||||
...signatureService,
|
||||
algorithm: "RSA-SHA256",
|
||||
client_secret: get(data, "client_secret") || "",
|
||||
endpointurl: get(data, "endpointurl") || "",
|
||||
body: get(data, "requestBody", {}),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("generateSignature error", err);
|
||||
return res.status(500).json({ error: err.message || "internal error" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import path from "path";
|
|||
import { hashToken } from "../middleware/auth";
|
||||
import db from "../db";
|
||||
import { formatTimestamp } from "../lib/formatTimestamp";
|
||||
import callGate from "../lib/gate";
|
||||
import { getParameter } from "../lib/getParameter";
|
||||
import { get } from "lodash";
|
||||
|
||||
function loadCertForUser(user: any): string | null {
|
||||
// try deriving .cer from private_key_file
|
||||
|
|
@ -179,3 +182,56 @@ export async function b2bAccessToken(req: Request, res: Response) {
|
|||
.json({ responseCode: "5000000", responseMessage: "internal error" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function signatureBank(req: Request, res: Response) {
|
||||
try {
|
||||
const clientKey = (req.headers["x-token-key"] ||
|
||||
req.headers["X-TOKEN-KEY"] ||
|
||||
req.headers["x-token-key" as any]) as string;
|
||||
const data = req.body || {};
|
||||
const requiredHeader = ["X-TOKEN-KEY"];
|
||||
const requiredBody = ["endpointurl", "requestBody"];
|
||||
for (const h of requiredHeader) {
|
||||
if (!req.headers[h.toLowerCase()]) {
|
||||
return res.status(400).json({ error: `missing header ${h}` });
|
||||
}
|
||||
}
|
||||
for (const b of requiredBody) {
|
||||
if (!data[b]) {
|
||||
return res.status(400).json({ error: `missing body ${b}` });
|
||||
}
|
||||
}
|
||||
const user = await db.users.findFirst({
|
||||
where: { token_access: clientKey },
|
||||
include: {
|
||||
parameters: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
responseCode: "4017300",
|
||||
responseMessage: "Unauthorized. Client ID not found",
|
||||
});
|
||||
}
|
||||
const signature = await callGate({
|
||||
client: "mandiri",
|
||||
action: "generateSignatureService",
|
||||
data: {
|
||||
isGenerateNewToken: true,
|
||||
client_secret: getParameter(user, "client_secret") || "",
|
||||
endpointurl: get(data, "endpointurl", ""),
|
||||
body: get(data, "requestBody", {}),
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
...signature,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("b2bAccessToken error", err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ responseCode: "5000000", responseMessage: "internal error" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Request, Response } from "express";
|
||||
import callGateFromReq from "../lib/gateClient";
|
||||
import getTokenAuth from "../lib/getTokenAuth";
|
||||
import { getUserFromToken } from "../middleware/auth";
|
||||
import { dbQueryClient } from "../lib/dbQueryClient";
|
||||
|
|
@ -29,11 +28,26 @@ export async function inquiry(req: Request, res: Response) {
|
|||
return res.status(400).json({ error: `missing body field ${f}` });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await callGateFromReq(req, {
|
||||
const parameters = await db.parameters.findFirst({
|
||||
where: {
|
||||
param_key: "partnerServiceId",
|
||||
param_value: get(body, "partnerServiceId"),
|
||||
},
|
||||
});
|
||||
const user = await db.users.findFirst({
|
||||
where: { user_id: parameters?.user_id },
|
||||
include: {
|
||||
database: true,
|
||||
banks: true,
|
||||
parameters: true,
|
||||
tenants: true,
|
||||
},
|
||||
});
|
||||
const result = await callGate({
|
||||
client: "mandiri",
|
||||
action: "getInvoiceVirtualAccount",
|
||||
data: { ...body },
|
||||
user: user,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
|
|
@ -46,7 +60,9 @@ export async function inquiry(req: Request, res: Response) {
|
|||
}
|
||||
} catch (err) {
|
||||
console.error("transfer inquiry error:", err);
|
||||
return res.status(500).json({ error: "internal_error" });
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "internal_error", message: get(err, "message") });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { get } from "lodash";
|
||||
import { db } from "../../../db";
|
||||
import axios from "axios";
|
||||
import { formatTimestamp } from "../../formatTimestamp";
|
||||
import generateSignatureService from "./generateSignatureService";
|
||||
import { getParameter } from "../../getParameter";
|
||||
export default async function ({ data }: { data: any }) {
|
||||
const endpointurl = "/api/v1.0/transfer-va/payment";
|
||||
const user = await db.users.findFirst({
|
||||
where: {
|
||||
clientbank_id: data.clientbank_id,
|
||||
},
|
||||
include: { banks: true, database: true, parameters: true },
|
||||
});
|
||||
if (!user) throw new Error("User not found");
|
||||
const timestamp = formatTimestamp();
|
||||
const signatureService = await generateSignatureService({
|
||||
data: {
|
||||
accessToken: data.accessToken,
|
||||
timestamp,
|
||||
isGenerateNewToken: true,
|
||||
client_secret: getParameter(user, "client_secret") || "",
|
||||
endpointurl,
|
||||
body: get(data, "requestBody", {}),
|
||||
},
|
||||
user,
|
||||
});
|
||||
data.accessToken = signatureService.accessToken;
|
||||
data.timestamp = signatureService.timestamp;
|
||||
data.signature = signatureService.signature;
|
||||
const clientId = data.clientbank_id;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-PARTNER-ID": clientId,
|
||||
"X-TIMESTAMP": data.timestamp,
|
||||
"X-SIGNATURE": data.signature,
|
||||
Authorization: `Bearer ${data.accessToken}`,
|
||||
"X-EXTERNAL-ID": data.external_id,
|
||||
"CHANNEL-ID": data.channel_id,
|
||||
};
|
||||
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
|
||||
|
||||
const resp = await axios.post(
|
||||
mandiriUrl + endpointurl,
|
||||
{ ...get(data, "requestBody", {}) },
|
||||
{
|
||||
headers,
|
||||
timeout: 15000,
|
||||
}
|
||||
);
|
||||
return {
|
||||
...resp.data,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { get } from "lodash";
|
||||
import { db } from "../../../db";
|
||||
import axios from "axios";
|
||||
import { formatTimestamp } from "../../formatTimestamp";
|
||||
import generateSignatureService from "./generateSignatureService";
|
||||
import { getParameter } from "../../getParameter";
|
||||
export default async function ({ data }: { data: any }) {
|
||||
const endpointurl = "/api/v1.0/transfer-va/status";
|
||||
const user = await db.users.findFirst({
|
||||
where: {
|
||||
clientbank_id: data.clientbank_id,
|
||||
},
|
||||
include: { banks: true, database: true, parameters: true },
|
||||
});
|
||||
if (!user) throw new Error("User not found");
|
||||
const timestamp = formatTimestamp();
|
||||
const signatureService = await generateSignatureService({
|
||||
data: {
|
||||
accessToken: data.accessToken,
|
||||
timestamp,
|
||||
isGenerateNewToken: true,
|
||||
client_secret: getParameter(user, "client_secret") || "",
|
||||
endpointurl,
|
||||
body: get(data, "requestBody", {}),
|
||||
},
|
||||
user,
|
||||
});
|
||||
data.accessToken = signatureService.accessToken;
|
||||
data.timestamp = signatureService.timestamp;
|
||||
data.signature = signatureService.signature;
|
||||
const clientId = data.clientbank_id;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-PARTNER-ID": clientId,
|
||||
"X-TIMESTAMP": data.timestamp,
|
||||
"X-SIGNATURE": data.signature,
|
||||
Authorization: `Bearer ${data.accessToken}`,
|
||||
"X-EXTERNAL-ID": data.external_id,
|
||||
"CHANNEL-ID": data.channel_id,
|
||||
};
|
||||
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
|
||||
|
||||
const resp = await axios.post(
|
||||
mandiriUrl + endpointurl,
|
||||
{ ...get(data, "requestBody", {}) },
|
||||
{
|
||||
headers,
|
||||
timeout: 15000,
|
||||
}
|
||||
);
|
||||
return {
|
||||
...resp.data,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ export default async function ({ data }: { data: any }) {
|
|||
});
|
||||
if (!user) throw new Error("User not found");
|
||||
const clientId = data.clientbank_id;
|
||||
const timestamp = formatTimestamp();
|
||||
const timestamp = get(data, "isNewTimestamp", true)
|
||||
? formatTimestamp()
|
||||
: get(data, "timestamp", formatTimestamp());
|
||||
const headers: Record<string, any> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ export default async function ({
|
|||
const isGenerateNewToken = get(data, "isGenerateNewToken", false);
|
||||
let timestamp = data.timestamp;
|
||||
let accessToken = data.accessToken;
|
||||
let isNewTimestamp = get(data, "isNewTimestamp", true);
|
||||
if (isGenerateNewToken) {
|
||||
const signatureAuth = await generateSignatureAuth({
|
||||
data: {
|
||||
clientbank_id: user.clientbank_id,
|
||||
timestamp: timestamp,
|
||||
isNewTimestamp,
|
||||
},
|
||||
});
|
||||
timestamp = signatureAuth.timestamp;
|
||||
|
|
@ -48,5 +51,5 @@ export default async function ({
|
|||
timeout: 15000,
|
||||
}
|
||||
);
|
||||
return { ...resp.data, timestamp, accessToken };
|
||||
return { ...resp.data, timestamp, accessToken, headers };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default async function ({
|
|||
virtualAccountNo: invoiceId,
|
||||
dbName: get(user, "database.name", ""),
|
||||
});
|
||||
|
||||
if (!c_bpartner) {
|
||||
throw new Error("C_BPartner_ID not found for the given Virtual Account No");
|
||||
}
|
||||
|
|
@ -42,6 +43,16 @@ export default async function ({
|
|||
is_pay: "N",
|
||||
},
|
||||
});
|
||||
// console.log({
|
||||
// _sum: {
|
||||
// grandtotal: true,
|
||||
// },
|
||||
// where: {
|
||||
// c_bpartner_id: c_bpartner.c_bpartner_id,
|
||||
// db_id: user.database.db_id,
|
||||
// is_pay: "N",
|
||||
// },
|
||||
// });
|
||||
if (lo.get(grandtotal, "_sum.grandtotal") === null) {
|
||||
throw new Error(
|
||||
"No outstanding amount found for the given Virtual Account No"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { authMiddleware, hashToken } from "./auth";
|
||||
import { db } from "../db";
|
||||
import callGateFromReq from "../lib/gateClient";
|
||||
import { get } from "lodash";
|
||||
import generateSignatureService from "../lib/bank/mandiri/generateSignatureService";
|
||||
import { getParameter } from "../lib/getParameter";
|
||||
|
||||
export async function vaMiddleware(
|
||||
req: Request,
|
||||
|
|
@ -20,84 +20,53 @@ export async function vaMiddleware(
|
|||
if (!token) {
|
||||
return res.status(401).json({ error: "missing token" });
|
||||
}
|
||||
const session = await db.sessions.findFirst({
|
||||
where: { token_hash: hashToken(token), is_revoked: false },
|
||||
include: { users: true },
|
||||
const data = req.body || {};
|
||||
if (!get(data, "partnerServiceId")) {
|
||||
return res.status(400).json({
|
||||
responseCode: "2002400",
|
||||
responseMessage: `Invalid Mandatory Field partnerServiceId`,
|
||||
});
|
||||
}
|
||||
const parameters = await db.parameters.findFirst({
|
||||
where: {
|
||||
param_key: "partnerServiceId",
|
||||
param_value: get(data, "partnerServiceId"),
|
||||
},
|
||||
});
|
||||
if (!session) {
|
||||
return res.status(401).json({ error: "invalid token" });
|
||||
}
|
||||
if (!timestamp || !signature) {
|
||||
return res.status(400).json({ error: "missing required headers" });
|
||||
}
|
||||
try {
|
||||
const result = await callGateFromReq(req, {
|
||||
client: "mandiri",
|
||||
action: "verifySignature",
|
||||
data: {
|
||||
timestamp,
|
||||
signature,
|
||||
httpmethod: req.method === "POST" ? "POST" : "GET",
|
||||
endpointurl: req.originalUrl,
|
||||
accesstoken: token,
|
||||
requestbody: req.body,
|
||||
typeVerify: "transaction",
|
||||
},
|
||||
if (!parameters) {
|
||||
return res.status(404).json({
|
||||
responseCode: "4042412",
|
||||
responseMessage: `Invalid partnerServiceId`,
|
||||
});
|
||||
if (!result) {
|
||||
return res.status(401).json({
|
||||
responseCode: "4012400",
|
||||
responseMessage: `Unauthorized`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message: any = get(err, "message", "internal_error");
|
||||
if (
|
||||
message === "Invalid access token" ||
|
||||
message === "Access token expired"
|
||||
) {
|
||||
return res.status(401).json({
|
||||
responseCode: "4012400",
|
||||
responseMessage: `Unauthorized ${message}`,
|
||||
});
|
||||
} else {
|
||||
console.error("vaMiddleware error", err);
|
||||
return res.status(500).json({
|
||||
responseCode: "5002401",
|
||||
responseMessage: `Internal Server Error ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Prefer RSA verification if the user record contains a public cert
|
||||
const user = session.users;
|
||||
// some projects store certs in different user columns; access via any to avoid TS errors
|
||||
const uAny = user as any;
|
||||
const certPem = (uAny &&
|
||||
(uAny.public_cert ||
|
||||
uAny.cert_pem ||
|
||||
uAny.certificate ||
|
||||
uAny.publicCert)) as string | undefined;
|
||||
const clientId = (uAny &&
|
||||
(uAny.client_id ||
|
||||
uAny.clientbank_id ||
|
||||
uAny.clientKey ||
|
||||
uAny.clientkey)) as string | undefined;
|
||||
|
||||
let ok = false;
|
||||
|
||||
if (authHeader) {
|
||||
const token = authHeader.slice("Bearer ".length).trim();
|
||||
const tokenHash = hashToken(token);
|
||||
const session = await db.sessions.findFirst({
|
||||
where: { token_hash: tokenHash, is_revoked: false },
|
||||
const users = await db.users.findFirst({
|
||||
where: { user_id: parameters.user_id },
|
||||
include: { database: true, banks: true, parameters: true, tenants: true },
|
||||
});
|
||||
const signatureService = await generateSignatureService({
|
||||
data: {
|
||||
accessToken: token,
|
||||
timestamp: timestamp,
|
||||
isNewTimestamp: false,
|
||||
isGenerateNewToken: false,
|
||||
client_secret: getParameter(users, "client_secret") || "",
|
||||
endpointurl: req.originalUrl,
|
||||
body: data,
|
||||
},
|
||||
user: users as any,
|
||||
});
|
||||
const signatureServiceResult = signatureService.signature;
|
||||
if (signatureServiceResult !== signature) {
|
||||
return res.status(401).json({
|
||||
responseCode: "4012400",
|
||||
responseMessage: `Unauthorized. Invalid signature`,
|
||||
});
|
||||
// If we have an auth header, we're good
|
||||
return authMiddleware(req as any, res, next as any);
|
||||
} else {
|
||||
return res.status(401).json({ error: "missing token" });
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error("vaMiddleware error", err);
|
||||
return res.status(500).json({ error: "internal_error" });
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "internal_error", message: get(err, "message") });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ export const setRoutes = () => {
|
|||
router.post("/generate-signature", (req, res) =>
|
||||
signController.generateSignature(req, res)
|
||||
);
|
||||
// Signature generation for testing (uses PRIVATE_KEY_PATH file)
|
||||
router.post("/generate-signature-bank", (req, res) =>
|
||||
signController.generateSignature(req, res)
|
||||
);
|
||||
// Verify signature using public cert (default: public/mandiri/midsuit.cer)
|
||||
router.post("/verify-signature", (req, res) =>
|
||||
signController.verifySignature(req, res)
|
||||
|
|
@ -110,6 +114,9 @@ export const setRoutes = () => {
|
|||
versionRouter.post("/access-token/b2b", (req, res) =>
|
||||
tokenController.b2bAccessToken(req, res)
|
||||
);
|
||||
versionRouter.post("/utilities/signature-service", (req, res) =>
|
||||
tokenController.signatureBank(req, res)
|
||||
);
|
||||
router.use("/:version", versionRouter);
|
||||
|
||||
// Sync endpoints group
|
||||
|
|
|
|||
Loading…
Reference in New Issue