Wire the order parser to the WhatsApp bot. A known user forwards an order, ends it with Done, picks a shop, confirms a summary and the order is saved.
138 lines
5.5 KiB
TypeScript
138 lines
5.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { openDb } from './index';
|
|
import { runMigrations } from './migrate';
|
|
import { makeDb, seedFixture } from '../testing/helpers';
|
|
|
|
const throwsConstraint = (fn: () => unknown) => expect(fn).toThrow(/CHECK|UNIQUE|FOREIGN KEY|constraint/i);
|
|
|
|
describe('migrations', () => {
|
|
it('apply once and are then a no-op', () => {
|
|
const db = openDb(':memory:');
|
|
expect(runMigrations(db)).toEqual(['001_init.sql', '002_conversation_flow.sql']);
|
|
expect(runMigrations(db)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('conversation flow columns (002)', () => {
|
|
it('exist, start empty, and ask_kind only takes known values', () => {
|
|
const db = makeDb();
|
|
const f = seedFixture(db);
|
|
db.prepare("INSERT INTO conversations (user_id, chat_id, state) VALUES (?, 'c', 'IDLE')").run(f.admin.id);
|
|
const row = db.prepare('SELECT * FROM conversations').get() as Record<string, unknown>;
|
|
for (const col of ['last_prompt', 'reminded_at', 'ask_kind', 'held_text']) expect(row[col]).toBeNull();
|
|
throwsConstraint(() => db.prepare("UPDATE conversations SET ask_kind = 'other'").run());
|
|
db.prepare("UPDATE conversations SET ask_kind = 'split'").run();
|
|
});
|
|
});
|
|
|
|
describe('schema rules', () => {
|
|
const orderFor = (db: ReturnType<typeof makeDb>) => {
|
|
const f = seedFixture(db);
|
|
const id = Number(
|
|
db
|
|
.prepare(
|
|
`INSERT INTO orders (shop_id, draw_date, source, created_by_user_id, raw_text)
|
|
VALUES (?, '2026-11-03', 'whatsapp_text', ?, '0888')`,
|
|
)
|
|
.run(f.shops.a, f.admin.id).lastInsertRowid,
|
|
);
|
|
return { f, orderId: id };
|
|
};
|
|
|
|
it('accepts a normal order line, including leading zeros', () => {
|
|
const db = makeDb();
|
|
const { orderId } = orderFor(db);
|
|
db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, ?)').run(orderId, '0057', 2);
|
|
const row = db.prepare('SELECT ending, qty FROM order_lines').get() as { ending: string; qty: number };
|
|
expect(row).toEqual({ ending: '0057', qty: 2 });
|
|
});
|
|
|
|
it('rejects an ending that is not exactly 4 digits', () => {
|
|
const db = makeDb();
|
|
const { orderId } = orderFor(db);
|
|
const add = (ending: string) => () =>
|
|
db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, 1)').run(orderId, ending);
|
|
throwsConstraint(add('888'));
|
|
throwsConstraint(add('08888'));
|
|
throwsConstraint(add('08a8'));
|
|
});
|
|
|
|
it('rejects a quantity of zero or less', () => {
|
|
const db = makeDb();
|
|
const { orderId } = orderFor(db);
|
|
const add = (qty: number) => () =>
|
|
db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, ?)').run(orderId, '0888', qty);
|
|
throwsConstraint(add(0));
|
|
throwsConstraint(add(-1));
|
|
});
|
|
|
|
it('requires a well-formed draw date on every order', () => {
|
|
const db = makeDb();
|
|
const f = seedFixture(db);
|
|
const add = (date: string | null) => () =>
|
|
db
|
|
.prepare("INSERT INTO orders (shop_id, draw_date, source, created_by_user_id, raw_text) VALUES (?, ?, 'web', ?, '0888')")
|
|
.run(f.shops.a, date, f.admin.id);
|
|
expect(add('2026-11-03')).not.toThrow();
|
|
throwsConstraint(add(null));
|
|
throwsConstraint(add('3/11/2026'));
|
|
});
|
|
|
|
it('only allows prize tiers 4 to 9', () => {
|
|
const db = makeDb();
|
|
const draw = Number(
|
|
db.prepare("INSERT INTO draws (draw_date, scheme, draw_code) VALUES ('2026-11-03', 'TEST', 'TS-1')").run().lastInsertRowid,
|
|
);
|
|
const add = (tier: number) => () =>
|
|
db.prepare('INSERT INTO prizes (draw_id, tier, amount, ending) VALUES (?, ?, 100, ?)').run(draw, tier, '1234');
|
|
expect(add(4)).not.toThrow();
|
|
throwsConstraint(add(3));
|
|
throwsConstraint(add(10));
|
|
});
|
|
|
|
it('keeps phone numbers unique but allows several users without one', () => {
|
|
const db = makeDb();
|
|
const f = seedFixture(db);
|
|
const add = (username: string, phone: string | null) => () =>
|
|
db
|
|
.prepare('INSERT INTO users (username, password_hash, phone) VALUES (?, ?, ?)')
|
|
.run(username, 'x', phone);
|
|
throwsConstraint(add('dup', f.admin.phone));
|
|
expect(add('nophone1', null)).not.toThrow();
|
|
expect(add('nophone2', null)).not.toThrow();
|
|
});
|
|
|
|
it('refuses a phone number that is not digits of a sensible length', () => {
|
|
const db = makeDb();
|
|
const add = (phone: string) => () =>
|
|
db.prepare('INSERT INTO users (username, password_hash, phone) VALUES (?, ?, ?)').run('u' + phone, 'x', phone);
|
|
throwsConstraint(add('+919847049032'));
|
|
throwsConstraint(add('98470 49032'));
|
|
throwsConstraint(add('1234567')); // too short
|
|
expect(add('919847049032')).not.toThrow();
|
|
});
|
|
|
|
it('keeps usernames unique', () => {
|
|
const db = makeDb();
|
|
seedFixture(db);
|
|
const add = () => db.prepare("INSERT INTO users (username, password_hash) VALUES ('admin', 'x')").run();
|
|
throwsConstraint(add);
|
|
});
|
|
|
|
it('refuses a duplicate WhatsApp message id', () => {
|
|
const db = makeDb();
|
|
const add = () =>
|
|
db.prepare("INSERT INTO ingest_log (wa_message_id, kind, outcome) VALUES ('M1', 'text', 'accepted')").run();
|
|
expect(add).not.toThrow();
|
|
throwsConstraint(add);
|
|
});
|
|
|
|
it('deletes an order\'s lines together with the order', () => {
|
|
const db = makeDb();
|
|
const { orderId } = orderFor(db);
|
|
db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, 1)').run(orderId, '0888');
|
|
db.prepare('DELETE FROM orders WHERE id = ?').run(orderId);
|
|
expect(db.prepare('SELECT COUNT(*) AS n FROM order_lines').get()).toEqual({ n: 0 });
|
|
});
|
|
});
|