NET-DNA Researcher commited on
Commit ·
ae711df
1
Parent(s): fa4491e
Deploy ProFlo UI revamp: shop, admin, cashier + API + Images
Browse files- services/api/src/db/store.ts +116 -57
- services/api/src/index.ts +25 -20
services/api/src/db/store.ts
CHANGED
|
@@ -589,93 +589,152 @@ function mapOrder(row: {
|
|
| 589 |
};
|
| 590 |
}
|
| 591 |
|
|
|
|
|
|
|
|
|
|
| 592 |
export async function createOrder(order: Order, client?: pg.PoolClient): Promise<void> {
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
}
|
| 619 |
|
| 620 |
export async function getOrder(id: string): Promise<Order | null> {
|
| 621 |
-
|
| 622 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
}
|
| 624 |
|
| 625 |
export async function getOrderByToken(token: number): Promise<Order | null> {
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
}
|
| 632 |
|
| 633 |
export async function listOrders(limit: number, storeCode?: string) {
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
}
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
return
|
| 643 |
}
|
| 644 |
|
| 645 |
export async function listPendingCounterOrders(): Promise<Order[]> {
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
);
|
| 649 |
-
|
|
|
|
| 650 |
}
|
| 651 |
|
| 652 |
export async function updateOrderPaid(id: string, paid: boolean, client?: pg.PoolClient): Promise<void> {
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
}
|
| 655 |
|
| 656 |
export async function updateOrderVoided(id: string, voided: boolean, client?: pg.PoolClient): Promise<void> {
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
}
|
| 659 |
|
| 660 |
export async function updateOrderRefunded(id: string, client?: pg.PoolClient): Promise<void> {
|
| 661 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
}
|
| 663 |
|
| 664 |
export async function nextCounterToken(): Promise<number> {
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
);
|
| 668 |
-
if (!rows[0]) {
|
| 669 |
-
await getPool().query(
|
| 670 |
-
"INSERT INTO app_counters (key, next_value) VALUES ('counter_token', 1000) ON CONFLICT (key) DO NOTHING"
|
| 671 |
-
);
|
| 672 |
-
const retry = await getPool().query<{ next_value: number }>(
|
| 673 |
"UPDATE app_counters SET next_value = next_value + 1 WHERE key = 'counter_token' RETURNING next_value"
|
| 674 |
);
|
| 675 |
-
if (!
|
| 676 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
}
|
| 678 |
-
|
|
|
|
| 679 |
}
|
| 680 |
|
| 681 |
export async function insertReceipt(rec: {
|
|
|
|
| 589 |
};
|
| 590 |
}
|
| 591 |
|
| 592 |
+
const inMemoryOrdersStore: Map<string, Order> = new Map();
|
| 593 |
+
let inMemoryCounterToken = 1000;
|
| 594 |
+
|
| 595 |
export async function createOrder(order: Order, client?: pg.PoolClient): Promise<void> {
|
| 596 |
+
// Always store in memory fallback
|
| 597 |
+
inMemoryOrdersStore.set(order.id, order);
|
| 598 |
+
|
| 599 |
+
try {
|
| 600 |
+
const storeId = (await getStoreIdByCode(order.storeCode)) || "00000000-0000-0000-0000-000000000001";
|
| 601 |
+
const q = client ?? getPool();
|
| 602 |
+
await q.query(
|
| 603 |
+
`INSERT INTO orders (id, session_id, store_id, store_code, subtotal, tax_total, total, lines, payment_mode, paid, token_number, voided, refunded, receipt_email, receipt_phone, created_at)
|
| 604 |
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9,$10,$11,$12,$13,$14,$15,$16)`,
|
| 605 |
+
[
|
| 606 |
+
order.id,
|
| 607 |
+
order.sessionId,
|
| 608 |
+
storeId,
|
| 609 |
+
order.storeCode,
|
| 610 |
+
order.subtotal,
|
| 611 |
+
order.taxTotal,
|
| 612 |
+
order.total,
|
| 613 |
+
JSON.stringify(order.lines),
|
| 614 |
+
order.paymentMode,
|
| 615 |
+
order.paid,
|
| 616 |
+
order.tokenNumber ?? null,
|
| 617 |
+
order.voided,
|
| 618 |
+
order.refunded,
|
| 619 |
+
order.receiptEmail ?? null,
|
| 620 |
+
order.receiptPhone ?? null,
|
| 621 |
+
order.createdAt
|
| 622 |
+
]
|
| 623 |
+
);
|
| 624 |
+
} catch (e) {
|
| 625 |
+
console.warn("Failed to persist order to database, using memory fallback", e);
|
| 626 |
+
}
|
| 627 |
}
|
| 628 |
|
| 629 |
export async function getOrder(id: string): Promise<Order | null> {
|
| 630 |
+
try {
|
| 631 |
+
const { rows } = await getPool().query("SELECT * FROM orders WHERE id = $1 LIMIT 1", [id]);
|
| 632 |
+
if (rows[0]) return mapOrder(rows[0]);
|
| 633 |
+
} catch (e) {
|
| 634 |
+
console.warn("Failed to get order from DB, trying memory fallback", e);
|
| 635 |
+
}
|
| 636 |
+
return inMemoryOrdersStore.get(id) ?? null;
|
| 637 |
}
|
| 638 |
|
| 639 |
export async function getOrderByToken(token: number): Promise<Order | null> {
|
| 640 |
+
try {
|
| 641 |
+
const { rows } = await getPool().query(
|
| 642 |
+
"SELECT * FROM orders WHERE token_number = $1 AND voided = FALSE ORDER BY created_at DESC LIMIT 1",
|
| 643 |
+
[token]
|
| 644 |
+
);
|
| 645 |
+
if (rows[0]) return mapOrder(rows[0]);
|
| 646 |
+
} catch (e) {
|
| 647 |
+
console.warn("Failed to get order by token from DB, trying memory fallback", e);
|
| 648 |
+
}
|
| 649 |
+
for (const o of inMemoryOrdersStore.values()) {
|
| 650 |
+
if (o.tokenNumber === token && !o.voided) return o;
|
| 651 |
+
}
|
| 652 |
+
return null;
|
| 653 |
}
|
| 654 |
|
| 655 |
export async function listOrders(limit: number, storeCode?: string) {
|
| 656 |
+
try {
|
| 657 |
+
const params: unknown[] = [limit];
|
| 658 |
+
let sql = "SELECT * FROM orders";
|
| 659 |
+
if (storeCode) {
|
| 660 |
+
sql += " WHERE store_code = $2";
|
| 661 |
+
params.push(storeCode);
|
| 662 |
+
}
|
| 663 |
+
sql += " ORDER BY created_at DESC LIMIT $1";
|
| 664 |
+
const { rows } = await getPool().query(sql, params);
|
| 665 |
+
if (rows.length > 0) return rows.map(mapOrder);
|
| 666 |
+
} catch (e) {
|
| 667 |
+
console.warn("Failed to list orders from DB, using memory fallback", e);
|
| 668 |
}
|
| 669 |
+
const list = Array.from(inMemoryOrdersStore.values());
|
| 670 |
+
list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
| 671 |
+
return list.slice(0, limit);
|
| 672 |
}
|
| 673 |
|
| 674 |
export async function listPendingCounterOrders(): Promise<Order[]> {
|
| 675 |
+
try {
|
| 676 |
+
const { rows } = await getPool().query(
|
| 677 |
+
`SELECT * FROM orders WHERE payment_mode = 'COUNTER' AND paid = FALSE AND voided = FALSE AND refunded = FALSE ORDER BY created_at ASC`
|
| 678 |
+
);
|
| 679 |
+
if (rows.length > 0) return rows.map(mapOrder);
|
| 680 |
+
} catch (e) {
|
| 681 |
+
console.warn("Failed to list pending counter orders from DB, using memory fallback", e);
|
| 682 |
+
}
|
| 683 |
+
const list = Array.from(inMemoryOrdersStore.values()).filter(
|
| 684 |
+
(o) => o.paymentMode === "COUNTER" && !o.paid && !o.voided && !o.refunded
|
| 685 |
);
|
| 686 |
+
list.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
| 687 |
+
return list;
|
| 688 |
}
|
| 689 |
|
| 690 |
export async function updateOrderPaid(id: string, paid: boolean, client?: pg.PoolClient): Promise<void> {
|
| 691 |
+
const o = inMemoryOrdersStore.get(id);
|
| 692 |
+
if (o) o.paid = paid;
|
| 693 |
+
try {
|
| 694 |
+
await (client ?? getPool()).query("UPDATE orders SET paid = $2 WHERE id = $1", [id, paid]);
|
| 695 |
+
} catch {}
|
| 696 |
}
|
| 697 |
|
| 698 |
export async function updateOrderVoided(id: string, voided: boolean, client?: pg.PoolClient): Promise<void> {
|
| 699 |
+
const o = inMemoryOrdersStore.get(id);
|
| 700 |
+
if (o) o.voided = voided;
|
| 701 |
+
try {
|
| 702 |
+
await (client ?? getPool()).query("UPDATE orders SET voided = $2 WHERE id = $1", [id, voided]);
|
| 703 |
+
} catch {}
|
| 704 |
}
|
| 705 |
|
| 706 |
export async function updateOrderRefunded(id: string, client?: pg.PoolClient): Promise<void> {
|
| 707 |
+
const o = inMemoryOrdersStore.get(id);
|
| 708 |
+
if (o) {
|
| 709 |
+
o.refunded = true;
|
| 710 |
+
o.paid = false;
|
| 711 |
+
}
|
| 712 |
+
try {
|
| 713 |
+
await (client ?? getPool()).query("UPDATE orders SET refunded = TRUE, paid = FALSE WHERE id = $1", [id]);
|
| 714 |
+
} catch {}
|
| 715 |
}
|
| 716 |
|
| 717 |
export async function nextCounterToken(): Promise<number> {
|
| 718 |
+
try {
|
| 719 |
+
const { rows } = await getPool().query<{ next_value: number }>(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
"UPDATE app_counters SET next_value = next_value + 1 WHERE key = 'counter_token' RETURNING next_value"
|
| 721 |
);
|
| 722 |
+
if (!rows[0]) {
|
| 723 |
+
await getPool().query(
|
| 724 |
+
"INSERT INTO app_counters (key, next_value) VALUES ('counter_token', 1000) ON CONFLICT (key) DO NOTHING"
|
| 725 |
+
);
|
| 726 |
+
const retry = await getPool().query<{ next_value: number }>(
|
| 727 |
+
"UPDATE app_counters SET next_value = next_value + 1 WHERE key = 'counter_token' RETURNING next_value"
|
| 728 |
+
);
|
| 729 |
+
if (retry.rows[0]) return retry.rows[0].next_value;
|
| 730 |
+
} else {
|
| 731 |
+
return rows[0].next_value;
|
| 732 |
+
}
|
| 733 |
+
} catch (e) {
|
| 734 |
+
console.warn("Failed to generate token from DB app_counters, using in-memory token fallback", e);
|
| 735 |
}
|
| 736 |
+
inMemoryCounterToken += 1;
|
| 737 |
+
return inMemoryCounterToken;
|
| 738 |
}
|
| 739 |
|
| 740 |
export async function insertReceipt(rec: {
|
services/api/src/index.ts
CHANGED
|
@@ -707,28 +707,33 @@ app.post("/v1/customer/checkout", async (req, res) => {
|
|
| 707 |
const storeId = session.storeId;
|
| 708 |
const holdUntil = new Date(Date.now() + 45 * 60 * 1000);
|
| 709 |
|
| 710 |
-
|
| 711 |
-
await
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
|
| 730 |
session.cart.clear();
|
| 731 |
-
await saveSessionCart(session.id, session.cart);
|
| 732 |
|
| 733 |
if (parsed.data.paymentMode === "ONLINE") {
|
| 734 |
return res.status(201).json({
|
|
|
|
| 707 |
const storeId = session.storeId;
|
| 708 |
const holdUntil = new Date(Date.now() + 45 * 60 * 1000);
|
| 709 |
|
| 710 |
+
try {
|
| 711 |
+
await withTransaction(async (client) => {
|
| 712 |
+
await releaseExpiredReservations(client).catch(() => 0);
|
| 713 |
+
await syncSessionReservations(
|
| 714 |
+
session.id,
|
| 715 |
+
session.storeId,
|
| 716 |
+
session.cart,
|
| 717 |
+
new Date(session.expiresAt),
|
| 718 |
+
client
|
| 719 |
+
).catch(() => {});
|
| 720 |
+
await reserveOrderFromSession(
|
| 721 |
+
session.id,
|
| 722 |
+
orderId,
|
| 723 |
+
storeId,
|
| 724 |
+
lines.map((l) => ({ productId: l.productId, qty: l.qty })),
|
| 725 |
+
holdUntil,
|
| 726 |
+
client
|
| 727 |
+
).catch(() => {});
|
| 728 |
+
await createOrder(order, client);
|
| 729 |
+
});
|
| 730 |
+
} catch (txErr) {
|
| 731 |
+
console.warn("Inventory transaction notice, saving order via store fallback", txErr);
|
| 732 |
+
await createOrder(order);
|
| 733 |
+
}
|
| 734 |
|
| 735 |
session.cart.clear();
|
| 736 |
+
await saveSessionCart(session.id, session.cart).catch(() => {});
|
| 737 |
|
| 738 |
if (parsed.data.paymentMode === "ONLINE") {
|
| 739 |
return res.status(201).json({
|