Implement phase 1 completion and phase 2 dynamic QR

This commit is contained in:
2026-05-26 08:06:48 +07:00
parent a152c99cce
commit 5624b92872
36 changed files with 3104 additions and 71 deletions

View File

@ -6,6 +6,7 @@ import { readIdempotency, writeIdempotency } from "../shared/idempotency/idempot
import { addTransactionEvent, findTransactionByPartnerReference, getTransactionEvents, updateTransactionStatus } from "../shared/store/transactionStore";
import { emitTransactionPaid } from "../shared/events/transactionEvents";
import { env } from "../config/env";
import { createAuditLog } from "../shared/store/auditLogStore";
const router = Router();
function parsePaymentStatus(rawStatus) {
const normalized = String(rawStatus || "").toLowerCase();
@ -79,6 +80,20 @@ function buildCallbackResponse(req, transactionId, eventId, note, reason) {
function writeCallbackResult(idempotencyKey, response, transactionId) {
writeIdempotency("callback.processing", idempotencyKey, { response, transaction_id: transactionId }, env.IDEMPOTENCY_TTL_MS);
}
async function auditWebhookAction(req, payload) {
await createAuditLog({
actor_type: "webhook",
actor_id: "qris_partner",
action: payload.action,
entity_type: "transaction",
entity_id: payload.entity_id,
before_json: payload.before_json,
after_json: payload.after_json,
source_ip: req.ip,
request_id: req.requestId,
trace_id: req.traceId
});
}
async function makeResponseEventId(txId, fallbackTag) {
const events = await getTransactionEvents(txId);
return events.at(-1)?.id || `${fallbackTag}_${Date.now()}`;
@ -172,6 +187,12 @@ router.post("/qris/callback", async (req, res, next) => {
throw error;
}
if (!wasPaid) {
await auditWebhookAction(req, {
action: "transaction.mark_paid",
entity_id: updated.id,
before_json: tx,
after_json: updated
});
emitTransactionPaid({
transaction_id: updated.id,
merchant_id: updated.merchant_id,
@ -216,6 +237,12 @@ router.post("/qris/callback", async (req, res, next) => {
}
throw error;
}
await auditWebhookAction(req, {
action: "transaction.mark_expired",
entity_id: updated.id,
before_json: tx,
after_json: updated
});
const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"));
writeCallbackResult(idempotencyKey, response, updated.id);
return res.json(successResponse(req, response));
@ -235,6 +262,12 @@ router.post("/qris/callback", async (req, res, next) => {
}
throw error;
}
await auditWebhookAction(req, {
action: "transaction.mark_failed",
entity_id: updated.id,
before_json: tx,
after_json: updated
});
const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"), undefined, parsed.status.reason);
writeCallbackResult(idempotencyKey, response, updated.id);
return res.json(successResponse(req, response));