Implement phase 1 completion and phase 2 dynamic QR
This commit is contained in:
84
dist/shared/store/auditLogStore.js
vendored
Normal file
84
dist/shared/store/auditLogStore.js
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapAuditLog(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
actor_type: row.actor_type,
|
||||
actor_id: row.actor_id || undefined,
|
||||
action: row.action,
|
||||
entity_type: row.entity_type,
|
||||
entity_id: row.entity_id,
|
||||
before_json: row.before_json || null,
|
||||
after_json: row.after_json || null,
|
||||
source_ip: row.source_ip || undefined,
|
||||
request_id: row.request_id || undefined,
|
||||
trace_id: row.trace_id || undefined,
|
||||
created_at: row.created_at
|
||||
};
|
||||
}
|
||||
export async function createAuditLog(payload) {
|
||||
const { rows } = await getPool().query(`INSERT INTO audit_logs (
|
||||
id,
|
||||
actor_type,
|
||||
actor_id,
|
||||
action,
|
||||
entity_type,
|
||||
entity_id,
|
||||
before_json,
|
||||
after_json,
|
||||
source_ip,
|
||||
request_id,
|
||||
trace_id,
|
||||
created_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
||||
RETURNING *`, [
|
||||
randomUUID(),
|
||||
payload.actor_type,
|
||||
payload.actor_id || null,
|
||||
payload.action,
|
||||
payload.entity_type,
|
||||
payload.entity_id,
|
||||
payload.before_json || null,
|
||||
payload.after_json || null,
|
||||
payload.source_ip || null,
|
||||
payload.request_id || null,
|
||||
payload.trace_id || null,
|
||||
nowIso()
|
||||
]);
|
||||
return mapAuditLog(rows[0]);
|
||||
}
|
||||
export async function listAuditLogs(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.entity_type) {
|
||||
clauses.push(`entity_type = $${i++}`);
|
||||
params.push(filter.entity_type);
|
||||
}
|
||||
if (filter?.entity_id) {
|
||||
clauses.push(`entity_id = $${i++}`);
|
||||
params.push(filter.entity_id);
|
||||
}
|
||||
if (filter?.action) {
|
||||
clauses.push(`action = $${i++}`);
|
||||
params.push(filter.action);
|
||||
}
|
||||
if (filter?.from) {
|
||||
clauses.push(`created_at >= $${i++}`);
|
||||
params.push(filter.from);
|
||||
}
|
||||
if (filter?.to) {
|
||||
clauses.push(`created_at <= $${i++}`);
|
||||
params.push(filter.to);
|
||||
}
|
||||
const limit = Math.min(Math.max(filter?.limit || 100, 1), 500);
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ${limit}`, params);
|
||||
return rows.map(mapAuditLog);
|
||||
}
|
||||
export function toAuditLogPayload(auditLog) {
|
||||
return { ...auditLog };
|
||||
}
|
||||
89
dist/shared/store/deviceConfigStore.js
vendored
Normal file
89
dist/shared/store/deviceConfigStore.js
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
const DEFAULT_SETTINGS = {
|
||||
volume: 80,
|
||||
language: "id-ID",
|
||||
heartbeat_interval_seconds: 60
|
||||
};
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapConfig(row) {
|
||||
return {
|
||||
device_id: row.device_id,
|
||||
config_version: Number(row.config_version),
|
||||
settings_json: row.settings_json || {},
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
function mapAck(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
device_id: row.device_id,
|
||||
config_version: Number(row.config_version),
|
||||
status: row.status,
|
||||
reason: row.reason || undefined,
|
||||
payload_json: row.payload_json || {},
|
||||
acked_at: row.acked_at
|
||||
};
|
||||
}
|
||||
export async function getDeviceConfig(deviceId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM device_configs WHERE device_id = $1", [deviceId]);
|
||||
return rows[0] ? mapConfig(rows[0]) : null;
|
||||
}
|
||||
export async function getOrCreateDeviceConfig(deviceId) {
|
||||
const existing = await getDeviceConfig(deviceId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return upsertDeviceConfig({
|
||||
device_id: deviceId,
|
||||
settings_json: DEFAULT_SETTINGS
|
||||
});
|
||||
}
|
||||
export async function upsertDeviceConfig(payload) {
|
||||
const existing = await getDeviceConfig(payload.device_id);
|
||||
const nextVersion = payload.config_version || (existing ? existing.config_version + 1 : 1);
|
||||
const { rows } = await getPool().query(`INSERT INTO device_configs (device_id, config_version, settings_json, updated_at)
|
||||
VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (device_id) DO UPDATE
|
||||
SET config_version = EXCLUDED.config_version,
|
||||
settings_json = EXCLUDED.settings_json,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING *`, [payload.device_id, nextVersion, payload.settings_json, nowIso()]);
|
||||
return mapConfig(rows[0]);
|
||||
}
|
||||
export async function createDeviceConfigAck(payload) {
|
||||
const { rows } = await getPool().query(`INSERT INTO device_config_acks (
|
||||
id,
|
||||
device_id,
|
||||
config_version,
|
||||
status,
|
||||
reason,
|
||||
payload_json,
|
||||
acked_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7)
|
||||
RETURNING *`, [
|
||||
`cfgack_${randomUUID()}`,
|
||||
payload.device_id,
|
||||
payload.config_version,
|
||||
payload.status,
|
||||
payload.reason || null,
|
||||
payload.payload_json || {},
|
||||
nowIso()
|
||||
]);
|
||||
return mapAck(rows[0]);
|
||||
}
|
||||
export async function listDeviceConfigAcks(deviceId, limit = 50) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_config_acks
|
||||
WHERE device_id = $1
|
||||
ORDER BY acked_at DESC
|
||||
LIMIT $2`, [deviceId, Math.min(Math.max(limit, 1), 200)]);
|
||||
return rows.map(mapAck);
|
||||
}
|
||||
export function toDeviceConfigPayload(config) {
|
||||
return { ...config };
|
||||
}
|
||||
export function toDeviceConfigAckPayload(ack) {
|
||||
return { ...ack };
|
||||
}
|
||||
74
dist/shared/store/ledgerStore.js
vendored
Normal file
74
dist/shared/store/ledgerStore.js
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapLedgerEntry(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
transaction_id: row.transaction_id,
|
||||
merchant_id: row.merchant_id,
|
||||
entry_type: row.entry_type,
|
||||
amount: Number(row.amount),
|
||||
currency: row.currency,
|
||||
direction: row.direction,
|
||||
status: row.status,
|
||||
metadata_json: row.metadata_json || {},
|
||||
created_at: row.created_at
|
||||
};
|
||||
}
|
||||
export async function createPaidLedgerPlaceholder(tx) {
|
||||
const { rows } = await getPool().query(`INSERT INTO ledger_entries (
|
||||
id,
|
||||
transaction_id,
|
||||
merchant_id,
|
||||
entry_type,
|
||||
amount,
|
||||
currency,
|
||||
direction,
|
||||
status,
|
||||
metadata_json,
|
||||
created_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||
ON CONFLICT (transaction_id, entry_type) DO UPDATE
|
||||
SET amount = EXCLUDED.amount,
|
||||
currency = EXCLUDED.currency,
|
||||
metadata_json = ledger_entries.metadata_json || EXCLUDED.metadata_json
|
||||
RETURNING *`, [
|
||||
randomUUID(),
|
||||
tx.id,
|
||||
tx.merchant_id,
|
||||
"gross_income",
|
||||
tx.amount,
|
||||
tx.currency,
|
||||
"credit",
|
||||
"posted",
|
||||
{
|
||||
placeholder: true,
|
||||
source: "fase1_paid_transaction",
|
||||
partner_reference: tx.partner_reference
|
||||
},
|
||||
nowIso()
|
||||
]);
|
||||
return mapLedgerEntry(rows[0]);
|
||||
}
|
||||
export async function listLedgerEntries(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.transaction_id) {
|
||||
clauses.push(`transaction_id = $${i++}`);
|
||||
params.push(filter.transaction_id);
|
||||
}
|
||||
if (filter?.merchant_id) {
|
||||
clauses.push(`merchant_id = $${i++}`);
|
||||
params.push(filter.merchant_id);
|
||||
}
|
||||
const limit = Math.min(Math.max(filter?.limit || 100, 1), 500);
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM ledger_entries ${where} ORDER BY created_at DESC LIMIT ${limit}`, params);
|
||||
return rows.map(mapLedgerEntry);
|
||||
}
|
||||
export function toLedgerEntryPayload(entry) {
|
||||
return { ...entry };
|
||||
}
|
||||
38
dist/shared/store/locationStore.js
vendored
38
dist/shared/store/locationStore.js
vendored
@ -65,19 +65,45 @@ export async function createTerminal(payload) {
|
||||
return mapTerminal(rows[0]);
|
||||
}
|
||||
export async function listOutlets(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.merchant_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM outlets WHERE merchant_id = $1 ORDER BY created_at DESC", [filter.merchant_id]);
|
||||
return rows.map(mapOutlet);
|
||||
clauses.push(`merchant_id = $${i++}`);
|
||||
params.push(filter.merchant_id);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM outlets ORDER BY created_at DESC");
|
||||
if (filter?.status) {
|
||||
clauses.push(`status = $${i++}`);
|
||||
params.push(filter.status);
|
||||
}
|
||||
if (filter?.q) {
|
||||
const value = `%${filter.q.toLowerCase()}%`;
|
||||
clauses.push(`(LOWER(name) LIKE $${i++} OR LOWER(outlet_code) LIKE $${i++})`);
|
||||
params.push(value, value);
|
||||
}
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM outlets ${where} ORDER BY created_at DESC`, params);
|
||||
return rows.map(mapOutlet);
|
||||
}
|
||||
export async function listTerminals(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.outlet_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM terminals WHERE outlet_id = $1 ORDER BY created_at DESC", [filter.outlet_id]);
|
||||
return rows.map(mapTerminal);
|
||||
clauses.push(`outlet_id = $${i++}`);
|
||||
params.push(filter.outlet_id);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM terminals ORDER BY created_at DESC");
|
||||
if (filter?.status) {
|
||||
clauses.push(`status = $${i++}`);
|
||||
params.push(filter.status);
|
||||
}
|
||||
if (filter?.q) {
|
||||
const value = `%${filter.q.toLowerCase()}%`;
|
||||
clauses.push(`(LOWER(terminal_code) LIKE $${i++} OR LOWER(COALESCE(partner_reference, '')) LIKE $${i++})`);
|
||||
params.push(value, value);
|
||||
}
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM terminals ${where} ORDER BY created_at DESC`, params);
|
||||
return rows.map(mapTerminal);
|
||||
}
|
||||
export async function getOutletById(id) {
|
||||
|
||||
74
dist/shared/store/mqttMessageStore.js
vendored
Normal file
74
dist/shared/store/mqttMessageStore.js
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapMessage(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
direction: row.direction,
|
||||
device_id: row.device_id,
|
||||
topic: row.topic,
|
||||
message_type: row.message_type,
|
||||
correlation_id: row.correlation_id || undefined,
|
||||
payload_json: row.payload_json || {},
|
||||
publish_status: row.publish_status,
|
||||
reason: row.reason || undefined,
|
||||
created_at: row.created_at
|
||||
};
|
||||
}
|
||||
export async function createMqttMessage(payload) {
|
||||
const { rows } = await getPool().query(`INSERT INTO mqtt_messages (
|
||||
id,
|
||||
direction,
|
||||
device_id,
|
||||
topic,
|
||||
message_type,
|
||||
correlation_id,
|
||||
payload_json,
|
||||
publish_status,
|
||||
reason,
|
||||
created_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||
RETURNING *`, [
|
||||
`mqtt_${randomUUID()}`,
|
||||
payload.direction,
|
||||
payload.device_id,
|
||||
payload.topic,
|
||||
payload.message_type,
|
||||
payload.correlation_id || null,
|
||||
payload.payload_json || {},
|
||||
payload.publish_status || "recorded",
|
||||
payload.reason || null,
|
||||
nowIso()
|
||||
]);
|
||||
return mapMessage(rows[0]);
|
||||
}
|
||||
export async function listMqttMessages(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.device_id) {
|
||||
clauses.push(`device_id = $${i++}`);
|
||||
params.push(filter.device_id);
|
||||
}
|
||||
if (filter?.direction) {
|
||||
clauses.push(`direction = $${i++}`);
|
||||
params.push(filter.direction);
|
||||
}
|
||||
if (filter?.message_type) {
|
||||
clauses.push(`message_type = $${i++}`);
|
||||
params.push(filter.message_type);
|
||||
}
|
||||
if (filter?.correlation_id) {
|
||||
clauses.push(`correlation_id = $${i++}`);
|
||||
params.push(filter.correlation_id);
|
||||
}
|
||||
const limit = Math.min(Math.max(filter?.limit || 100, 1), 500);
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM mqtt_messages ${where} ORDER BY created_at DESC LIMIT ${limit}`, params);
|
||||
return rows.map(mapMessage);
|
||||
}
|
||||
export function toMqttMessagePayload(message) {
|
||||
return { ...message };
|
||||
}
|
||||
7
dist/shared/store/transactionStore.js
vendored
7
dist/shared/store/transactionStore.js
vendored
@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
import { createPaidLedgerPlaceholder } from "./ledgerStore";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
@ -161,7 +162,11 @@ export async function updateTransactionStatus(id, to, options) {
|
||||
...options.eventContext
|
||||
}
|
||||
});
|
||||
return mapTransaction(rows[0]);
|
||||
const updated = mapTransaction(rows[0]);
|
||||
if (to === "paid") {
|
||||
await createPaidLedgerPlaceholder(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
export async function getTransactionById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE id = $1", [id]);
|
||||
|
||||
Reference in New Issue
Block a user