Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"start": "node src/server.js",
"dev": "node --watch src/server.js",
"seed": "node src/db/seed.js",
"seed:demo": "node src/db/seed-demo.js",
"backup": "node src/scripts/backup.js",
"test": "node --test test/api.test.js"
},
Expand Down
295 changes: 295 additions & 0 deletions backend/src/db/seed-demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
'use strict';

require('dotenv').config({ path: require('path').join(__dirname, '..', '..', '..', '.env') });

const { getDb, closeDb } = require('./index');

/**
* Seed deterministic demo data for live presentations.
*
* Unlike seed.js (random UUIDs), this uses fixed IDs so provenance URLs
* are predictable and QR codes can be pre-printed.
*
* Safe to re-run: checks for existing demo farm before inserting.
*
* Usage:
* node src/db/seed-demo.js # seed demo data
* node src/db/seed-demo.js --reset # drop + re-seed
*/
function seedDemo() {
const db = getDb();

const reset = process.argv.includes('--reset');
if (reset) {
console.log('Resetting database for fresh demo seed...');
db.exec('DELETE FROM payments');
db.exec('DELETE FROM transfers');
db.exec('DELETE FROM lots');
db.exec('DELETE FROM checkins');
db.exec('DELETE FROM shifts');
db.exec('DELETE FROM workers');
db.exec('DELETE FROM farms');
}

const existing = db.prepare('SELECT id FROM farms WHERE id = ?').get(IDS.farm);
if (existing) {
console.log('Demo data already seeded. Use --reset to re-seed.');
closeDb();
return;
}

console.log('Seeding demo data...');

const now = new Date();
const today = now.toISOString().slice(0, 10);
const yesterday = new Date(now - 86400000);
const threeDaysAgo = new Date(now - 3 * 86400000);
const fmt = (d) => (d instanceof Date ? d : new Date(d)).toISOString().replace('T', ' ').slice(0, 19);

// ── Farm ──────────────────────────────────────────────────────────
db.prepare(`
INSERT INTO farms (id, name, location, altitude_m, owner_name, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(IDS.farm, 'Finca El Salvador', 'Santa Ana, El Salvador', 1400, 'Carlos Mendoza', fmt(threeDaysAgo));

// ── Foreman ───────────────────────────────────────────────────────
db.prepare(`
INSERT INTO workers (id, farm_id, name, phone, role, liquid_address, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(IDS.foreman, IDS.farm, 'Miguel Angel Torres', '+503-7234-5678',
'foreman', 'tex1qforeman0000000000000000000000000000000', fmt(threeDaysAgo));

// ── Workers ───────────────────────────────────────────────────────
const workers = [
{ id: IDS.workers[0], name: 'Ana Garcia', phone: '+503-7111-0001' },
{ id: IDS.workers[1], name: 'Roberto Hernandez', phone: '+503-7111-0002' },
{ id: IDS.workers[2], name: 'Maria Lopez', phone: '+503-7111-0003' },
{ id: IDS.workers[3], name: 'Jose Martinez', phone: '+503-7111-0004' },
{ id: IDS.workers[4], name: 'Carmen Rodriguez', phone: '+503-7111-0005' },
{ id: IDS.workers[5], name: 'Luis Ramirez', phone: '+503-7111-0006' },
];

const insertWorker = db.prepare(`
INSERT INTO workers (id, farm_id, name, phone, role, liquid_address, created_at)
VALUES (?, ?, ?, ?, 'worker', ?, ?)
`);
workers.forEach((w, i) => {
insertWorker.run(w.id, IDS.farm, w.name, w.phone,
'liq1qworker00000000000000000000000000000' + (i + 1), fmt(threeDaysAgo));
});

// ── Shift 1: 3 days ago, closed ──────────────────────────────────
insertShift(db, {
id: IDS.shifts[0], farmId: IDS.farm, foremanId: IDS.foreman,
date: new Date(now - 3 * 86400000), status: 'closed',
workerIds: IDS.workers.slice(0, 5), fmt,
});

// ── Shift 2: yesterday, closed ───────────────────────────────────
insertShift(db, {
id: IDS.shifts[1], farmId: IDS.farm, foremanId: IDS.foreman,
date: yesterday, status: 'closed',
workerIds: IDS.workers.slice(0, 4), fmt,
});

// ── Shift 3: today, open (for live demo check-ins) ──────────────
insertShift(db, {
id: IDS.shifts[2], farmId: IDS.farm, foremanId: IDS.foreman,
date: now, status: 'open',
workerIds: IDS.workers.slice(0, 2), fmt, // only 2 checked in so far
});

// ── Lot 1: full custody chain (linked to shift 1) ────────────────
const lot1Asset = 'liq_' + IDS.lots[0].replace(/-/g, '').slice(0, 32);
db.prepare(`
INSERT INTO lots (id, shift_id, farm_id, weight_kg, grade, gps_lat, gps_lng, asset_id, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(IDS.lots[0], IDS.shifts[0], IDS.farm, 380.5, 'A', 13.9942, -89.5469, lot1Asset,
'Primera cosecha de la temporada. Cerezas rojas maduras.',
fmt(new Date(threeDaysAgo.getTime() + 9 * 3600000)));

insertCustodyChain(db, IDS.lots[0], threeDaysAgo, fmt);

// ── Lot 2: partial chain (linked to shift 2, only to dry mill) ───
const lot2Asset = 'liq_' + IDS.lots[1].replace(/-/g, '').slice(0, 32);
db.prepare(`
INSERT INTO lots (id, shift_id, farm_id, weight_kg, grade, gps_lat, gps_lng, asset_id, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(IDS.lots[1], IDS.shifts[1], IDS.farm, 245.0, 'B', 13.9950, -89.5475, lot2Asset,
'Segunda cosecha. Mix of red and yellow cherries.',
fmt(new Date(yesterday.getTime() + 9 * 3600000)));

insertPartialCustodyChain(db, IDS.lots[1], yesterday, fmt);

// ── Payments: shift 1 (paid), shift 2 (pending) ──────────────────
const PAY_RATE = 5000;
const insertPayment = db.prepare(`
INSERT INTO payments (id, worker_id, shift_id, amount_sats, lightning_invoice, payment_hash, status, paid_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

// Shift 1 — all 5 paid
IDS.workers.slice(0, 5).forEach((wid, i) => {
insertPayment.run(
IDS.payments.shift1[i], wid, IDS.shifts[0], PAY_RATE,
'lnbc50u1demo_invoice_' + wid.slice(0, 8),
'payment_hash_' + wid.slice(0, 8),
'paid',
fmt(new Date(threeDaysAgo.getTime() + 10 * 3600000 + i * 60000)),
fmt(threeDaysAgo));
});

// Shift 2 — 4 pending
IDS.workers.slice(0, 4).forEach((wid, i) => {
insertPayment.run(
IDS.payments.shift2[i], wid, IDS.shifts[1], PAY_RATE,
null, null, 'pending', null, fmt(yesterday));
});

closeDb();

console.log('');
console.log('Demo seed complete!');
console.log('');
console.log(' Farm: Finca El Salvador');
console.log(' Foreman: Miguel Angel Torres');
console.log(' Workers: 6');
console.log(' Shifts: 2 closed + 1 open (today)');
console.log(' Lots: 2 (1 full chain, 1 partial)');
console.log('');
console.log(' Lot 1 (full chain): ' + IDS.lots[0]);
console.log(' Lot 2 (partial): ' + IDS.lots[1]);
console.log(' Today\'s open shift: ' + IDS.shifts[2]);
console.log('');
console.log(' Provenance URLs:');
console.log(' http://localhost:3000/provenance/' + IDS.lots[0]);
console.log(' http://localhost:3000/provenance/' + IDS.lots[1]);
console.log('');
}

// ── Deterministic IDs ─────────────────────────────────────────────────
// Fixed UUIDs so provenance URLs and QR codes are stable across re-seeds.
const IDS = {
farm: 'f0000000-0000-4000-a000-000000000001',
foreman: 'w0000000-0000-4000-a000-000000000010',
workers: [
'w0000000-0000-4000-a000-000000000001',
'w0000000-0000-4000-a000-000000000002',
'w0000000-0000-4000-a000-000000000003',
'w0000000-0000-4000-a000-000000000004',
'w0000000-0000-4000-a000-000000000005',
'w0000000-0000-4000-a000-000000000006',
],
shifts: [
's0000000-0000-4000-a000-000000000001',
's0000000-0000-4000-a000-000000000002',
's0000000-0000-4000-a000-000000000003',
],
lots: [
'a0000000-cafe-4000-a000-000000000001',
'a0000000-cafe-4000-a000-000000000002',
],
checkins: {
shift1: ['c1000000-0000-4000-a000-00000000000' + 1, 'c1000000-0000-4000-a000-00000000000' + 2, 'c1000000-0000-4000-a000-00000000000' + 3, 'c1000000-0000-4000-a000-00000000000' + 4, 'c1000000-0000-4000-a000-00000000000' + 5],
shift2: ['c2000000-0000-4000-a000-00000000000' + 1, 'c2000000-0000-4000-a000-00000000000' + 2, 'c2000000-0000-4000-a000-00000000000' + 3, 'c2000000-0000-4000-a000-00000000000' + 4],
shift3: ['c3000000-0000-4000-a000-00000000000' + 1, 'c3000000-0000-4000-a000-00000000000' + 2],
},
transfers: [
't0000000-0000-4000-a000-000000000001',
't0000000-0000-4000-a000-000000000002',
't0000000-0000-4000-a000-000000000003',
't0000000-0000-4000-a000-000000000004',
't0000000-0000-4000-a000-000000000005',
't0000000-0000-4000-a000-000000000006',
't0000000-0000-4000-a000-000000000007',
't0000000-0000-4000-a000-000000000008',
],
payments: {
shift1: ['p1000000-0000-4000-a000-00000000000' + 1, 'p1000000-0000-4000-a000-00000000000' + 2, 'p1000000-0000-4000-a000-00000000000' + 3, 'p1000000-0000-4000-a000-00000000000' + 4, 'p1000000-0000-4000-a000-00000000000' + 5],
shift2: ['p2000000-0000-4000-a000-00000000000' + 1, 'p2000000-0000-4000-a000-00000000000' + 2, 'p2000000-0000-4000-a000-00000000000' + 3, 'p2000000-0000-4000-a000-00000000000' + 4],
},
};

// ── Helpers ───────────────────────────────────────────────────────────

function insertShift(db, { id, farmId, foremanId, date, status, workerIds, fmt }) {
const d = date instanceof Date ? date : new Date(date);
const qrData = JSON.stringify({ shiftId: id, farmId, foremanId, expiresAt: fmt(d) });
const closedAt = status === 'closed' ? fmt(new Date(d.getTime() + 8 * 3600000)) : null;

db.prepare(`
INSERT INTO shifts (id, farm_id, foreman_id, date, status, qr_data, liquid_tx, created_at, closed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, farmId, foremanId, d.toISOString().slice(0, 10), status, qrData,
status === 'closed' ? 'liq_asset_shift_' + id.slice(0, 8) : null,
fmt(d), closedAt);

// Check-ins
const shiftKey = id === IDS.shifts[0] ? 'shift1' : id === IDS.shifts[1] ? 'shift2' : 'shift3';
const insertCheckin = db.prepare(`
INSERT INTO checkins (id, shift_id, worker_id, checked_in_at, signature)
VALUES (?, ?, ?, ?, ?)
`);
workerIds.forEach((wid, i) => {
insertCheckin.run(
IDS.checkins[shiftKey][i], id, wid,
fmt(new Date(d.getTime() + (i + 1) * 5 * 60000)),
'sig_demo_' + wid.slice(0, 8));
});
}

function insertCustodyChain(db, lotId, baseDate, fmt) {
const ins = db.prepare(`
INSERT INTO transfers (id, lot_id, from_entity, to_entity, entity_type, metadata, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);

ins.run(IDS.transfers[0], lotId,
'Finca El Salvador', 'Finca El Salvador', 'farm',
JSON.stringify({ action: 'harvest', weight_kg: 380.5, grade: 'A', workers: ['Ana Garcia', 'Roberto Hernandez', 'Maria Lopez', 'Jose Martinez', 'Carmen Rodriguez'], notes: 'Hand-picked, red cherries only' }),
fmt(new Date(baseDate.getTime() + 9 * 3600000)));

ins.run(IDS.transfers[1], lotId,
'Finca El Salvador', 'Beneficio Humedo Santa Ana', 'wet_mill',
JSON.stringify({ action: 'wet_processing', received_weight_kg: 380.5, processing_method: 'washed', fermentation_hours: 36, notes: 'Clean fermentation, no off-flavors' }),
fmt(new Date(baseDate.getTime() + 24 * 3600000)));

ins.run(IDS.transfers[2], lotId,
'Beneficio Humedo Santa Ana', 'Beneficio Seco Las Flores', 'dry_mill',
JSON.stringify({ action: 'dry_processing', received_weight_kg: 76.1, drying_method: 'raised_beds', drying_days: 14, final_moisture_pct: 11.5, defect_count: 3, screen_size: '17/18' }),
fmt(new Date(baseDate.getTime() + 20 * 86400000)));

ins.run(IDS.transfers[3], lotId,
'Beneficio Seco Las Flores', 'Caravela Coffee El Salvador', 'exporter',
JSON.stringify({ action: 'export_preparation', received_weight_kg: 75.0, export_weight_kg: 69.0, ico_certificate: 'ICO-SLV-2026-CAFE01', destination: 'Portland, OR, USA' }),
fmt(new Date(baseDate.getTime() + 25 * 86400000)));

ins.run(IDS.transfers[4], lotId,
'Caravela Coffee El Salvador', 'Heart Coffee Roasters', 'roaster',
JSON.stringify({ action: 'roasting', received_weight_kg: 69.0, roasted_weight_kg: 58.6, roast_profile: 'medium', cupping_score: 86.5, tasting_notes: 'Chocolate, citrus, brown sugar' }),
fmt(new Date(baseDate.getTime() + 40 * 86400000)));
}

function insertPartialCustodyChain(db, lotId, baseDate, fmt) {
const ins = db.prepare(`
INSERT INTO transfers (id, lot_id, from_entity, to_entity, entity_type, metadata, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);

ins.run(IDS.transfers[5], lotId,
'Finca El Salvador', 'Finca El Salvador', 'farm',
JSON.stringify({ action: 'harvest', weight_kg: 245.0, grade: 'B', workers: ['Ana Garcia', 'Roberto Hernandez', 'Maria Lopez', 'Jose Martinez'], notes: 'Mix of red and yellow cherries' }),
fmt(new Date(baseDate.getTime() + 9 * 3600000)));

ins.run(IDS.transfers[6], lotId,
'Finca El Salvador', 'Beneficio Humedo Santa Ana', 'wet_mill',
JSON.stringify({ action: 'wet_processing', received_weight_kg: 245.0, processing_method: 'honey', fermentation_hours: 24, notes: 'Honey process — partial mucilage removed' }),
fmt(new Date(baseDate.getTime() + 24 * 3600000)));

ins.run(IDS.transfers[7], lotId,
'Beneficio Humedo Santa Ana', 'Beneficio Seco Las Flores', 'dry_mill',
JSON.stringify({ action: 'dry_processing', received_weight_kg: 53.9, drying_method: 'patio', drying_days: 18, final_moisture_pct: 11.8, defect_count: 7, screen_size: '15/16' }),
fmt(new Date(baseDate.getTime() + 20 * 86400000)));
}

seedDemo();
Loading