This commit is contained in:
seb
2026-09-24 23:26:25 +02:00
parent 075b1f0ddc
commit f8f61a1212
40 changed files with 1245 additions and 337 deletions

View File

@@ -121,6 +121,9 @@ import manifestattachmentRemove from './store/manifestattachment/remove.js';
import manifestattachmentListByManifestId from './store/manifestattachment/listByManifestId.js';
import manifestattachmentGetById from './store/manifestattachment/getById.js';
import manifestattachmentOrder from './store/manifestattachment/order.js';
import ledgerAdd from './store/ledger/add.js';
import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js';
import ledgerListBySku from './store/ledger/listBySku.js';
export default class LibDashStore {
/**
@@ -331,6 +334,12 @@ export default class LibDashStore {
order: manifestattachmentOrder(provider),
};
this.ledger = {
add: ledgerAdd(provider),
listByManifestarticleId: ledgerListByManifestarticleId(provider),
listBySku: ledgerListBySku(provider),
};
this.purge = storePurge(provider);
this.initialize = storeInitialize(provider);

View File

@@ -0,0 +1,88 @@
export const CHECK_MANIFESTARTICLE_ACCESS = `
SELECT ma.id
FROM manifestarticle ma
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE ma.id = @manifestarticleId AND m.company_id = @companyId;
`;
export const GET_LATEST_ROW = `
SELECT TOP 1
id,
manifestarticle_id,
transaction_type,
amount,
transaction_text,
hash_version,
prev_hash,
created_at
FROM ledger WITH (UPDLOCK, HOLDLOCK)
ORDER BY id DESC;
`;
export const INSERT_ROW = `
INSERT INTO ledger (
manifestarticle_id,
transaction_type,
amount,
transaction_text,
hash_version,
prev_hash
)
OUTPUT
INSERTED.id,
INSERTED.manifestarticle_id,
INSERTED.transaction_type,
INSERTED.amount,
INSERTED.transaction_text,
INSERTED.hash_version,
INSERTED.prev_hash,
INSERTED.created_at
VALUES (
@manifestarticleId,
@transactionType,
@amount,
@transactionText,
@hashVersion,
@prevHash
);
`;
export const LIST_BY_MANIFESTARTICLE = `
SELECT
l.id,
l.manifestarticle_id,
ma.sku,
l.transaction_type,
l.amount,
l.transaction_text,
l.hash_version,
l.prev_hash,
l.created_at
FROM ledger l
INNER JOIN manifestarticle ma ON l.manifestarticle_id = ma.id
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE l.manifestarticle_id = @manifestarticleId
AND m.company_id = @companyId
AND (@transactionType IS NULL OR l.transaction_type = @transactionType)
ORDER BY l.id ASC;
`;
export const LIST_BY_SKU = `
SELECT
l.id,
l.manifestarticle_id,
ma.sku,
l.transaction_type,
l.amount,
l.transaction_text,
l.hash_version,
l.prev_hash,
l.created_at
FROM ledger l
INNER JOIN manifestarticle ma ON l.manifestarticle_id = ma.id
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE ma.sku = @sku
AND m.company_id = @companyId
AND (@transactionType IS NULL OR l.transaction_type = @transactionType)
ORDER BY l.id ASC;
`;

View File

@@ -600,6 +600,7 @@ BEGIN
transaction_text NVARCHAR(MAX) NOT NULL,
hash_version INT NOT NULL,
prev_hash NVARCHAR(128) NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_ledger_created_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_ledger_manifestarticle FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION
) WITH (LEDGER = ON (APPEND_ONLY = ON));
END

View File

@@ -385,6 +385,18 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
this.db.exec(script);
}
}
// Ensure created_at exists on ledger table if created previously
try {
const ledgerCols = this.db.pragma('table_info("ledger")').map((c) => c.name);
if (ledgerCols.length > 0 && !ledgerCols.includes('created_at')) {
this.db.exec(
`ALTER TABLE ledger ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
);
}
} catch {
// ignore
}
}
/**

View File

@@ -0,0 +1,89 @@
export const CHECK_MANIFESTARTICLE_ACCESS = `
SELECT ma.id
FROM manifestarticle ma
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE ma.id = @manifestarticleId AND m.company_id = @companyId;
`;
export const GET_LATEST_ROW = `
SELECT
id,
manifestarticle_id,
transaction_type,
amount,
transaction_text,
hash_version,
prev_hash,
created_at
FROM ledger
ORDER BY id DESC
LIMIT 1;
`;
export const INSERT_ROW = `
INSERT INTO ledger (
manifestarticle_id,
transaction_type,
amount,
transaction_text,
hash_version,
prev_hash
)
VALUES (
@manifestarticleId,
@transactionType,
@amount,
@transactionText,
@hashVersion,
@prevHash
)
RETURNING
id,
manifestarticle_id,
transaction_type,
amount,
transaction_text,
hash_version,
prev_hash,
created_at;
`;
export const LIST_BY_MANIFESTARTICLE = `
SELECT
l.id,
l.manifestarticle_id,
ma.sku,
l.transaction_type,
l.amount,
l.transaction_text,
l.hash_version,
l.prev_hash,
l.created_at
FROM ledger l
INNER JOIN manifestarticle ma ON l.manifestarticle_id = ma.id
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE l.manifestarticle_id = @manifestarticleId
AND m.company_id = @companyId
AND (@transactionType IS NULL OR l.transaction_type = @transactionType)
ORDER BY l.id ASC;
`;
export const LIST_BY_SKU = `
SELECT
l.id,
l.manifestarticle_id,
ma.sku,
l.transaction_type,
l.amount,
l.transaction_text,
l.hash_version,
l.prev_hash,
l.created_at
FROM ledger l
INNER JOIN manifestarticle ma ON l.manifestarticle_id = ma.id
INNER JOIN manifest m ON ma.manifest_id = m.id
WHERE ma.sku = @sku
AND m.company_id = @companyId
AND (@transactionType IS NULL OR l.transaction_type = @transactionType)
ORDER BY l.id ASC;
`;

View File

@@ -432,6 +432,7 @@ CREATE TABLE IF NOT EXISTS ledger (
transaction_text TEXT NOT NULL,
hash_version INTEGER NOT NULL,
prev_hash TEXT,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION
);
`;

View File

@@ -2,6 +2,8 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
import taxEnsure from '../tax/ensure.js';
@@ -20,6 +22,7 @@ export default function create(provider) {
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Assigns a supplier to an article, optionally creating a purchase price.
@@ -82,7 +85,11 @@ export default function create(provider) {
}
const runCreate = async (runner) => {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const { articleId, companyId, companyuserId } = await resolve(
googleAuthId,
sku,
runner,
);
await assertSupplier(runner, companyId, entityId);
@@ -153,7 +160,28 @@ export default function create(provider) {
companyId,
});
return shapeArticleSupplier(loadRes.rows[0]);
const supplierRow = loadRes.rows[0];
const supplierName =
supplierRow?.supplierName ?? (entityId ? `Lieferant #${entityId}` : 'Lieferant');
const details = [];
const actualSku = supplierRow?.sku ?? supplierSku;
if (actualSku) {
details.push(`Artikelnr: "${actualSku}"`);
}
const actualPrice = supplierRow?.priceNet ?? netPrice;
if (actualPrice !== null && actualPrice !== undefined) {
details.push(`EK: ${actualPrice}`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Lieferant "${supplierName}" hinzugefügt${suffix}`,
});
return shapeArticleSupplier(supplierRow);
};
if (tx) {

View File

@@ -1,5 +1,7 @@
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import makeRunner from '../runner.js';
@@ -7,6 +9,7 @@ export default function del(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Deletes a supplier assignment and cleans up its associated purchase price.
@@ -27,7 +30,11 @@ export default function del(provider) {
}
const runDelete = async (runner) => {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const { articleId, companyId, companyuserId } = await resolve(
googleAuthId,
sku,
runner,
);
const existingRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
id,
@@ -51,6 +58,21 @@ export default function del(provider) {
articleId,
});
}
const supplierName =
existing.supplierName ?? (existing.entityId ? `Lieferant #${existing.entityId}` : 'Lieferant');
const details = [];
if (existing.sku) {
details.push(`Artikelnr: "${existing.sku}"`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Lieferant "${supplierName}" entfernt${suffix}`,
});
};
if (tx) {

View File

@@ -2,6 +2,8 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
import taxEnsure from '../tax/ensure.js';
@@ -19,6 +21,7 @@ export default function update(provider) {
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Updates an existing supplier assignment and its associated purchase price.
@@ -76,7 +79,11 @@ export default function update(provider) {
}
const runUpdate = async (runner) => {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const { articleId, companyId, companyuserId } = await resolve(
googleAuthId,
sku,
runner,
);
const existingRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
id,
@@ -161,6 +168,22 @@ export default function update(provider) {
priceId = priceRes.rows[0]?.id ?? priceRes.insertId;
}
}
} else if (priceId != null && (patch.taxId !== undefined || patch.taxRate !== undefined)) {
let taxId = patch.taxId;
if (taxId == null || !Number.isFinite(taxId)) {
if (patch.taxRate != null && Number.isFinite(patch.taxRate)) {
taxId = await ensureTax(patch.taxRate, runner);
} else {
taxId = existing.priceTaxId;
}
}
await runner.query(queries.UPDATE_PRICE, {
id: priceId,
articleId,
net: existing.priceNet,
taxId,
});
}
const nextSku =
@@ -184,6 +207,70 @@ export default function update(provider) {
priceId,
});
const supplierName =
existing.supplierName ?? (existing.entityId ? `Lieferant #${existing.entityId}` : 'Lieferant');
const historyLines = [];
if (patch.sku !== undefined) {
const oldSku = existing.sku ?? '';
const newSku = nextSku ?? '';
if (oldSku !== newSku) {
historyLines.push(
`Lieferant "${supplierName}": Lieferanten-Artikelnr. geändert von "${oldSku}" auf "${newSku}"`,
);
}
}
if (patch.pu !== undefined && Number(patch.pu) !== Number(existing.pu)) {
historyLines.push(
`Lieferant "${supplierName}": VPE geändert von "${existing.pu}" auf "${nextPu}"`,
);
}
if (patch.stock !== undefined) {
const oldStock = existing.stock != null ? String(existing.stock) : '';
const newStock = nextStock != null ? String(nextStock) : '';
if (oldStock !== newStock) {
historyLines.push(
`Lieferant "${supplierName}": Lagerbestand geändert von "${oldStock}" auf "${newStock}"`,
);
}
}
if (patch.netPrice !== undefined) {
if (patch.netPrice === null) {
if (existing.priceId != null && existing.priceNet != null) {
historyLines.push(
`Lieferant "${supplierName}": Einkaufspreis geändert von "${existing.priceNet}" auf ""`,
);
}
} else {
const oldPrice = existing.priceNet != null ? String(existing.priceNet) : '';
const newPrice = String(patch.netPrice);
if (oldPrice !== newPrice) {
historyLines.push(
`Lieferant "${supplierName}": Einkaufspreis geändert von "${oldPrice}" auf "${newPrice}"`,
);
}
}
}
if (patch.taxRate !== undefined && patch.taxRate !== null && existing.priceTaxRate != null) {
if (Number(existing.priceTaxRate) !== Number(patch.taxRate)) {
historyLines.push(
`Lieferant "${supplierName}": MwSt. geändert von "${existing.priceTaxRate}%" auf "${patch.taxRate}%"`,
);
}
}
if (historyLines.length) {
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: historyLines.join('\n'),
});
}
const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
id,
articleId,

148
src/store/ledger/add.js Normal file
View File

@@ -0,0 +1,148 @@
import { createHash } from 'node:crypto';
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
export function shape(row) {
return {
id: row.id,
manifestarticle_id: row.manifestarticle_id,
transaction_type: row.transaction_type,
amount: row.amount,
transaction_text: row.transaction_text,
hash_version: row.hash_version,
prev_hash: row.prev_hash,
created_at:
row.created_at instanceof Date
? row.created_at.toISOString()
: new Date(row.created_at).toISOString(),
};
}
// Computes the SHA-512 hex hash of a previous ledger entry row
// containing all database columns in canonical JSON format.
export function computePrevHash(row) {
if (!row) {
return INIT_HASH;
}
const createdAt =
row.created_at instanceof Date
? row.created_at.toISOString()
: new Date(row.created_at).toISOString();
const payload = JSON.stringify({
id: row.id,
manifestarticle_id: row.manifestarticle_id,
transaction_type: row.transaction_type,
amount: row.amount,
transaction_text: row.transaction_text,
hash_version: row.hash_version,
prev_hash: row.prev_hash,
created_at: createdAt,
});
return createHash('sha512').update(payload, 'utf8').digest('hex');
}
/**
* Appends a new append-only entry to the ledger table.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Ledger entry payload.
* @param {number} input.manifestarticle_id - ID of the referenced manifest line (can also pass manifestarticleId).
* @param {string} input.transaction_type - Transaction type (can also pass transactionType).
* @param {number} input.amount - Movement amount.
* @param {string} input.transaction_text - Transaction memo/details (can also pass transactionText).
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, manifestarticle_id: number, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>} The created ledger row.
* @throws {Error} If user not authorized, company not selected, manifest article not found, or input invalid.
*
* @example
* const entry = await store.ledger.add(googleAuthId, {
* manifestarticle_id: 1,
* transaction_type: 'PAYMENT',
* amount: 100.5,
* transaction_text: 'Invoice paid',
* });
*/
export default function add(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
return async (googleAuthId, input = {}, tx) => {
const rawManifestarticleId =
input.manifestarticle_id ?? input.manifestarticleId;
const rawTransactionType =
input.transaction_type ?? input.transactionType;
const rawAmount = input.amount;
const rawTransactionText =
input.transaction_text ?? input.transactionText;
const manifestarticleId = Number(rawManifestarticleId);
if (!Number.isInteger(manifestarticleId) || manifestarticleId <= 0) {
throw new Error('Invalid ledger entry');
}
const transactionType =
typeof rawTransactionType === 'string'
? rawTransactionType.trim()
: '';
if (!transactionType || transactionType.length > 100) {
throw new Error('Invalid ledger entry');
}
const amount = Number(rawAmount);
if (!Number.isFinite(amount)) {
throw new Error('Invalid ledger entry');
}
if (rawTransactionText == null) {
throw new Error('Invalid ledger entry');
}
const transactionText = String(rawTransactionText);
const doAdd = async (runner) => {
const { companyId } = await access(googleAuthId, runner);
const check = await runner.query(queries.CHECK_MANIFESTARTICLE_ACCESS, {
manifestarticleId,
companyId,
});
if (!check.rows[0]) {
throw new Error('Manifest article not found');
}
const latest = await runner.query(queries.GET_LATEST_ROW);
const prevRow = latest.rows[0];
const prevHash = computePrevHash(prevRow);
const inserted = await runner.query(queries.INSERT_ROW, {
manifestarticleId,
transactionType,
amount,
transactionText,
hashVersion: 1,
prevHash,
});
const row = inserted.rows[0];
if (!row) {
throw new Error('Failed to insert ledger entry');
}
return shape(row);
};
if (tx) {
return doAdd(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(doAdd);
}
return doAdd(makeRunner(provider));
};
}

View File

@@ -0,0 +1,85 @@
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
manifestarticle_id: row.manifestarticle_id,
sku: row.sku ?? null,
transaction_type: row.transaction_type,
amount: row.amount,
transaction_text: row.transaction_text,
hash_version: row.hash_version,
prev_hash: row.prev_hash,
created_at:
row.created_at instanceof Date
? row.created_at.toISOString()
: new Date(row.created_at).toISOString(),
};
}
/**
* Lists ledger entries for a specific manifest article line, optionally filtered by transaction type.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} manifestarticleId - The id of the manifest article line.
* @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string }.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, manifestarticle_id: number, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>}
* @throws {Error} If user not authorized, company not selected, or manifest article not found.
*
* @example
* const entries = await store.ledger.listByManifestarticleId(googleAuthId, 42, 'PAYMENT');
* // => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
*/
export default function listByManifestarticleId(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
return async (googleAuthId, manifestarticleId, optionsOrType, tx) => {
let transactionType = null;
let explicitTx = tx;
if (typeof optionsOrType === 'string') {
transactionType = optionsOrType.trim() || null;
} else if (typeof optionsOrType === 'object' && optionsOrType !== null) {
if (typeof optionsOrType.query === 'function') {
explicitTx = optionsOrType;
} else {
const rawType =
optionsOrType.transaction_type ?? optionsOrType.transactionType;
if (typeof rawType === 'string') {
transactionType = rawType.trim() || null;
}
}
}
const id = Number(manifestarticleId);
if (!Number.isInteger(id) || id <= 0) {
throw new Error('Invalid manifest article id');
}
const runner = makeRunner(provider, explicitTx);
const { companyId } = await access(googleAuthId, runner);
const check = await runner.query(queries.CHECK_MANIFESTARTICLE_ACCESS, {
manifestarticleId: id,
companyId,
});
if (!check.rows[0]) {
throw new Error('Manifest article not found');
}
const res = await runner.query(queries.LIST_BY_MANIFESTARTICLE, {
manifestarticleId: id,
companyId,
transactionType: transactionType || null,
});
return res.rows.map(shape);
};
}

View File

@@ -0,0 +1,76 @@
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
manifestarticle_id: row.manifestarticle_id,
sku: row.sku ?? null,
transaction_type: row.transaction_type,
amount: row.amount,
transaction_text: row.transaction_text,
hash_version: row.hash_version,
prev_hash: row.prev_hash,
created_at:
row.created_at instanceof Date
? row.created_at.toISOString()
: new Date(row.created_at).toISOString(),
};
}
/**
* Lists ledger entries for a given article SKU, optionally filtered by transaction type.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string }.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, manifestarticle_id: number, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>}
* @throws {Error} If user not authorized, company not selected, or SKU invalid.
*
* @example
* const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY');
* // => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
*/
export default function listBySku(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
return async (googleAuthId, sku, optionsOrType, tx) => {
let transactionType = null;
let explicitTx = tx;
if (typeof optionsOrType === 'string') {
transactionType = optionsOrType.trim() || null;
} else if (typeof optionsOrType === 'object' && optionsOrType !== null) {
if (typeof optionsOrType.query === 'function') {
explicitTx = optionsOrType;
} else {
const rawType =
optionsOrType.transaction_type ?? optionsOrType.transactionType;
if (typeof rawType === 'string') {
transactionType = rawType.trim() || null;
}
}
}
const trimmedSku = typeof sku === 'string' ? sku.trim() : '';
if (!trimmedSku) {
throw new Error('Invalid SKU');
}
const runner = makeRunner(provider, explicitTx);
const { companyId } = await access(googleAuthId, runner);
const res = await runner.query(queries.LIST_BY_SKU, {
sku: trimmedSku,
companyId,
transactionType: transactionType || null,
});
return res.rows.map(shape);
};
}