From 8a626b17fc5b689447937db63cb6fb46def35790 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 13 Oct 2026 13:21:11 +0200 Subject: [PATCH] u --- API.md | 2 + API.order.md | 18 ++ src/index.js | 2 + src/providers/mssql/purgeSql.js | 1 + src/providers/mssql/schema.js | 39 +++ src/providers/sqlite3/schema.js | 32 +++ src/store/order/create.js | 3 + src/store/order/list.js | 114 +++------ src/store/order/rebuildSummaries.js | 19 ++ src/store/order/syncSummary.js | 364 ++++++++++++++++++++++++++++ src/store/order/update.js | 2 + src/store/payment/create.js | 2 + src/store/payment/delete.js | 12 + src/store/shipment/create.js | 3 + src/store/shipment/delete.js | 11 + 15 files changed, 541 insertions(+), 83 deletions(-) create mode 100644 src/store/order/rebuildSummaries.js create mode 100644 src/store/order/syncSummary.js diff --git a/API.md b/API.md index a3d808c..e5f4043 100644 --- a/API.md +++ b/API.md @@ -185,6 +185,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, offset: number, limit: number}|Array>` + - `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void` + - `syncSummary(): void` - `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - [shipment](API.shipment.md) - `create(provider: import('../../providers/provider.js').DatabaseProvider): void` diff --git a/API.order.md b/API.order.md index 91731fc..2d91f03 100644 --- a/API.order.md +++ b/API.order.md @@ -127,6 +127,24 @@ const orders = await store.order.list(42); // => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }] ``` +## `order.rebuildSummaries` + +*Source: `src/store/order/rebuildSummaries.js`* + +Rebuilds the `ordersummary` table for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `provider` | `import('../../providers/provider.js').DatabaseProvider` | | + +## `order.syncSummary` + +*Source: `src/store/order/syncSummary.js`* + +Centralized sync and rebuild logic for the materialized `ordersummary` table. + ## `order.update` *Source: `src/store/order/update.js`* diff --git a/src/index.js b/src/index.js index fa7b2ee..ddd7ce1 100644 --- a/src/index.js +++ b/src/index.js @@ -98,6 +98,7 @@ import orderGetById from './store/order/getById.js'; import orderCreate from './store/order/create.js'; import orderUpdate from './store/order/update.js'; import orderDelete from './store/order/delete.js'; +import orderRebuildSummaries from './store/order/rebuildSummaries.js'; import posterminalList from './store/posterminal/list.js'; import posterminalUpsert from './store/posterminal/upsert.js'; import posterminalDelete from './store/posterminal/delete.js'; @@ -427,6 +428,7 @@ export default class LibDashStore { create: orderCreate(provider), update: orderUpdate(provider), delete: orderDelete(provider), + rebuildSummaries: orderRebuildSummaries(provider), }; this.posterminal = { diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index a19ceb0..1c97f67 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -1,5 +1,6 @@ /** Drop order = reverse of schema create dependency order. */ export const TABLES = [ + 'ordersummary', 'package', 'shipping', 'payment', diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 5bfd728..3a943e8 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -1177,6 +1177,44 @@ BEGIN END `; +export const INIT_ORDERSUMMARY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ordersummary') +BEGIN + CREATE TABLE ordersummary ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + manifest_id INT NOT NULL CONSTRAINT UQ_ordersummary_manifest UNIQUE, + order_number NVARCHAR(100) NOT NULL, + order_date DATETIME2 NOT NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_ordersummary_created_at DEFAULT SYSUTCDATETIME(), + entity_id INT NULL, + customer_name NVARCHAR(255) NULL, + positions INT NOT NULL CONSTRAINT DF_ordersummary_positions DEFAULT 0, + gross_total FLOAT NOT NULL CONSTRAINT DF_ordersummary_gross_total DEFAULT 0, + paid_total FLOAT NOT NULL CONSTRAINT DF_ordersummary_paid_total DEFAULT 0, + ordered_quantity FLOAT NOT NULL CONSTRAINT DF_ordersummary_ordered_quantity DEFAULT 0, + shipped_quantity FLOAT NOT NULL CONSTRAINT DF_ordersummary_shipped_quantity DEFAULT 0, + is_paid TINYINT NOT NULL CONSTRAINT DF_ordersummary_is_paid DEFAULT 0, + is_shipped TINYINT NOT NULL CONSTRAINT DF_ordersummary_is_shipped DEFAULT 0, + CONSTRAINT FK_ordersummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION, + CONSTRAINT FK_ordersummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, + CONSTRAINT FK_ordersummary_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL + ); + + CREATE INDEX IX_ordersummary_company_date + ON ordersummary (company_id, order_date DESC, manifest_id DESC); + + CREATE INDEX IX_ordersummary_company_shipped + ON ordersummary (company_id, is_shipped); + + CREATE INDEX IX_ordersummary_company_paid + ON ordersummary (company_id, is_paid); + + CREATE INDEX IX_ordersummary_company_number + ON ordersummary (company_id, order_number); +END +`; + export const INIT_MCP_CLIENT = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client') BEGIN @@ -1299,6 +1337,7 @@ export const SCHEMA_SCRIPTS = [ INIT_PAYMENT, INIT_SHIPPING, INIT_PACKAGE, + INIT_ORDERSUMMARY, INIT_MCP_CLIENT, INIT_MCP_AUTH_CODE, INIT_MCP_TOKEN, diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index f01f912..1cc6975 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -845,6 +845,37 @@ CREATE TABLE IF NOT EXISTS package ( ); `; +export const INIT_ORDERSUMMARY = ` +CREATE TABLE IF NOT EXISTS ordersummary ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_id INTEGER NOT NULL, + manifest_id INTEGER NOT NULL UNIQUE, + order_number TEXT NOT NULL, + order_date TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + entity_id INTEGER NULL, + customer_name TEXT NULL, + positions INTEGER NOT NULL DEFAULT 0, + gross_total REAL NOT NULL DEFAULT 0, + paid_total REAL NOT NULL DEFAULT 0, + ordered_quantity REAL NOT NULL DEFAULT 0, + shipped_quantity REAL NOT NULL DEFAULT 0, + is_paid INTEGER NOT NULL DEFAULT 0, + is_shipped INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION, + FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS IX_ordersummary_company_date + ON ordersummary (company_id, order_date DESC, manifest_id DESC); +CREATE INDEX IF NOT EXISTS IX_ordersummary_company_shipped + ON ordersummary (company_id, is_shipped); +CREATE INDEX IF NOT EXISTS IX_ordersummary_company_paid + ON ordersummary (company_id, is_paid); +CREATE INDEX IF NOT EXISTS IX_ordersummary_company_number + ON ordersummary (company_id, order_number); +`; + export const INIT_MCP_CLIENT = ` CREATE TABLE IF NOT EXISTS mcp_client ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -953,6 +984,7 @@ export const SCHEMA_SCRIPTS = [ INIT_PAYMENT, INIT_SHIPPING, INIT_PACKAGE, + INIT_ORDERSUMMARY, INIT_MCP_CLIENT, INIT_MCP_AUTH_CODE, INIT_MCP_TOKEN, diff --git a/src/store/order/create.js b/src/store/order/create.js index 2fcdc31..63e86fb 100644 --- a/src/store/order/create.js +++ b/src/store/order/create.js @@ -17,6 +17,7 @@ import taxEnsure from '../tax/ensure.js'; import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js'; import { recordOrderHistory } from '../articlehistory/record.js'; +import { syncOrderSummary } from './syncSummary.js'; import makeRunner from '../runner.js'; export default function create(provider) { @@ -129,6 +130,8 @@ export default function create(provider) { id: manifestId, }); + await syncOrderSummary(runner, { companyId, manifestId }); + return doc; }; diff --git a/src/store/order/list.js b/src/store/order/list.js index 766b58d..8d0864a 100644 --- a/src/store/order/list.js +++ b/src/store/order/list.js @@ -124,97 +124,19 @@ export default function list(provider) { // Only unpaid (strictly boolean true; overpaid orders also count as unpaid) if (opts?.onlyUnpaid === true) { - conditions.push(`( - COALESCE(( - SELECT SUM(ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0)) - FROM manifestarticle ma - WHERE ma.manifest_id = m.id - ), 0) = 0 - OR - ABS( - COALESCE(( - SELECT SUM(p.amount) - FROM payment p - WHERE p.manifest_id = m.id - ), 0) - - - COALESCE(( - SELECT SUM(ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0)) - FROM manifestarticle ma - WHERE ma.manifest_id = m.id - ), 0) - ) >= 0.01 - )`); + conditions.push('COALESCE(os.is_paid, 0) = 0'); } // Only undelivered (strictly boolean true) if (opts?.onlyUndelivered === true) { - const prefixRes = await runner.query(sqlShipmentPrefix, { companyId }); - params.shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-'; - - if (isSqlite) { - conditions.push(`NOT ( - ( - COALESCE((SELECT SUM(ma.amount) FROM manifestarticle ma WHERE ma.manifest_id = m.id), 0) > 0 - AND - COALESCE(( - SELECT SUM(ma_s.amount) - FROM manifest m_s - JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id - WHERE m_s.company_id = m.company_id - AND m_s."type" = 'shipment' - AND m_s."number" LIKE (m."number" || @shipmentPrefix || '%') - ), 0) >= (SELECT SUM(ma.amount) FROM manifestarticle ma WHERE ma.manifest_id = m.id) - 0.001 - ) - OR EXISTS ( - SELECT 1 FROM shipping s - WHERE ( - s.manifest_id = m.id - OR s.manifest_id IN ( - SELECT m_s.id FROM manifest m_s - WHERE m_s.company_id = m.company_id - AND m_s."type" = 'shipment' - AND m_s."number" LIKE (m."number" || @shipmentPrefix || '%') - ) - ) - AND LOWER(s."state") IN ('shipped', 'delivered') - ) - )`); - } else { - conditions.push(`NOT ( - ( - COALESCE((SELECT SUM(ma.amount) FROM manifestarticle ma WHERE ma.manifest_id = m.id), 0) > 0 - AND - COALESCE(( - SELECT SUM(ma_s.amount) - FROM manifest m_s - JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id - WHERE m_s.company_id = m.company_id - AND m_s.[type] = 'shipment' - AND m_s.[number] LIKE (m.[number] + @shipmentPrefix + '%') - ), 0) >= (SELECT SUM(ma.amount) FROM manifestarticle ma WHERE ma.manifest_id = m.id) - 0.001 - ) - OR EXISTS ( - SELECT 1 FROM shipping s - WHERE ( - s.manifest_id = m.id - OR s.manifest_id IN ( - SELECT m_s.id FROM manifest m_s - WHERE m_s.company_id = m.company_id - AND m_s.[type] = 'shipment' - AND m_s.[number] LIKE (m.[number] + @shipmentPrefix + '%') - ) - ) - AND LOWER(s.[state]) IN ('shipped', 'delivered') - ) - )`); - } + conditions.push('COALESCE(os.is_shipped, 0) = 0'); } const whereClause = conditions.join('\n AND '); const sqlCount = ` SELECT COUNT(*) AS total FROM manifest m + LEFT JOIN ordersummary os ON os.manifest_id = m.id LEFT JOIN entity e ON e.id = m.entity_id WHERE ${whereClause} `; @@ -229,8 +151,17 @@ export default function list(provider) { m.entity_id AS entityId, m.external_number AS externalNumber, m."date", - m.created_at AS createdAt + m.created_at AS createdAt, + os.positions, + os.gross_total AS grossTotal, + os.paid_total AS paidTotal, + os.ordered_quantity AS orderedQuantity, + os.shipped_quantity AS shippedQuantity, + os.is_paid AS isPaid, + os.is_shipped AS isShipped, + os.customer_name AS customerName FROM manifest m + LEFT JOIN ordersummary os ON os.manifest_id = m.id LEFT JOIN entity e ON e.id = m.entity_id WHERE ${whereClause} ORDER BY m."date" DESC, m.id DESC @@ -245,8 +176,17 @@ export default function list(provider) { m.entity_id AS entityId, m.external_number AS externalNumber, m.[date], - m.created_at AS createdAt + m.created_at AS createdAt, + os.positions, + os.gross_total AS grossTotal, + os.paid_total AS paidTotal, + os.ordered_quantity AS orderedQuantity, + os.shipped_quantity AS shippedQuantity, + os.is_paid AS isPaid, + os.is_shipped AS isShipped, + os.customer_name AS customerName FROM manifest m + LEFT JOIN ordersummary os ON os.manifest_id = m.id LEFT JOIN entity e ON e.id = m.entity_id WHERE ${whereClause} ORDER BY m.[date] DESC, m.id DESC @@ -292,6 +232,14 @@ export default function list(provider) { const orders = manifestRows.map((row) => ({ ...shapeSummary(orderType, row), lines: linesByManifestId.get(row.id) ?? [], + positions: row.positions !== undefined && row.positions !== null ? Number(row.positions) : undefined, + grossTotal: row.grossTotal !== undefined && row.grossTotal !== null ? Number(row.grossTotal) : undefined, + paidTotal: row.paidTotal !== undefined && row.paidTotal !== null ? Number(row.paidTotal) : undefined, + orderedQuantity: row.orderedQuantity !== undefined && row.orderedQuantity !== null ? Number(row.orderedQuantity) : undefined, + shippedQuantity: row.shippedQuantity !== undefined && row.shippedQuantity !== null ? Number(row.shippedQuantity) : undefined, + isPaid: row.isPaid !== undefined && row.isPaid !== null ? Boolean(row.isPaid) : undefined, + isShipped: row.isShipped !== undefined && row.isShipped !== null ? Boolean(row.isShipped) : undefined, + customerName: row.customerName ?? undefined, })); if (isPaged) { diff --git a/src/store/order/rebuildSummaries.js b/src/store/order/rebuildSummaries.js new file mode 100644 index 0000000..ab4ebd9 --- /dev/null +++ b/src/store/order/rebuildSummaries.js @@ -0,0 +1,19 @@ +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { rebuildOrderSummaries } from './syncSummary.js'; + +/** + * Rebuilds the `ordersummary` table for the user's selected company. + * + * @param {import('../../providers/provider.js').DatabaseProvider} provider + */ +export default function rebuildSummaries(provider) { + const access = resolveSelectedCompanyAccess(provider); + + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + await rebuildOrderSummaries(runner, companyId); + return { ok: true, companyId }; + }; +} diff --git a/src/store/order/syncSummary.js b/src/store/order/syncSummary.js new file mode 100644 index 0000000..d7adac4 --- /dev/null +++ b/src/store/order/syncSummary.js @@ -0,0 +1,364 @@ +/** + * Centralized sync and rebuild logic for the materialized `ordersummary` table. + */ + +const SQL_GET_SHIPMENT_PREFIX = ` + SELECT prefix FROM numbers WHERE company_id = @companyId AND [type] = 'shipment' +`; + +const SQL_GET_ORDER_BY_ID_SQLITE = ` + SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id + FROM manifest m + WHERE m.id = @manifestId AND m.company_id = @companyId AND m."type" = 'order' +`; + +const SQL_GET_ORDER_BY_ID_MSSQL = ` + SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id + FROM manifest m + WHERE m.id = @manifestId AND m.company_id = @companyId AND m.[type] = 'order' +`; + +const SQL_GET_ORDER_BY_NUMBER_SQLITE = ` + SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id + FROM manifest m + WHERE m."number" = @number AND m.company_id = @companyId AND m."type" = 'order' +`; + +const SQL_GET_ORDER_BY_NUMBER_MSSQL = ` + SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id + FROM manifest m + WHERE m.[number] = @number AND m.company_id = @companyId AND m.[type] = 'order' +`; + +const SQL_LINE_TOTALS = ` + SELECT + COUNT(*) AS positions, + COALESCE(SUM(amount), 0) AS ordered_quantity, + COALESCE(SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)), 0) AS gross_total + FROM manifestarticle + WHERE manifest_id = @manifestId +`; + +const SQL_PAYMENT_TOTAL = ` + SELECT COALESCE(SUM(amount), 0) AS paid_total + FROM payment + WHERE manifest_id = @manifestId +`; + +const SQL_SHIPPED_QTY_SQLITE = ` + SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity + FROM manifest m_s + JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = @companyId + AND m_s."type" = 'shipment' + AND m_s."number" >= (@orderNumber || @shipmentPrefix) + AND m_s."number" < (@orderNumber || @shipmentPrefix || char(127)) +`; + +const SQL_SHIPPED_QTY_MSSQL = ` + SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity + FROM manifest m_s + JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = @companyId + AND m_s.[type] = 'shipment' + AND m_s.[number] >= (@orderNumber + @shipmentPrefix) + AND m_s.[number] < (@orderNumber + @shipmentPrefix + NCHAR(127)) +`; + +const SQL_CUSTOMER_NAME_SQLITE = ` + SELECT TRIM(COALESCE(first_name || ' ', '') || COALESCE(name, '')) AS customer_name + FROM entity + WHERE id = @entityId +`; + +const SQL_CUSTOMER_NAME_MSSQL = ` + SELECT LTRIM(RTRIM(ISNULL(first_name + ' ', '') + ISNULL(name, ''))) AS customer_name + FROM entity + WHERE id = @entityId +`; + +const SQL_UPSERT_SUMMARY_SQLITE = ` + INSERT INTO ordersummary ( + company_id, manifest_id, order_number, order_date, created_at, + entity_id, customer_name, positions, gross_total, paid_total, + ordered_quantity, shipped_quantity, is_paid, is_shipped + ) VALUES ( + @companyId, @manifestId, @orderNumber, @orderDate, @createdAt, + @entityId, @customerName, @positions, @grossTotal, @paidTotal, + @orderedQuantity, @shippedQuantity, @isPaid, @isShipped + ) + ON CONFLICT(manifest_id) DO UPDATE SET + order_number = excluded.order_number, + order_date = excluded.order_date, + created_at = excluded.created_at, + entity_id = excluded.entity_id, + customer_name = excluded.customer_name, + positions = excluded.positions, + gross_total = excluded.gross_total, + paid_total = excluded.paid_total, + ordered_quantity = excluded.ordered_quantity, + shipped_quantity = excluded.shipped_quantity, + is_paid = excluded.is_paid, + is_shipped = excluded.is_shipped +`; + +const SQL_UPSERT_SUMMARY_MSSQL = ` + MERGE INTO ordersummary AS target + USING (SELECT @manifestId AS manifest_id) AS source + ON (target.manifest_id = source.manifest_id) + WHEN MATCHED THEN + UPDATE SET + order_number = @orderNumber, + order_date = @orderDate, + created_at = @createdAt, + entity_id = @entityId, + customer_name = @customerName, + positions = @positions, + gross_total = @grossTotal, + paid_total = @paidTotal, + ordered_quantity = @orderedQuantity, + shipped_quantity = @shippedQuantity, + is_paid = @isPaid, + is_shipped = @isShipped + WHEN NOT MATCHED THEN + INSERT ( + company_id, manifest_id, order_number, order_date, created_at, + entity_id, customer_name, positions, gross_total, paid_total, + ordered_quantity, shipped_quantity, is_paid, is_shipped + ) VALUES ( + @companyId, @manifestId, @orderNumber, @orderDate, @createdAt, + @entityId, @customerName, @positions, @grossTotal, @paidTotal, + @orderedQuantity, @shippedQuantity, @isPaid, @isShipped + ); +`; + +/** + * Recalculates and upserts a single order summary into `ordersummary`. + * + * @param {object} runner - Database runner. + * @param {object} opts + * @param {number} opts.companyId + * @param {number} [opts.manifestId] - Order manifest id. + * @param {string} [opts.orderNumber] - Order number (if manifestId not given). + */ +export async function syncOrderSummary(runner, { companyId, manifestId, orderNumber }) { + const isSqlite = runner.dialect === 'sqlite3'; + + let orderRow; + if (manifestId) { + const res = await runner.query( + isSqlite ? SQL_GET_ORDER_BY_ID_SQLITE : SQL_GET_ORDER_BY_ID_MSSQL, + { manifestId, companyId }, + ); + orderRow = res.rows[0]; + } else if (orderNumber) { + const res = await runner.query( + isSqlite ? SQL_GET_ORDER_BY_NUMBER_SQLITE : SQL_GET_ORDER_BY_NUMBER_MSSQL, + { number: orderNumber, companyId }, + ); + orderRow = res.rows[0]; + } + + if (!orderRow) { + if (manifestId) { + await runner.query('DELETE FROM ordersummary WHERE manifest_id = @manifestId', { manifestId }); + } + return; + } + + const orderId = orderRow.id; + const num = orderRow.number; + + const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId }); + const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-'; + + const [lineRes, payRes, shipRes] = await Promise.all([ + runner.query(SQL_LINE_TOTALS, { manifestId: orderId }), + runner.query(SQL_PAYMENT_TOTAL, { manifestId: orderId }), + runner.query(isSqlite ? SQL_SHIPPED_QTY_SQLITE : SQL_SHIPPED_QTY_MSSQL, { + companyId, + orderNumber: num, + shipmentPrefix, + }), + ]); + + let customerName = null; + if (orderRow.entity_id) { + const custRes = await runner.query( + isSqlite ? SQL_CUSTOMER_NAME_SQLITE : SQL_CUSTOMER_NAME_MSSQL, + { entityId: orderRow.entity_id }, + ); + customerName = custRes.rows[0]?.customer_name || null; + } + + const positions = Number(lineRes.rows[0]?.positions || 0); + const orderedQuantity = Math.round(Number(lineRes.rows[0]?.ordered_quantity || 0) * 1000) / 1000; + const grossTotal = Math.round(Number(lineRes.rows[0]?.gross_total || 0) * 100) / 100; + const paidTotal = Math.round(Number(payRes.rows[0]?.paid_total || 0) * 100) / 100; + const shippedQuantity = Math.round(Number(shipRes.rows[0]?.shipped_quantity || 0) * 1000) / 1000; + + const isPaid = grossTotal > 0 && Math.abs(paidTotal - grossTotal) < 0.01 ? 1 : 0; + const isShipped = orderedQuantity > 0 && Math.abs(shippedQuantity - orderedQuantity) < 0.001 ? 1 : 0; + + await runner.query(isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL, { + companyId, + manifestId: orderId, + orderNumber: num, + orderDate: orderRow.date, + createdAt: orderRow.created_at, + entityId: orderRow.entity_id ?? null, + customerName, + positions, + grossTotal, + paidTotal, + orderedQuantity, + shippedQuantity, + isPaid, + isShipped, + }); +} + +/** + * Full rebuild of all orders in `ordersummary` for a given company. + * + * @param {object} runner + * @param {number} companyId + */ +export async function rebuildOrderSummaries(runner, companyId) { + const isSqlite = runner.dialect === 'sqlite3'; + + const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId }); + const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-'; + + if (isSqlite) { + const sqlRebuildSqlite = ` + WITH + order_articles AS ( + SELECT + manifest_id, + COUNT(*) AS positions, + SUM(amount) AS ordered_quantity, + SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total + FROM manifestarticle + GROUP BY manifest_id + ), + order_payments AS ( + SELECT + manifest_id, + SUM(amount) AS paid_total + FROM payment + GROUP BY manifest_id + ), + shipment_totals AS ( + SELECT + SUBSTR(m_s."number", 1, INSTR(m_s."number", @shipmentPrefix) - 1) AS order_number, + SUM(ma_s.amount) AS shipped_quantity + FROM manifest m_s + JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = @companyId + AND m_s."type" = 'shipment' + AND INSTR(m_s."number", @shipmentPrefix) > 0 + GROUP BY SUBSTR(m_s."number", 1, INSTR(m_s."number", @shipmentPrefix) - 1) + ) + INSERT INTO ordersummary ( + company_id, manifest_id, order_number, order_date, created_at, + entity_id, customer_name, positions, gross_total, paid_total, + ordered_quantity, shipped_quantity, is_paid, is_shipped + ) + SELECT + m.company_id, + m.id AS manifest_id, + m."number" AS order_number, + m."date" AS order_date, + m.created_at, + m.entity_id, + TRIM(COALESCE(e.first_name || ' ', '') || COALESCE(e.name, '')) AS customer_name, + COALESCE(oa.positions, 0) AS positions, + ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total, + ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total, + ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity, + ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity, + CASE WHEN COALESCE(oa.gross_total, 0) > 0 AND ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid, + CASE WHEN COALESCE(oa.ordered_quantity, 0) > 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped + FROM manifest m + LEFT JOIN entity e ON e.id = m.entity_id + LEFT JOIN order_articles oa ON oa.manifest_id = m.id + LEFT JOIN order_payments op ON op.manifest_id = m.id + LEFT JOIN shipment_totals st ON st.order_number = m."number" + WHERE m.company_id = @companyId AND m."type" = 'order' + ON CONFLICT(manifest_id) DO UPDATE SET + order_number = excluded.order_number, + order_date = excluded.order_date, + created_at = excluded.created_at, + entity_id = excluded.entity_id, + customer_name = excluded.customer_name, + positions = excluded.positions, + gross_total = excluded.gross_total, + paid_total = excluded.paid_total, + ordered_quantity = excluded.ordered_quantity, + shipped_quantity = excluded.shipped_quantity, + is_paid = excluded.is_paid, + is_shipped = excluded.is_shipped; + `; + await runner.query(sqlRebuildSqlite, { companyId, shipmentPrefix }); + } else { + await runner.query('DELETE FROM ordersummary WHERE company_id = @companyId', { companyId }); + const sqlRebuildMssql = ` + WITH + order_articles AS ( + SELECT + manifest_id, + COUNT(*) AS positions, + SUM(amount) AS ordered_quantity, + SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total + FROM manifestarticle + GROUP BY manifest_id + ), + order_payments AS ( + SELECT + manifest_id, + SUM(amount) AS paid_total + FROM payment + GROUP BY manifest_id + ), + shipment_totals AS ( + SELECT + SUBSTRING(m_s.[number], 1, CHARINDEX(@shipmentPrefix, m_s.[number]) - 1) AS order_number, + SUM(ma_s.amount) AS shipped_quantity + FROM manifest m_s + JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = @companyId + AND m_s.[type] = 'shipment' + AND CHARINDEX(@shipmentPrefix, m_s.[number]) > 0 + GROUP BY SUBSTRING(m_s.[number], 1, CHARINDEX(@shipmentPrefix, m_s.[number]) - 1) + ) + INSERT INTO ordersummary ( + company_id, manifest_id, order_number, order_date, created_at, + entity_id, customer_name, positions, gross_total, paid_total, + ordered_quantity, shipped_quantity, is_paid, is_shipped + ) + SELECT + m.company_id, + m.id AS manifest_id, + m.[number] AS order_number, + m.[date] AS order_date, + m.created_at, + m.entity_id, + LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, ''))) AS customer_name, + COALESCE(oa.positions, 0) AS positions, + ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total, + ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total, + ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity, + ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity, + CASE WHEN COALESCE(oa.gross_total, 0) > 0 AND ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid, + CASE WHEN COALESCE(oa.ordered_quantity, 0) > 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped + FROM manifest m + LEFT JOIN entity e ON e.id = m.entity_id + LEFT JOIN order_articles oa ON oa.manifest_id = m.id + LEFT JOIN order_payments op ON op.manifest_id = m.id + LEFT JOIN shipment_totals st ON st.order_number = m.[number] + WHERE m.company_id = @companyId AND m.[type] = 'order'; + `; + await runner.query(sqlRebuildMssql, { companyId, shipmentPrefix }); + } +} diff --git a/src/store/order/update.js b/src/store/order/update.js index 08c8b19..212c3e0 100644 --- a/src/store/order/update.js +++ b/src/store/order/update.js @@ -14,6 +14,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import taxEnsure from '../tax/ensure.js'; import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js'; +import { syncOrderSummary } from './syncSummary.js'; import makeRunner from '../runner.js'; export default function update(provider) { @@ -146,6 +147,7 @@ export default function update(provider) { } const doc = await loadDoc(runner, { type: orderType, companyId, id }); + await syncOrderSummary(runner, { companyId, manifestId: id }); return doc; }; diff --git a/src/store/payment/create.js b/src/store/payment/create.js index 2fabcc5..396a9f2 100644 --- a/src/store/payment/create.js +++ b/src/store/payment/create.js @@ -1,6 +1,7 @@ import { queriesFor, requireId, requireText } from '../logistics/helpers.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveManifestAccess from '../manifestattachment/resolveManifestAccess.js'; +import { syncOrderSummary } from '../order/syncSummary.js'; import makeRunner from '../runner.js'; function shape(row) { @@ -64,6 +65,7 @@ export default function create(provider) { type, amount: input.amount, }; + await syncOrderSummary(runner, { companyId, manifestId }); return shape(row); }; diff --git a/src/store/payment/delete.js b/src/store/payment/delete.js index c34b51e..ff68821 100644 --- a/src/store/payment/delete.js +++ b/src/store/payment/delete.js @@ -1,5 +1,6 @@ import { queriesFor, requireId } from '../logistics/helpers.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import { syncOrderSummary } from '../order/syncSummary.js'; import makeRunner from '../runner.js'; /** @@ -21,6 +22,17 @@ export default function del(provider) { const paymentId = requireId(id, 'Payment'); const runner = makeRunner(provider, tx); const { companyId } = await access(googleAuthId, runner); + + const paymentRow = await runner.query( + 'SELECT manifest_id FROM payment WHERE id = @id', + { id: paymentId }, + ); + const manifestId = paymentRow.rows[0]?.manifest_id; + await runner.query(q.PAYMENT_DELETE, { id: paymentId, companyId }); + + if (manifestId) { + await syncOrderSummary(runner, { companyId, manifestId }); + } }; } diff --git a/src/store/shipment/create.js b/src/store/shipment/create.js index 42f082c..86ec6aa 100644 --- a/src/store/shipment/create.js +++ b/src/store/shipment/create.js @@ -3,6 +3,7 @@ import taxEnsure from '../tax/ensure.js'; import { insertLines, parseDate } from '../manifestLines.js'; import makeRunner from '../runner.js'; import { loadShipment, queriesFor } from './helpers.js'; +import { syncOrderSummary } from '../order/syncSummary.js'; function requireId(val, name) { const n = Number(val); @@ -181,6 +182,8 @@ export default function create(provider) { ensureTax, }); + await syncOrderSummary(runner, { companyId, manifestId: orderId }); + return loadShipment(runner, q, companyId, manifestId); }; diff --git a/src/store/shipment/delete.js b/src/store/shipment/delete.js index c3925e4..00e4918 100644 --- a/src/store/shipment/delete.js +++ b/src/store/shipment/delete.js @@ -1,6 +1,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; import { queriesFor } from './helpers.js'; +import { syncOrderSummary } from '../order/syncSummary.js'; function requireId(val, name) { const n = Number(val); @@ -52,9 +53,19 @@ export default function del(provider) { throw new Error('Shipped shipment cannot be deleted'); } + const shipmentNumber = String(check.rows[0].number || ''); + const prefixRes = await runner.query(q.GET_SHIPMENT_PREFIX, { companyId }); + const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-'; + const pIdx = shipmentNumber.indexOf(shipmentPrefix); + const orderNumber = pIdx > 0 ? shipmentNumber.slice(0, pIdx) : null; + await runner.query(q.DELETE_SHIPMENT, { id: shipmentId, companyId, }); + + if (orderNumber) { + await syncOrderSummary(runner, { companyId, orderNumber }); + } }; }