import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; /** * Lists ledger entries for the user's selected company across all articles, * supporting pagination, date cutoff via seekMonths, and search. * * @param {import('../../providers/provider.js').Provider} provider * @returns {(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>} * * @example * const res = await store.ledger.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'kjh', seekMonths: -1 }); * // => { ledger: [...], total, unfilteredTotal, offset, limit } */ export default function listHistory(provider) { const access = resolveSelectedCompanyAccess(provider); const isSqlite = provider?.dialect === 'sqlite3'; return async (googleAuthId, opts, tx) => { let offset = 0; let limit = 50; let isPaged = true; let resolvedTx = tx; if (opts != null && typeof opts === 'object' && !opts._isTx) { if (opts.offset !== undefined) { const parsedOffset = Number(opts.offset); if (!Number.isInteger(parsedOffset) || parsedOffset < 0) { throw new Error('Invalid offset'); } offset = parsedOffset; } if (opts.limit !== undefined) { const parsedLimit = Number(opts.limit); if (!Number.isInteger(parsedLimit) || parsedLimit < 1) { throw new Error('Invalid limit'); } limit = parsedLimit; } } else if (opts != null) { resolvedTx = opts; } const runner = makeRunner(provider, resolvedTx); const { companyId, languageId } = await access(googleAuthId, runner); const conditions = ['m.company_id = @companyId']; const params = { companyId, languageId, }; // 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(`l.created_at <= @dateTo`); } // Free-text search if (typeof opts?.search === 'string' && opts.search.trim() !== '') { params.searchPattern = `%${opts.search.trim()}%`; const numCol = isSqlite ? 'm."number"' : 'm.[number]'; conditions.push(`( ma.sku LIKE @searchPattern OR ma.name LIKE @searchPattern OR at.name LIKE @searchPattern OR ${numCol} LIKE @searchPattern OR m.external_number LIKE @searchPattern OR l.transaction_type LIKE @searchPattern OR l.transaction_text LIKE @searchPattern )`); } const whereClause = conditions.join('\n AND '); const sqlCount = ` SELECT COUNT(*) AS total FROM ledger l INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifest m ON m.id = ma.manifest_id LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId WHERE ${whereClause} `; const sqlCountBase = ` SELECT COUNT(*) AS total FROM ledger l INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifest m ON m.id = ma.manifest_id WHERE m.company_id = @companyId `; let sqlList; if (isSqlite) { sqlList = ` SELECT l.id, l.created_at AS "date", ma.sku, COALESCE(at.name, ma.name) AS name, l.transaction_type, l.transaction_text, l.amount FROM ledger l INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifest m ON m.id = ma.manifest_id LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId WHERE ${whereClause} ORDER BY l.created_at DESC, l.id DESC ${isPaged ? 'LIMIT @limit OFFSET @offset' : ''} `; } else { sqlList = ` SELECT l.id, l.created_at AS [date], ma.sku, COALESCE(at.name, ma.name) AS name, l.transaction_type, l.transaction_text, l.amount FROM ledger l INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifest m ON m.id = ma.manifest_id LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId WHERE ${whereClause} ORDER BY l.created_at DESC, l.id DESC ${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''} `; } const hasUserFilters = ( opts?.seekMonths !== undefined || (typeof opts?.search === 'string' && opts.search.trim() !== '') ); params.offset = offset; params.limit = limit; const [countResult, listResult, unfilteredCountResult] = await Promise.all([ runner.query(sqlCount, params), runner.query(sqlList, params), hasUserFilters ? runner.query(sqlCountBase, { companyId }) : null, ]); const total = Number(countResult.rows[0]?.total ?? 0); const unfilteredTotal = hasUserFilters ? Number(unfilteredCountResult?.rows[0]?.total ?? 0) : total; return { ledger: listResult.rows.map((row) => ({ id: row.id, date: row.date instanceof Date ? row.date.toISOString() : row.date ? new Date(row.date).toISOString() : null, sku: row.sku ?? null, name: row.name ?? null, transaction_type: row.transaction_type, transaction_text: row.transaction_text, amount: row.amount, })), total, unfilteredTotal, offset, limit, }; }; }