Implement phase 1 completion and phase 2 dynamic QR
This commit is contained in:
@ -20,7 +20,35 @@ async function main() {
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const txResult = await client.query("DELETE FROM transactions WHERE partner_reference LIKE 'PR-%' RETURNING id");
|
||||
const auditTable = await client.query("SELECT to_regclass('public.audit_logs') AS name");
|
||||
const idsResult = await client.query(`
|
||||
SELECT id FROM transactions WHERE partner_reference LIKE 'PR-%' OR partner_reference LIKE 'DYN-%'
|
||||
UNION
|
||||
SELECT id FROM devices WHERE device_code LIKE 'DEV-%'
|
||||
UNION
|
||||
SELECT id FROM merchants WHERE legal_name LIKE 'Smoke Merchant %'
|
||||
UNION
|
||||
SELECT outlets.id
|
||||
FROM outlets
|
||||
JOIN merchants ON merchants.id = outlets.merchant_id
|
||||
WHERE merchants.legal_name LIKE 'Smoke Merchant %'
|
||||
UNION
|
||||
SELECT terminals.id
|
||||
FROM terminals
|
||||
JOIN outlets ON outlets.id = terminals.outlet_id
|
||||
JOIN merchants ON merchants.id = outlets.merchant_id
|
||||
WHERE merchants.legal_name LIKE 'Smoke Merchant %'
|
||||
UNION
|
||||
SELECT device_bindings.id
|
||||
FROM device_bindings
|
||||
JOIN devices ON devices.id = device_bindings.device_id
|
||||
WHERE devices.device_code LIKE 'DEV-%'
|
||||
`);
|
||||
const auditIds = idsResult.rows.map((row) => row.id);
|
||||
const auditResult = auditTable.rows[0]?.name && auditIds.length
|
||||
? await client.query("DELETE FROM audit_logs WHERE entity_id = ANY($1::text[])", [auditIds])
|
||||
: { rowCount: 0 };
|
||||
const txResult = await client.query("DELETE FROM transactions WHERE partner_reference LIKE 'PR-%' OR partner_reference LIKE 'DYN-%' RETURNING id");
|
||||
const devResult = await client.query("DELETE FROM devices WHERE device_code LIKE 'DEV-%' RETURNING id");
|
||||
const merchantResult = await client.query("DELETE FROM merchants WHERE legal_name LIKE 'Smoke Merchant %' RETURNING id");
|
||||
|
||||
@ -30,6 +58,7 @@ async function main() {
|
||||
transactions_deleted: txResult.rowCount,
|
||||
devices_deleted: devResult.rowCount,
|
||||
merchants_deleted: merchantResult.rowCount,
|
||||
audit_logs_deleted: auditResult.rowCount,
|
||||
note: "outlets/terminals are removed via merchant cascade"
|
||||
}));
|
||||
} catch (error) {
|
||||
|
||||
@ -37,6 +37,32 @@ async function req(path, options = {}) {
|
||||
return body;
|
||||
}
|
||||
|
||||
async function reqExpect(path, expectedStatus, options = {}) {
|
||||
const response = await fetch(`${BASE}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {})
|
||||
},
|
||||
body: Object.prototype.hasOwnProperty.call(options, 'body') ? JSON.stringify(options.body) : undefined
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let body = null;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
|
||||
if (response.status !== expectedStatus) {
|
||||
throw new Error(`${options._label || path} expected ${expectedStatus}, got ${response.status}`);
|
||||
}
|
||||
|
||||
console.log(`${options._label || `${options.method || 'GET'} ${path}`} => ${response.status} ${short(body)}`);
|
||||
return body;
|
||||
}
|
||||
|
||||
async function reqAdmin(path, opts = {}) {
|
||||
return req(path, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${ADMIN_TOKEN}` } });
|
||||
}
|
||||
@ -151,8 +177,24 @@ async function reqDevice(path, opts = {}) {
|
||||
_label: 'POST /integrations/qris/callback'
|
||||
});
|
||||
|
||||
await req('/integrations/qris/callback', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': signature },
|
||||
body: { ...callback, signature },
|
||||
_label: 'POST /integrations/qris/callback duplicate'
|
||||
});
|
||||
|
||||
await reqExpect('/integrations/qris/callback', 401, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': 'bad-signature' },
|
||||
body: { ...callback, signature: 'bad-signature' },
|
||||
_label: 'POST /integrations/qris/callback invalid signature'
|
||||
});
|
||||
|
||||
await reqAdmin(`/admin/transactions/${txId}`, { _label: 'GET /admin/transactions/:id' });
|
||||
await reqAdmin(`/admin/transactions/${txId}/events`, { _label: 'GET /admin/transactions/:id/events' });
|
||||
await reqAdmin(`/admin/ledger-entries?transaction_id=${txId}`, { _label: 'GET /admin/ledger-entries' });
|
||||
await reqAdmin(`/admin/audit-logs?entity_id=${txId}`, { _label: 'GET /admin/audit-logs' });
|
||||
await reqAdmin(`/admin/transactions/${txId}/heartbeats`, { _label: 'GET /admin/transactions/:id/heartbeats' });
|
||||
await reqAdmin(`/admin/devices/${deviceId}/heartbeats`, { _label: 'GET /admin/devices/:id/heartbeats' });
|
||||
await reqAdmin('/admin/notifications/failed', { _label: 'GET /admin/notifications/failed' });
|
||||
@ -163,5 +205,272 @@ async function reqDevice(path, opts = {}) {
|
||||
});
|
||||
await reqAdmin('/admin/dashboard/summary', { _label: 'GET /admin/dashboard/summary' });
|
||||
|
||||
const noBindingOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
|
||||
method: 'POST',
|
||||
body: { name: `No Binding Outlet ${ts}` },
|
||||
_label: 'POST /admin/merchants/:id/outlets no-binding'
|
||||
});
|
||||
const noBindingOutletId = noBindingOutlet?.data?.id;
|
||||
const noBindingTerminal = await reqAdmin(`/admin/outlets/${noBindingOutletId}/terminals`, {
|
||||
method: 'POST',
|
||||
body: { terminal_code: `TERM-NB-${ts}`, qr_mode: 'static' },
|
||||
_label: 'POST /admin/outlets/:id/terminals no-binding'
|
||||
});
|
||||
const noBindingTerminalId = noBindingTerminal?.data?.id;
|
||||
const noBindingTx = await reqAdmin('/admin/transactions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
partner_reference: `PR-NB-${ts}`,
|
||||
merchant_id: merchantId,
|
||||
outlet_id: noBindingOutletId,
|
||||
terminal_id: noBindingTerminalId,
|
||||
amount: 9900,
|
||||
currency: 'IDR',
|
||||
qr_mode: 'static',
|
||||
initiation_mode: 'static',
|
||||
status: 'initiated'
|
||||
},
|
||||
_label: 'POST /admin/transactions no-binding'
|
||||
});
|
||||
const noBindingCallback = {
|
||||
partner_reference: `PR-NB-${ts}`,
|
||||
partner_txn_id: `PTX-NB-${ts}`,
|
||||
amount: 9900,
|
||||
currency: 'IDR',
|
||||
payment_status: 'paid',
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString()
|
||||
};
|
||||
const noBindingSignature = createHmac('sha256', SECRET).update(JSON.stringify(noBindingCallback)).digest('hex');
|
||||
await req('/integrations/qris/callback', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': noBindingSignature },
|
||||
body: { ...noBindingCallback, signature: noBindingSignature },
|
||||
_label: 'POST /integrations/qris/callback no-binding'
|
||||
});
|
||||
await reqAdmin(`/admin/ledger-entries?transaction_id=${noBindingTx?.data?.id}`, {
|
||||
_label: 'GET /admin/ledger-entries no-binding'
|
||||
});
|
||||
await reqAdmin('/admin/notifications/failed', { _label: 'GET /admin/notifications/failed no-binding' });
|
||||
|
||||
const dynamicOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
|
||||
method: 'POST',
|
||||
body: { name: `Dynamic API Outlet ${ts}` },
|
||||
_label: 'POST /admin/merchants/:id/outlets dynamic-api'
|
||||
});
|
||||
const dynamicOutletId = dynamicOutlet?.data?.id;
|
||||
const dynamicTerminal = await reqAdmin(`/admin/outlets/${dynamicOutletId}/terminals`, {
|
||||
method: 'POST',
|
||||
body: { terminal_code: `TERM-DYN-${ts}`, qr_mode: 'dynamic_api' },
|
||||
_label: 'POST /admin/outlets/:id/terminals dynamic-api'
|
||||
});
|
||||
const dynamicTerminalId = dynamicTerminal?.data?.id;
|
||||
const dynamicDevice = await reqAdmin('/admin/devices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
device_code: `DEV-API-${ts}`,
|
||||
vendor: 'acme',
|
||||
model: 'api-v1',
|
||||
communication_mode: 'api',
|
||||
capability_profile_json: {
|
||||
dynamic_qr: { api_direct: true, mqtt: false },
|
||||
flows: ['dynamic_qr:api_direct', 'static_payment_notification']
|
||||
},
|
||||
status: 'active'
|
||||
},
|
||||
_label: 'POST /admin/devices dynamic-api'
|
||||
});
|
||||
const dynamicDeviceId = dynamicDevice?.data?.id;
|
||||
await reqAdmin(`/admin/devices/${dynamicDeviceId}/bind`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
merchant_id: merchantId,
|
||||
outlet_id: dynamicOutletId,
|
||||
terminal_id: dynamicTerminalId
|
||||
},
|
||||
_label: 'POST /admin/devices/:id/bind dynamic-api'
|
||||
});
|
||||
await reqExpect('/device/transactions/dynamic-qr', 400, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${DEVICE_TOKEN}` },
|
||||
body: {
|
||||
device_id: deviceId,
|
||||
terminal_id: dynamicTerminalId,
|
||||
amount: 15000,
|
||||
currency: 'IDR',
|
||||
request_id: `DYN-STATIC-${ts}`
|
||||
},
|
||||
_label: 'POST /device/transactions/dynamic-qr unsupported device'
|
||||
});
|
||||
const dynamicRequestId = `DYN-REQ-${ts}`;
|
||||
const dynamicQr = await reqDevice('/device/transactions/dynamic-qr', {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': dynamicRequestId },
|
||||
body: {
|
||||
device_id: dynamicDeviceId,
|
||||
terminal_id: dynamicTerminalId,
|
||||
amount: 32100,
|
||||
currency: 'IDR',
|
||||
request_id: dynamicRequestId
|
||||
},
|
||||
_label: 'POST /device/transactions/dynamic-qr'
|
||||
});
|
||||
const dynamicQrReplay = await reqDevice('/device/transactions/dynamic-qr', {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': dynamicRequestId },
|
||||
body: {
|
||||
device_id: dynamicDeviceId,
|
||||
terminal_id: dynamicTerminalId,
|
||||
amount: 32100,
|
||||
currency: 'IDR',
|
||||
request_id: dynamicRequestId
|
||||
},
|
||||
_label: 'POST /device/transactions/dynamic-qr duplicate'
|
||||
});
|
||||
if (dynamicQr?.data?.transaction_id !== dynamicQrReplay?.data?.transaction_id) {
|
||||
throw new Error('dynamic QR idempotency returned a different transaction');
|
||||
}
|
||||
const dynamicCallback = {
|
||||
partner_reference: dynamicQr?.data?.partner_reference,
|
||||
partner_txn_id: `PTX-DYN-${ts}`,
|
||||
amount: 32100,
|
||||
currency: 'IDR',
|
||||
payment_status: 'paid',
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString()
|
||||
};
|
||||
const dynamicSignature = createHmac('sha256', SECRET).update(JSON.stringify(dynamicCallback)).digest('hex');
|
||||
await req('/integrations/qris/callback', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': dynamicSignature },
|
||||
body: { ...dynamicCallback, signature: dynamicSignature },
|
||||
_label: 'POST /integrations/qris/callback dynamic-api'
|
||||
});
|
||||
await reqAdmin(`/admin/transactions/${dynamicQr?.data?.transaction_id}`, {
|
||||
_label: 'GET /admin/transactions/:id dynamic-api'
|
||||
});
|
||||
|
||||
const mqttOutlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
|
||||
method: 'POST',
|
||||
body: { name: `Dynamic MQTT Outlet ${ts}` },
|
||||
_label: 'POST /admin/merchants/:id/outlets dynamic-mqtt'
|
||||
});
|
||||
const mqttOutletId = mqttOutlet?.data?.id;
|
||||
const mqttTerminal = await reqAdmin(`/admin/outlets/${mqttOutletId}/terminals`, {
|
||||
method: 'POST',
|
||||
body: { terminal_code: `TERM-MQTT-${ts}`, qr_mode: 'dynamic_mqtt' },
|
||||
_label: 'POST /admin/outlets/:id/terminals dynamic-mqtt'
|
||||
});
|
||||
const mqttTerminalId = mqttTerminal?.data?.id;
|
||||
const mqttDevice = await reqAdmin('/admin/devices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
device_code: `DEV-MQTT-${ts}`,
|
||||
vendor: 'acme',
|
||||
model: 'mqtt-v1',
|
||||
communication_mode: 'mqtt',
|
||||
capability_profile_json: {
|
||||
dynamic_qr: { api_direct: false, mqtt: true },
|
||||
flows: ['dynamic_qr:mqtt', 'static_payment_notification']
|
||||
},
|
||||
status: 'active'
|
||||
},
|
||||
_label: 'POST /admin/devices dynamic-mqtt'
|
||||
});
|
||||
const mqttDeviceId = mqttDevice?.data?.id;
|
||||
await reqAdmin(`/admin/devices/${mqttDeviceId}/bind`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
merchant_id: merchantId,
|
||||
outlet_id: mqttOutletId,
|
||||
terminal_id: mqttTerminalId
|
||||
},
|
||||
_label: 'POST /admin/devices/:id/bind dynamic-mqtt'
|
||||
});
|
||||
const mqttRequestId = `MQTT-DYN-${ts}`;
|
||||
const mqttQr = await reqDevice('/device/mqtt/uplink/dynamic-qr/request', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
message_type: 'dynamic_qr_request',
|
||||
request_id: mqttRequestId,
|
||||
device_id: mqttDeviceId,
|
||||
terminal_id: mqttTerminalId,
|
||||
amount: 43200,
|
||||
currency: 'IDR',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
_label: 'POST /device/mqtt/uplink/dynamic-qr/request'
|
||||
});
|
||||
const mqttQrReplay = await reqDevice('/device/mqtt/uplink/dynamic-qr/request', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
message_type: 'dynamic_qr_request',
|
||||
request_id: mqttRequestId,
|
||||
device_id: mqttDeviceId,
|
||||
terminal_id: mqttTerminalId,
|
||||
amount: 43200,
|
||||
currency: 'IDR',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
_label: 'POST /device/mqtt/uplink/dynamic-qr/request duplicate'
|
||||
});
|
||||
if (mqttQr?.data?.transaction_id !== mqttQrReplay?.data?.transaction_id) {
|
||||
throw new Error('MQTT dynamic QR idempotency returned a different transaction');
|
||||
}
|
||||
await reqAdmin(`/admin/devices/${mqttDeviceId}/mqtt-messages?correlation_id=${mqttRequestId}`, {
|
||||
_label: 'GET /admin/devices/:id/mqtt-messages dynamic-mqtt'
|
||||
});
|
||||
const mqttCallback = {
|
||||
partner_reference: mqttQr?.data?.partner_reference,
|
||||
partner_txn_id: `PTX-MQTT-${ts}`,
|
||||
amount: 43200,
|
||||
currency: 'IDR',
|
||||
payment_status: 'paid',
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString()
|
||||
};
|
||||
const mqttSignature = createHmac('sha256', SECRET).update(JSON.stringify(mqttCallback)).digest('hex');
|
||||
await req('/integrations/qris/callback', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': mqttSignature },
|
||||
body: { ...mqttCallback, signature: mqttSignature },
|
||||
_label: 'POST /integrations/qris/callback dynamic-mqtt'
|
||||
});
|
||||
await reqAdmin(`/admin/transactions/${mqttQr?.data?.transaction_id}`, {
|
||||
_label: 'GET /admin/transactions/:id dynamic-mqtt'
|
||||
});
|
||||
|
||||
const pushedConfig = await reqAdmin(`/admin/devices/${dynamicDeviceId}/config`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
settings: {
|
||||
volume: 65,
|
||||
language: 'id-ID',
|
||||
heartbeat_interval_seconds: 45
|
||||
}
|
||||
},
|
||||
_label: 'PATCH /admin/devices/:id/config'
|
||||
});
|
||||
const configVersion = pushedConfig?.data?.config?.config_version;
|
||||
await reqDevice(`/device/config?device_id=${dynamicDeviceId}`, {
|
||||
_label: 'GET /device/config'
|
||||
});
|
||||
await reqDevice('/device/config/ack', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
device_id: dynamicDeviceId,
|
||||
config_version: configVersion,
|
||||
status: 'applied',
|
||||
result_payload: { applied_at: new Date().toISOString() }
|
||||
},
|
||||
_label: 'POST /device/config/ack'
|
||||
});
|
||||
await reqAdmin(`/admin/devices/${dynamicDeviceId}/config`, {
|
||||
_label: 'GET /admin/devices/:id/config'
|
||||
});
|
||||
await reqAdmin(`/admin/devices/${dynamicDeviceId}/mqtt-messages?message_type=config_push`, {
|
||||
_label: 'GET /admin/devices/:id/mqtt-messages config'
|
||||
});
|
||||
|
||||
console.log(`Smoke point 4 flow done. tx=${txId} device=${deviceId}`);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user