206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
/**
|
|
* KoolTab Device Simulator
|
|
*
|
|
* A CLI tool that simulates a Flutter bar terminal device.
|
|
* Usage: npx ts-node scripts/device-sync.ts
|
|
*
|
|
* Steps:
|
|
* 1. Register a device (or paste an existing token)
|
|
* 2. Sync open tabs
|
|
* 3. Receive pending commands
|
|
* 4. Acknowledge commands
|
|
*/
|
|
|
|
const API_URL = process.env.API_URL || 'http://localhost:3001/api/v1';
|
|
|
|
const args = process.argv.slice(2);
|
|
const command = args[0] || 'help';
|
|
|
|
interface SyncResult {
|
|
cursor: string;
|
|
commands: Array<{
|
|
id: string;
|
|
type: string;
|
|
tabId: string;
|
|
expectedVersion: number;
|
|
paymentMethod: string;
|
|
requestedBy: string;
|
|
createdAt: string;
|
|
}>;
|
|
}
|
|
|
|
async function api<T = any>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
const res = await fetch(`${API_URL}${endpoint}`, {
|
|
headers: { 'Content-Type': 'application/json', ...(options.headers as Record<string, string>) },
|
|
...options,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => null);
|
|
const msg = body?.error?.message || `HTTP ${res.status}`;
|
|
throw new Error(msg);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
async function registerDevice() {
|
|
const venueId = args[1];
|
|
const name = args[2] || 'Simulated Terminal';
|
|
const code = args[3];
|
|
|
|
if (!venueId || !code) {
|
|
console.error('Usage: npx ts-node scripts/device-sync.ts register <venueId> <name> <registrationCode>');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Registering device "${name}" in venue ${venueId}...`);
|
|
const result = await api('/devices/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ venueId, name, registrationCode: code }),
|
|
});
|
|
|
|
console.log('Device registered!');
|
|
console.log(` Device ID: ${result.deviceId}`);
|
|
console.log(` Token: ${result.token}`);
|
|
console.log(` Expires: ${result.tokenExpiresAt}`);
|
|
console.log('\nSave this token for future sync operations.');
|
|
return result;
|
|
}
|
|
|
|
async function syncDevice() {
|
|
const deviceId = args[1];
|
|
const token = args[2];
|
|
|
|
if (!deviceId || !token) {
|
|
console.error('Usage: npx ts-node scripts/device-sync.ts sync <deviceId> <token> [cursor]');
|
|
process.exit(1);
|
|
}
|
|
|
|
const cursor = args[3];
|
|
const tabs = [
|
|
{ id: 'tab-' + Date.now(), customerName: 'Simulated Customer', version: 1, totalInCents: 1500, itemCount: 2 },
|
|
];
|
|
|
|
console.log(`Syncing device ${deviceId}...`);
|
|
const result = await api<SyncResult>(`/devices/${deviceId}/sync`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
body: JSON.stringify({ deviceId, cursor: cursor || null, openTabs: tabs }),
|
|
});
|
|
|
|
console.log('Sync complete!');
|
|
console.log(` New cursor: ${result.cursor}`);
|
|
console.log(` Commands received: ${result.commands.length}`);
|
|
|
|
for (const cmd of result.commands) {
|
|
console.log(` Command: ${cmd.id} - ${cmd.type} (tab: ${cmd.tabId}, payment: ${cmd.paymentMethod})`);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function ackCommand() {
|
|
const deviceId = args[1];
|
|
const token = args[2];
|
|
const commandId = args[3];
|
|
const status = args[4] || 'completed';
|
|
const result = args[5] || 'Tab closed successfully';
|
|
|
|
if (!deviceId || !token || !commandId) {
|
|
console.error('Usage: npx ts-node scripts/device-sync.ts ack <deviceId> <token> <commandId> [status] [result]');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Acknowledging command ${commandId} as ${status}...`);
|
|
const res = await api(`/devices/${deviceId}/commands/${commandId}/ack`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
body: JSON.stringify({ commandId, status, result }),
|
|
});
|
|
|
|
console.log('Ack complete!');
|
|
console.log(` Result: ${JSON.stringify(res)}`);
|
|
}
|
|
|
|
// Interactive demo sequence
|
|
async function demo() {
|
|
const venueId = args[1];
|
|
const code = args[2];
|
|
|
|
if (!venueId || !code) {
|
|
console.error('Usage: npx ts-node scripts/device-sync.ts demo <venueId> <registrationCode>');
|
|
console.error('First, generate a registration code from the dashboard (Devices > Add Device).');
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// 1. Register
|
|
const reg = await registerDevice();
|
|
const deviceId = reg.deviceId;
|
|
const token = reg.token;
|
|
|
|
// 2. Sync with tabs
|
|
console.log('\n--- Sync 1: Upload tabs ---');
|
|
const sync1 = await syncDevice();
|
|
|
|
if (sync1.commands.length > 0) {
|
|
// 3. Ack commands
|
|
for (const cmd of sync1.commands) {
|
|
console.log(`\n--- Ack: ${cmd.id} ---`);
|
|
await ackCommand(deviceId, token, cmd.id, 'completed', `Tab ${cmd.tabId} closed successfully`);
|
|
}
|
|
|
|
// 4. Sync again to see no pending commands
|
|
console.log('\n--- Sync 2: Verify no pending commands ---');
|
|
await syncDevice();
|
|
}
|
|
|
|
console.log('\n✅ Demo complete! Your device is fully functional.');
|
|
} catch (err: any) {
|
|
console.error('❌ Demo failed:', err.message);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
switch (command) {
|
|
case 'register':
|
|
await registerDevice();
|
|
break;
|
|
case 'sync':
|
|
await syncDevice();
|
|
break;
|
|
case 'ack':
|
|
await ackCommand();
|
|
break;
|
|
case 'demo':
|
|
await demo();
|
|
break;
|
|
default:
|
|
console.log(`
|
|
KoolTab Device Simulator
|
|
========================
|
|
|
|
Commands:
|
|
register <venueId> <name> <registrationCode> Register a new device
|
|
sync <deviceId> <token> [cursor] Sync device state
|
|
ack <deviceId> <token> <commandId> [status] Acknowledge a command
|
|
demo <venueId> <registrationCode> Run full demo sequence
|
|
|
|
Examples:
|
|
npx ts-node scripts/device-sync.ts register <venueId> "Bar Terminal 1" ABC123
|
|
npx ts-node scripts/device-sync.ts sync <deviceId> <token>
|
|
npx ts-node scripts/device-sync.ts ack <deviceId> <token> <commandId> completed
|
|
npx ts-node scripts/device-sync.ts demo <venueId> ABC123
|
|
|
|
Status values: completed, failed, conflict, expired
|
|
`);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error:', err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main(); |