refactor: added new pattern to order parser
All checks were successful
deploy / test (push) Successful in 1m51s
deploy / deploy (push) Successful in 20s

This commit is contained in:
Aswin B. S 2026-11-05 10:06:29 +05:30
parent 723379321d
commit e76e931c3c
12 changed files with 67 additions and 30 deletions

View File

@ -188,6 +188,26 @@ describe('6.4d double dash and plus as a quantity (owner, 5 Nov 2026)', () => {
});
});
describe('6.4f slash as a quantity (owner, 5 Nov 2026)', () => {
it('reads / before 1-3 digits after an ending, in every spacing', () => {
for (const text of ['1991/2', '1991 / 2', '1991 /2', '1991/ 2']) expect(read(text).lines).toEqual(['1991x2']);
expect(read('1991/50 1991/999').lines).toEqual(['1991x50', '1991x999']);
});
it('4 digits after it, a zero or a second slash stays unreadable', () => {
expect(read('0888/0057').unreadable).toEqual(['0888/0057:date_or_time']);
expect(read('1991/0').unreadable).toEqual(['1991/0:zero_quantity']);
expect(read('2026/11/04').unreadable).toEqual(['2026/11/04:date_or_time']);
expect(read('1991/').lines).toEqual([]);
});
it('a date that does not start with 4 digits is unchanged', () => {
expect(read('12/10/2026').unreadable).toEqual(['12/10/2026:date_or_time']);
expect(read('0888 12/10')).toMatchObject({ lines: ['0888x1'], unreadable: ['12/10:date_or_time'] });
});
it('known limit: a year and month (2026/11) reads as ending 2026, quantity 11', () => {
expect(read('2026/11').lines).toEqual(['2026x11']);
});
});
describe('6.4e colon as a quantity (owner, 5 Nov 2026)', () => {
it('reads : before 1-3 digits after an ending, in every spacing', () => {
for (const text of ['1123:5', '1123 : 5', '1123 :5', '1123: 5']) expect(read(text).lines).toEqual(['1123x5']);

View File

@ -23,7 +23,7 @@ function generate(seed: number): { text: string; expected: Line[] } {
}
const ending = pad4(r.int(10000));
const qty = r.int(3) === 0 ? 1 : 1 + r.int(150);
const form = r.pick(['bare', 'x', ' x ', '*', '=', ' = ', '_', '-', '--', ' -- ', '+', ' + ', ':', ' : ', '()', ' ()', 'space'] as const);
const form = r.pick(['bare', 'x', ' x ', '*', '=', ' = ', '_', '-', '--', ' -- ', '+', ' + ', ':', ' : ', '/', ' / ', '()', ' ()', 'space'] as const);
const q = String(qty);
const rendered =
form === 'bare' ? (qty === 1 ? ending : `${ending}x${q}`)

View File

@ -116,6 +116,11 @@ export function scanLine(body: string, atoms: Atom[], line: number): ScanItem[]
if (isOp(a, '.') && !isNoiseDot(k) && b !== undefined && b.kind === 'num') return withQty(i, b.text, k + 2);
if (isOp(a, '/') && b !== undefined && b.kind === 'num') {
if (b.text.length > 3) return { ok: false, reason: 'date_or_time', next: k + 2 };
return withQty(i, b.text, k + 2);
}
if (a !== undefined && a.kind === 'op' && QTY_OPS.has(a.text)) {
if (b !== undefined && b.kind === 'num') return withQty(i, b.text, k + 2);
if (a.glued) return { ok: false, reason: 'mixed_characters', next: k + 1 };

View File

@ -187,7 +187,7 @@ describe('POST /ingest: replies', () => {
await say('m1', f.shopUser.phone, '8910=3');
expect(sent).toEqual([]);
await say('m2', f.shopUser.phone, '?');
expect(sent).toEqual([{ chatId: chat, message: '✅ *Got it*\n1 line, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel' }]);
expect(sent).toEqual([{ chatId: chat, message: '✅ *Got it*\n1 number, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel' }]);
});
it('plays the whole dialog through /ingest and saves the order', async () => {
@ -252,7 +252,7 @@ describe('POST /ingest: replies', () => {
await say('m2', f.shopUser.phone, '?');
sendFails = false;
await say('m3', f.shopUser.phone, '?');
expect(sent.map((s) => s.message)).toEqual(['✅ *Got it*\n1 line, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel']);
expect(sent.map((s) => s.message)).toEqual(['✅ *Got it*\n1 number, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel']);
});
it('does not wait for the bridge before answering', async () => {

View File

@ -458,7 +458,7 @@ describe('photos sent together', () => {
expect(sent).toHaveLength(2);
expect(sent[0]).toContain('Read from your 2 photos');
expect(sent[1]).toContain('*Total:* 3 lines, 10 tickets');
expect(sent[1]).toContain('*Total:* 3 numbers, 10 tickets');
expect(getConversation(db, f.shopUser.id)?.state).toBe('CONFIRM');
});

View File

@ -58,7 +58,7 @@ describe('the walk-through from the plan (section 3.3)', () => {
'',
'*Shop:* Shop B',
'*Draw:* TODAY 3 Nov',
'*Total:* 2 lines, 4 tickets',
'*Total:* 2 numbers, 4 tickets',
'',
'*Tickets*',
'8910 - 3',
@ -79,7 +79,7 @@ describe('the walk-through from the plan (section 3.3)', () => {
'',
'*Shop:* Shop B',
'*Draw:* TODAY 3 Nov',
'*Total:* 3 lines, 6 tickets',
'*Total:* 3 numbers, 6 tickets',
'',
'*Tickets*',
'8910 - 3',
@ -208,13 +208,13 @@ describe('collecting', () => {
const say = chatAs(f.shopUser.id);
say('8910=3');
const [summary] = say('7810=1\ndone');
expect(summary).toContain('2 lines, 4 tickets');
expect(summary).toContain('2 numbers, 4 tickets');
});
it('a whole order with done in the very first message needs no menu', () => {
const say = chatAs(f.shopUser.id);
const [summary] = say('0888\n0057\ndone');
expect(summary).toContain('2 lines, 2 tickets');
expect(summary).toContain('2 numbers, 2 tickets');
expect(state(f.shopUser.id)).toBe('CONFIRM');
});
@ -222,7 +222,7 @@ describe('collecting', () => {
const say = chatAs(f.shopUser.id);
say('8910=3');
const [summary] = say('0888=2\n7810=1\n1');
expect(summary).toContain('3 lines, 6 tickets');
expect(summary).toContain('3 numbers, 6 tickets');
expect(summary).toContain('✅ Everything was read');
expect(state(f.shopUser.id)).toBe('CONFIRM');
});
@ -303,7 +303,7 @@ describe('unreadable text', () => {
expect(again).toContain('⚠️ *Not read, not in the order:*\n 0x38=1');
expect(again).toContain('⚠️ *Left out earlier, not in the order:*\n 08?8=2');
// the new line is unreadable too, so nothing was added to the order
expect(again).toContain('1 line, 3 tickets');
expect(again).toContain('1 number, 3 ticket');
expect(again).not.toContain('Everything was read');
});
@ -313,7 +313,7 @@ describe('unreadable text', () => {
say('done');
say('2');
const [again] = say('1234=1'); // a forgotten line, not a correction
expect(again).toContain('2 lines, 4 tickets');
expect(again).toContain('2 numbers, 4 tickets');
expect(again).toContain('⚠️ *Left out earlier, not in the order:*\n 08?8=2');
expect(again).not.toContain('Everything was read');
say('1');
@ -329,7 +329,7 @@ describe('unreadable text', () => {
say('done');
say('2');
const [again] = say('1234=1');
expect(again).toContain('*Total:* 2 lines, 4 tickets\n✅ Everything was read');
expect(again).toContain('*Total:* 2 numbers, 4 tickets\n✅ Everything was read');
expect(again).not.toContain('Left out earlier');
});
@ -363,7 +363,7 @@ describe('repeated endings', () => {
const say = chatAs(f.shopUser.id);
say('8910=3\n7810=1\n8910=3');
const [summary] = say('done');
expect(summary).toContain('3 lines, 7 tickets');
expect(summary).toContain('3 numbers, 7 tickets');
expect(summary).toContain('8910 - 6 (2 times)');
expect(summary).toContain('7810 - 1');
expect(state(f.shopUser.id)).toBe('CONFIRM');
@ -399,7 +399,7 @@ describe('the menus', () => {
say('done');
expect(say('2')).toEqual([msg.addPrompt()]);
const [summary] = say('7810=1');
expect(summary).toContain('2 lines, 4 tickets');
expect(summary).toContain('2 numbers, 4 tickets');
});
it('1 and 3 keep their meaning while an Add more is pending', () => {
@ -426,7 +426,7 @@ describe('the menus', () => {
const [reply] = say('7810=1');
expect(reply).toContain('*Order summary*');
expect(reply).toContain('*Total:* 2 lines, 4 tickets');
expect(reply).toContain('*Total:* 2 numbers, 4 tickets');
expect(reply).toContain('7810 - 1');
expect(reply).toContain(SUMMARY_MENU);
expect(state(f.shopUser.id)).toBe('CONFIRM');
@ -444,7 +444,7 @@ describe('the menus', () => {
say('8910=3');
say('done');
expect(say('2')).toEqual([msg.addPrompt()]);
expect(say('7810=1')[0]).toContain('*Total:* 2 lines, 4 tickets');
expect(say('7810=1')[0]).toContain('*Total:* 2 numbers, 4 tickets');
expect(state(f.shopUser.id)).toBe('CONFIRM');
});

View File

@ -225,7 +225,7 @@ describe('a readable photo', () => {
const replies = photo(id, '9001 2');
expect(replies[0]).toBe(msg.photoAdded('9001 2'));
expect(replies[1]).toContain('*Total:* 2 lines, 7 tickets');
expect(replies[1]).toContain('*Total:* 2 numbers, 7 tickets');
expect(conv(id)?.state).toBe('CONFIRM');
expect(orders()).toEqual([]);
@ -322,7 +322,7 @@ describe('a photo with no ticket lines', () => {
const [reply] = photo(id, slip);
expect(reply).toBe(msg.photoNoTickets(slip, notRead, msg.photoOpenOrder(1, 5)));
expect(reply).not.toContain('Got it');
expect(reply).toContain('1 line, 5 tickets so far.');
expect(reply).toContain('1 number, 5 tickets so far.');
expect(conv(id)).toMatchObject({ state: 'COLLECTING', buffer_text: '6379 5', source: 'whatsapp_text' });
});

View File

@ -56,7 +56,7 @@ describe('summary', () => {
'',
'*Shop:* Shop B',
'*Draw:* TODAY 3 Nov',
'*Total:* 2 lines, 4 tickets',
'*Total:* 2 numbers, 4 tickets',
'',
'*Tickets*',
'8910 - 3',
@ -84,7 +84,7 @@ describe('summary', () => {
'',
'*Shop:* Shop B',
'*Draw:* TODAY 3 Nov',
'*Total:* 3 lines, 6 tickets',
'*Total:* 3 numbers, 6 tickets',
'✅ Everything was read',
'',
'*Tickets*',
@ -105,7 +105,7 @@ describe('summary', () => {
'',
'*Shop:* Shop B',
'*Draw:* TODAY 3 Nov',
'*Total:* 3 lines, 6 tickets',
'*Total:* 3 numbers, 6 tickets',
'',
'*Tickets*',
'8910 - 3',
@ -146,7 +146,7 @@ describe('summary', () => {
entries: [{ ending: '0001', qty: 1, times: 1 }],
unreadable: [],
});
expect(t).toContain('*Total:* 1 line, 1 ticket\n✅ Everything was read');
expect(t).toContain('*Total:* 1 number, 1 ticket\n✅ Everything was read');
});
it('caps a long unreadable list and counts the rest', () => {
@ -165,7 +165,7 @@ describe('summary', () => {
describe('prompts', () => {
it('the collecting reply gives the totals and offers Done and Cancel', () => {
expect(m.progressUpdate(1, 3)).toBe('✅ *Got it*\n1 line, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel');
expect(m.progressUpdate(1, 3)).toBe('✅ *Got it*\n1 number, 3 tickets so far.\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel');
});
it('shop list is numbered in the order given', () => {
expect(m.shopList(['Shop A', 'Shop B'])).toBe('*Which shop?*\n\n1. Shop A\n2. Shop B');
@ -184,7 +184,7 @@ describe('prompts', () => {
);
});
it('reminder shows progress when something was read', () => {
expect(m.reminder(5, 12)).toBe('⏳ *Unfinished order*\n5 lines, 12 tickets so far.\n\n*Reply:*\n1. Done\n2. Cancel');
expect(m.reminder(5, 12)).toBe('⏳ *Unfinished order*\n5 numbers, 12 tickets so far.\n\n*Reply:*\n1. Done\n2. Cancel');
});
it('reminder omits progress when nothing was read', () => {
expect(m.reminder(0, 0)).toBe('⏳ *Unfinished order*\n\n*Reply:*\n1. Done\n2. Cancel');
@ -193,7 +193,7 @@ describe('prompts', () => {
expect(m.splitQuestion(5, 12)).toBe(
[
'⏳ *Unfinished order*',
'5 lines, 12 tickets so far.',
'5 numbers, 12 tickets so far.',
'',
'*Is this message:*',
'1. Part of that order',

View File

@ -43,7 +43,7 @@ export const SUMMARY_MENU = '*Reply:*\n1. Save\n2. Add more\n3. Cancel';
const FINISH_PROMPT = `*When the order is finished, reply:*\n${COLLECTING_MENU}`;
function soFar(lineCount: number, ticketCount: number): string[] {
return lineCount > 0 ? [`${plural(lineCount, 'line')}, ${plural(ticketCount, 'ticket')} so far.`] : [];
return lineCount > 0 ? [`${plural(lineCount, 'number')}, ${plural(ticketCount, 'ticket')} so far.`] : [];
}
export function progressUpdate(lineCount: number, ticketCount: number): string {
@ -92,7 +92,7 @@ export function summary(s: SummaryInput): string {
'',
`*Shop:* ${s.shopName}`,
`*Draw:* ${drawLabel(s.drawDate, s.today)}`,
`*Total:* ${plural(s.lineCount, 'line')}, ${plural(s.ticketCount, 'ticket')}`,
`*Total:* ${plural(s.lineCount, 'number')}, ${plural(s.ticketCount, 'ticket')}`,
];
if (s.unreadable.length === 0 && s.leftOut.length === 0) out.push('✅ Everything was read');
out.push('', '*Tickets*');

View File

@ -66,7 +66,7 @@ describe('reminder', () => {
expect(await sweepAt(after(10))).toEqual({ reminded: 1, expired: 0 });
expect(sent).toEqual([{ chatId: 'chat-1@lid', message: msg.reminder(2, 4) }]);
expect(sent[0]!.message).toBe('⏳ *Unfinished order*\n2 lines, 4 tickets so far.\n\n*Reply:*\n1. Done\n2. Cancel');
expect(sent[0]!.message).toBe('⏳ *Unfinished order*\n2 numbers, 4 tickets so far.\n\n*Reply:*\n1. Done\n2. Cancel');
expect(await sweepAt(after(11))).toEqual({ reminded: 0, expired: 0 });
expect(await sweepAt(after(20))).toEqual({ reminded: 0, expired: 0 });
@ -284,8 +284,8 @@ describe('the reply after a pause', () => {
it('words it with the running totals and the menu', () => {
const menu = '\n\n*When the order is finished, reply:*\n1. Done\n2. Cancel';
expect(msg.progressUpdate(8, 20)).toBe('✅ *Got it*\n8 lines, 20 tickets so far.' + menu);
expect(msg.progressUpdate(1, 1)).toBe('✅ *Got it*\n1 line, 1 ticket so far.' + menu);
expect(msg.progressUpdate(8, 20)).toBe('✅ *Got it*\n8 numbers, 20 tickets so far.' + menu);
expect(msg.progressUpdate(1, 1)).toBe('✅ *Got it*\n1 number, 1 ticket so far.' + menu);
expect(msg.progressUpdate(0, 0)).toBe('✅ *Got it*' + menu);
});

View File

@ -0,0 +1,10 @@
{
"totalTickets": 3,
"lines": [
"1991 x2 @1",
"3267 x1 @2"
],
"repeated": [],
"unreadable": [],
"ignored": []
}

View File

@ -0,0 +1,2 @@
1991/2
3267=1