This commit is contained in:
seb
2026-10-13 01:30:11 +02:00
parent 4b2ca7ce0e
commit 2ef2c0edbf
4 changed files with 416 additions and 25 deletions

View File

@@ -1,17 +1,13 @@
import {
COUNT_MANIFESTS as MSSQL_COUNT,
GET_MANIFEST_LINES_BY_MANIFEST_IDS as MSSQL_LINES_BY_MANIFEST_IDS,
LIST_MANIFESTS as MSSQL_LIST,
LIST_MANIFESTS_PAGED as MSSQL_LIST_PAGED,
ORDER_TYPE as MSSQL_ORDER_TYPE,
} from '../../providers/mssql/queries/manifestDoc.js';
import {
COUNT_MANIFESTS as SQLITE_COUNT,
GET_MANIFEST_LINES_BY_MANIFEST_IDS as SQLITE_LINES_BY_MANIFEST_IDS,
LIST_MANIFESTS as SQLITE_LIST,
LIST_MANIFESTS_PAGED as SQLITE_LIST_PAGED,
ORDER_TYPE as SQLITE_ORDER_TYPE,
} from '../../providers/sqlite3/queries/manifestDoc.js';
import { GET_SHIPMENT_PREFIX as MSSQL_GET_SHIPMENT_PREFIX } from '../../providers/mssql/queries/shipment.js';
import { GET_SHIPMENT_PREFIX as SQLITE_GET_SHIPMENT_PREFIX } from '../../providers/sqlite3/queries/shipment.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import { shapeSummary } from '../manifestDoc/helpers.js';
import { shapeLine } from '../manifestLines.js';
@@ -20,27 +16,31 @@ import makeRunner from '../runner.js';
export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_LIST : MSSQL_LIST;
const sqlCount = isSqlite ? SQLITE_COUNT : MSSQL_COUNT;
const sqlPaged = isSqlite ? SQLITE_LIST_PAGED : MSSQL_LIST_PAGED;
const sqlLinesByManifestIds = isSqlite
? SQLITE_LINES_BY_MANIFEST_IDS
: MSSQL_LINES_BY_MANIFEST_IDS;
const sqlShipmentPrefix = isSqlite
? SQLITE_GET_SHIPMENT_PREFIX
: MSSQL_GET_SHIPMENT_PREFIX;
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
/**
* Lists the orders of the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [opts] - Optional pagination options.
* @param {object} [opts] - Optional pagination and filter options.
* @param {number} [opts.offset=0] - Number of orders to skip.
* @param {number} [opts.limit=20] - Number of orders to return.
* @param {number} [opts.limit=50] - Number of orders to return.
* @param {string} [opts.search] - Free-text search matching order number, external number, or customer name.
* @param {boolean} [opts.onlyUnpaid=false] - When true, returns only unpaid orders.
* @param {boolean} [opts.onlyUndelivered=false] - When true, returns only undelivered orders.
* @param {number} [opts.seekMonths] - Negative integer for cutoff months ago (e.g. -1).
* @param {object} [tx] - Optional transaction.
* @returns {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<object>>} The orders.
*
* @example
* const result = await store.order.list(42, { offset: 0, limit: 20 });
* // => { orders: [...], total: 100, offset: 0, limit: 20 }
* const result = await store.order.list(42, { offset: 0, limit: 50, search: '1002', onlyUnpaid: true, onlyUndelivered: true, seekMonths: -1 });
* // => { orders: [...], total: 10, offset: 0, limit: 50 }
*
* const orders = await store.order.list(42);
* // => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }]
@@ -51,11 +51,20 @@ export default function list(provider) {
let isPaged = false;
let resolvedTx = tx;
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
opts.offset !== undefined ||
opts.limit !== undefined ||
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
opts.onlyUnpaid === true ||
opts.onlyUndelivered === true ||
opts.seekMonths !== undefined
);
if (opts != null && typeof opts === 'object' && !opts._isTx) {
if (opts.offset !== undefined || opts.limit !== undefined) {
if (hasFilter || opts.offset !== undefined || opts.limit !== undefined) {
isPaged = true;
const parsedOffset = Number(opts.offset ?? 0);
const parsedLimit = Number(opts.limit ?? 20);
const parsedLimit = Number(opts.limit ?? 50);
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
throw new Error('Invalid offset');
}
@@ -72,18 +81,193 @@ export default function list(provider) {
const runner = makeRunner(provider, resolvedTx);
const { companyId } = await access(googleAuthId, runner);
const conditions = ['m.company_id = @companyId', `m.${isSqlite ? '"type"' : '[type]'} = @type`];
const params = {
companyId,
type: orderType,
};
// Date cutoff via seekMonths (strictly integer)
if (opts?.seekMonths !== undefined) {
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
throw new Error('Invalid seekMonths');
}
const d = new Date();
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
params.dateTo = d.toISOString();
conditions.push(`m.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
}
// Free-text search (strictly string)
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
params.searchPattern = `%${opts.search.trim()}%`;
if (isSqlite) {
conditions.push(`(
m."number" LIKE @searchPattern
OR m.external_number LIKE @searchPattern
OR e.name LIKE @searchPattern
OR e.first_name LIKE @searchPattern
OR (IFNULL(e.first_name, '') || ' ' || IFNULL(e.name, '')) LIKE @searchPattern
OR (IFNULL(e.name, '') || ' ' || IFNULL(e.first_name, '')) LIKE @searchPattern
)`);
} else {
conditions.push(`(
m.[number] LIKE @searchPattern
OR m.external_number LIKE @searchPattern
OR e.name LIKE @searchPattern
OR e.first_name LIKE @searchPattern
OR (ISNULL(e.first_name, '') + ' ' + ISNULL(e.name, '')) LIKE @searchPattern
OR (ISNULL(e.name, '') + ' ' + ISNULL(e.first_name, '')) LIKE @searchPattern
)`);
}
}
// 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
)`);
}
// 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')
)
)`);
}
}
const whereClause = conditions.join('\n AND ');
const sqlCount = `
SELECT COUNT(*) AS total
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
`;
let sqlList;
if (isSqlite) {
sqlList = `
SELECT
m.id,
m."number",
m."type",
m.entity_id AS entityId,
m.external_number AS externalNumber,
m."date",
m.created_at AS createdAt
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
ORDER BY m."date" DESC, m.id DESC
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
`;
} else {
sqlList = `
SELECT
m.id,
m.[number],
m.[type],
m.entity_id AS entityId,
m.external_number AS externalNumber,
m.[date],
m.created_at AS createdAt
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
ORDER BY m.[date] DESC, m.id DESC
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
`;
}
let manifestRows;
let total = 0;
if (isPaged) {
params.offset = offset;
params.limit = limit;
const [countResult, listResult] = await Promise.all([
runner.query(sqlCount, { companyId, type: orderType }),
runner.query(sqlPaged, { companyId, type: orderType, offset, limit }),
runner.query(sqlCount, params),
runner.query(sqlList, params),
]);
total = Number(countResult.rows[0]?.total ?? 0);
manifestRows = listResult.rows;
} else {
const result = await runner.query(sql, { companyId, type: orderType });
const result = await runner.query(sqlList, params);
manifestRows = result.rows;
total = manifestRows.length;
}
@@ -122,5 +306,3 @@ export default function list(provider) {
return orders;
};
}