Files
daily_console_web/src/features/store-admin/offlineSales/salesSheet.test.ts
2026-08-25 18:00:16 +05:30

164 lines
6.6 KiB
TypeScript

import { strict as assert } from 'node:assert';
import { test } from 'node:test';
import * as XLSX from 'xlsx';
import type { SaleTemplate } from '@/api/offlineSales';
import {
buildTemplateWorkbook,
parseSalesWorkbook,
summarise,
templateFilename,
toBills,
} from './salesSheet';
const TEMPLATE: SaleTemplate = {
tenantid: 1135,
locationid: 0,
locations: [
{ locationid: 1, locationname: 'R mart — RS Puram', productcount: 2 },
{ locationid: 2, locationname: 'R mart — Peelamedu', productcount: 1 },
],
products: [
{ tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', productid: 9001, productname: 'Aachi Chilli Powder 100gm', currentstock: 24, price: 35, taxpercent: 5 },
{ tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', productid: 9004, productname: 'Amul Butter 500g', currentstock: 2, price: 265, taxpercent: 12 },
{ tenantid: 1135, locationid: 2, locationname: 'R mart — Peelamedu', productid: 9007, productname: 'Nestle Milkmaid 400g', currentstock: 40, price: 160, taxpercent: 12 },
],
};
/** Round-trip helper: build the workbook, fill in cells, hand back a buffer. */
function filled(edits: Record<number, Record<string, string | number>>): ArrayBuffer {
const workbook = buildTemplateWorkbook(TEMPLATE);
const sheet = workbook.Sheets['Sales'] as XLSX.WorkSheet;
const header = XLSX.utils.sheet_to_json<unknown[]>(sheet, { header: 1 })[0] as string[];
const column = (name: string) =>
header.findIndex((cell) => String(cell).toLowerCase().startsWith(name));
for (const [row, values] of Object.entries(edits)) {
for (const [name, value] of Object.entries(values)) {
const address = XLSX.utils.encode_cell({ r: Number(row) - 1, c: column(name) });
sheet[address] = typeof value === 'number' ? { t: 'n', v: value } : { t: 's', v: value };
}
}
const out = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }) as ArrayBuffer;
return out;
}
test('the workbook carries every product, ids filled in', () => {
const workbook = buildTemplateWorkbook(TEMPLATE);
assert.deepEqual(workbook.SheetNames, ['Sales', 'Store Info', 'Instructions']);
const rows = XLSX.utils.sheet_to_json<unknown[]>(workbook.Sheets['Sales'] as XLSX.WorkSheet, {
header: 1,
});
assert.equal(rows.length, 4, 'header plus three products');
assert.equal((rows[1] as unknown[])[3], 9001, 'productid is pre-filled');
assert.equal((rows[1] as unknown[])[6], '', 'qtysold is left empty, never defaulted to 0');
});
test('the filename says what the workbook spans', () => {
assert.equal(templateFilename(TEMPLATE, '2026-08-25'), 'counter-sales-all-stores-2026-08-25.xlsx');
assert.equal(
templateFilename({ ...TEMPLATE, locations: [TEMPLATE.locations[0] as never] }, '2026-08-25'),
'counter-sales-r-mart-rs-puram-2026-08-25.xlsx',
);
});
test('rows with no quantity are skipped, not reported as problems', () => {
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 3 } }), { tenantid: 1135 });
assert.deepEqual(parsed.fatal, []);
assert.equal(parsed.rows.length, 1);
assert.equal(parsed.skipped, 2);
assert.equal(parsed.rows[0]?.productid, 9001);
});
test('a file from another account is refused whole', () => {
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1 } }), { tenantid: 9999 });
assert.ok(parsed.fatal[0]?.includes('different account'));
});
test('selling more than the shelf holds is an error on that row', () => {
const parsed = parseSalesWorkbook(filled({ 3: { qtysold: 5 } }), { tenantid: 1135 });
assert.equal(parsed.rows.length, 1);
assert.ok(parsed.rows[0]?.errors.some((problem) => problem.includes('only 2 in stock')));
});
test('a pinned upload refuses rows belonging to another branch', () => {
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1 }, 4: { qtysold: 1 } }), {
tenantid: 1135,
locationid: 1,
});
const foreign = parsed.rows.find((row) => row.locationid === 2);
assert.ok(foreign, 'the row is still parsed, so the operator can see it');
assert.ok(foreign?.errors.some((problem) => problem.includes('another store')));
const own = parsed.rows.find((row) => row.locationid === 1);
assert.deepEqual(own?.errors, []);
});
test('an unpriced line is a warning, not a blocker', () => {
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1, unitprice: 0 } }), {
tenantid: 1135,
});
assert.deepEqual(parsed.rows[0]?.errors, []);
assert.ok(parsed.rows[0]?.warnings.some((note) => note.includes('no price')));
});
test('an unknown payment mode is caught before upload', () => {
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1, paymentmode: 'cheque' } }), {
tenantid: 1135,
});
assert.ok(parsed.rows[0]?.errors.some((problem) => problem.includes('not Cash, Card or UPI')));
});
test('the same bill number at two branches stays two bills', () => {
const parsed = parseSalesWorkbook(
filled({ 2: { qtysold: 1, billno: '1' }, 4: { qtysold: 1, billno: '1' } }),
{ tenantid: 1135 },
);
const bills = toBills(parsed.rows);
assert.equal(bills.length, 2);
assert.deepEqual(
bills.map((bill) => bill.locationid).sort(),
[1, 2],
);
});
test('rows sharing a bill number become one bill with its lines', () => {
const parsed = parseSalesWorkbook(
filled({ 2: { qtysold: 2, billno: 'A-7' }, 3: { qtysold: 1, billno: 'A-7' } }),
{ tenantid: 1135 },
);
const bills = toBills(parsed.rows);
assert.equal(bills.length, 1);
assert.equal(bills[0]?.items.length, 2);
assert.equal(bills[0]?.billno, 'A-7');
});
test('bill-level fields are taken from whichever row carries them', () => {
const parsed = parseSalesWorkbook(
filled({
2: { qtysold: 1, billno: 'B-2' },
3: { qtysold: 1, billno: 'B-2', paymentmode: 'UPI', customername: 'Kavitha' },
}),
{ tenantid: 1135 },
);
const bill = toBills(parsed.rows)[0];
assert.equal(bill?.paymentmode, 'UPI');
assert.equal(bill?.customername, 'Kavitha');
});
test('the preview total is unit price by quantity, less discount', () => {
const parsed = parseSalesWorkbook(
filled({ 2: { qtysold: 4, discountamount: 20 }, 4: { qtysold: 2 } }),
{ tenantid: 1135 },
);
const totals = summarise(parsed.rows);
assert.equal(totals.lines, 2);
assert.equal(totals.units, 6);
assert.equal(totals.amount, 35 * 4 - 20 + 160 * 2);
assert.equal(totals.stores, 2);
assert.equal(totals.bills, 2, 'no bill number means one bill per branch');
});
test('a file with nothing filled in says so rather than uploading nothing', () => {
const parsed = parseSalesWorkbook(filled({}), { tenantid: 1135 });
assert.ok(parsed.fatal[0]?.includes('No sold quantities'));
});