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

@ -0,0 +1,26 @@
function getProfile(device) {
return (device.capability_profile_json || {});
}
export function supportsDynamicQrFlow(device, flow) {
const profile = getProfile(device);
const flows = Array.isArray(profile.flows) ? profile.flows : [];
if (flow === "api_direct" && device.communication_mode !== "api") {
return false;
}
if (flow === "mqtt" && device.communication_mode !== "mqtt") {
return false;
}
if (typeof profile.dynamic_qr === "boolean") {
return profile.dynamic_qr || flows.includes(`dynamic_qr:${flow}`);
}
if (profile.dynamic_qr && typeof profile.dynamic_qr === "object") {
return Boolean(profile.dynamic_qr[flow]) || flows.includes(`dynamic_qr:${flow}`);
}
return flows.includes(`dynamic_qr:${flow}`);
}
export function resolveDeviceCapabilitySummary(device) {
return {
dynamic_qr_api_direct: supportsDynamicQrFlow(device, "api_direct"),
dynamic_qr_mqtt: supportsDynamicQrFlow(device, "mqtt")
};
}

View File

@ -0,0 +1,67 @@
import { randomUUID } from "node:crypto";
import { createTransaction, addTransactionEvent, toTransactionPayload } from "../store/transactionStore";
function makePartnerReference(requestId) {
const clean = requestId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40);
return `DYN-${clean || randomUUID().slice(0, 12)}`;
}
function makeDynamicQrPayload(input) {
const amountMinor = Math.round(input.amount * 100);
const encoded = Buffer.from(JSON.stringify({
type: "QRIS_DYNAMIC_MOCK",
transaction_id: input.transactionId,
partner_reference: input.partnerReference,
amount_minor: amountMinor,
currency: input.currency,
expires_at: input.expiresAt
})).toString("base64url");
return `QRIS-DYNAMIC-MOCK.${encoded}`;
}
export async function createDynamicQrTransaction(input) {
const ttlSeconds = Math.min(Math.max(input.expires_in_seconds || 300, 60), 1800);
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
const partnerReference = makePartnerReference(input.request_id);
const tx = await createTransaction({
merchant_id: input.merchant_id,
outlet_id: input.outlet_id,
terminal_id: input.terminal_id,
device_id: input.device_id,
partner_reference: partnerReference,
amount: input.amount,
currency: input.currency || "IDR",
qr_mode: "dynamic",
initiation_mode: input.initiation_mode || "dynamic_api",
status: "awaiting_payment",
expired_at: expiresAt
});
const qrPayload = makeDynamicQrPayload({
transactionId: tx.id,
partnerReference,
amount: tx.amount,
currency: tx.currency,
expiresAt
});
await addTransactionEvent({
transaction_id: tx.id,
event_type: "DYNAMIC_QR_CREATED",
source: "device",
payload_json: {
request_id: input.request_id,
correlation_id: input.request_id,
device_id: input.device_id,
qr_payload: qrPayload,
expires_at: expiresAt,
transaction: toTransactionPayload(tx)
}
});
return {
request_id: input.request_id,
correlation_id: input.request_id,
transaction_id: tx.id,
transaction_code: tx.transaction_code,
qr_type: "dynamic",
qr_payload: qrPayload,
expires_at: expiresAt,
status: "awaiting_payment",
partner_reference: partnerReference
};
}

View File

@ -27,10 +27,15 @@ export function buildPaymentSuccessPayload(input) {
export function makePaymentSuccessTopic(deviceId) {
return `devices/${deviceId}/downlink/payment/success`;
}
export async function publishPaymentSuccess(payload) {
export function makeDynamicQrResponseTopic(deviceId) {
return `devices/${deviceId}/downlink/dynamic-qr/response`;
}
export function makeConfigPushTopic(deviceId) {
return `devices/${deviceId}/downlink/config/push`;
}
async function publishMqttPayload(deviceId, topic, payload) {
const publishedAt = new Date().toISOString();
const topic = makePaymentSuccessTopic(payload.device_id);
if (shouldForceFail(payload.device_id)) {
if (shouldForceFail(deviceId)) {
return {
ok: false,
topic,
@ -50,3 +55,12 @@ export async function publishPaymentSuccess(payload) {
payload
};
}
export async function publishPaymentSuccess(payload) {
return publishMqttPayload(payload.device_id, makePaymentSuccessTopic(payload.device_id), payload);
}
export async function publishDynamicQrResponse(deviceId, payload) {
return publishMqttPayload(deviceId, makeDynamicQrResponseTopic(deviceId), payload);
}
export async function publishConfigPush(deviceId, payload) {
return publishMqttPayload(deviceId, makeConfigPushTopic(deviceId), payload);
}