WFDASH-WEBAPP/scripts/nss-grease-nipples-ingest.ts

187 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
* One-off ingest: write the 33 NSS grease nipple items
* (DIN71412A 180°, DIN71412B 45°, DIN71412C 90°, DIN3404 Button,
* DIN71412A Long) from `scripts/nss-grease-nipples-pb-import.json`
* into the PocketBase `stock_items` collection.
*
* Same safety rails as the other ingest scripts (idempotent,
* applyDefaultMarkupToPatch, per-row try/catch).
*
* Run:
* npx tsx scripts/nss-grease-nipples-ingest.ts --dry-run --limit 5
* npx tsx scripts/nss-grease-nipples-ingest.ts --limit 5
* npx tsx scripts/nss-grease-nipples-ingest.ts
*/
import "./_env";
import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin";
import { applyDefaultMarkupToPatch } from "../src/lib/server/stock-default-markup-service";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const IMPORT_JSON = join(
import.meta.dirname,
"nss-grease-nipples-pb-import.json",
);
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const limitArg = args.find((a) => a.startsWith("--limit="));
const limit = limitArg ? Number(limitArg.split("=")[1]) : Infinity;
type ImportRecord = {
name: string;
sku: string;
description: string;
location: string;
notes: string;
cost_history: Array<{
price: number;
currency: string;
source: string;
extracted_at: string;
source_url: string;
qty_basis: number;
extraction_method: string;
notes: string;
}>;
cost_price: number;
unit: string;
current_qty: number;
min_qty: number;
target_qty: number;
is_active: boolean;
supplier: string;
category: string;
};
function buildPatch(r: ImportRecord): Record<string, unknown> {
const seedEntry = {
cost_price: r.cost_price,
quantity: 0,
purchased_at: r.cost_history[0]?.extracted_at ?? new Date().toISOString(),
purchase_id: "nss-grease-nipples-2026-09-16",
supplier_id: r.supplier,
source: "nssfasteners.co.za",
source_url: r.cost_history[0]?.source_url ?? "",
currency: r.cost_history[0]?.currency ?? "ZAR",
qty_basis: r.cost_history[0]?.qty_basis ?? 1,
extraction_method:
r.cost_history[0]?.extraction_method ?? "browser_automation",
notes: r.cost_history[0]?.notes ?? r.location,
};
return applyDefaultMarkupToPatch(
{
sku: r.sku,
name: r.name,
description: r.description,
unit: r.unit,
location: r.location,
current_qty: r.current_qty,
min_qty: r.min_qty,
target_qty: r.target_qty,
supplier: r.supplier,
category: r.category,
is_active: r.is_active,
notes: r.notes,
cost_price: r.cost_price,
cost_history: [seedEntry],
},
undefined,
);
}
async function loadExistingSkus(): Promise<Set<string>> {
const pb = await getAdminPocketBase();
const rows = await pb.collection("stock_items").getFullList<{ sku: string }>({
fields: "sku",
batch: 500,
});
return new Set(rows.map((r) => (r.sku || "").trim()).filter(Boolean));
}
async function main(): Promise<void> {
const t0 = Date.now();
const importDoc = JSON.parse(readFileSync(IMPORT_JSON, "utf8")) as {
records: ImportRecord[];
};
const allRecords = importDoc.records;
const records = Number.isFinite(limit)
? allRecords.slice(0, limit)
: allRecords;
console.log(`Loaded ${allRecords.length} records from ${IMPORT_JSON}`);
if (records.length < allRecords.length) {
console.log(` --limit applied: running on first ${records.length}`);
}
console.log(`Mode: ${dryRun ? "DRY RUN (no writes)" : "WRITE"}`);
const existingSkus = await loadExistingSkus();
console.log(`Existing SKUs in PB: ${existingSkus.size}`);
const toIngest = records.filter((r) => !existingSkus.has(r.sku));
const skipped = records.filter((r) => existingSkus.has(r.sku));
console.log(`Records to ingest: ${toIngest.length}`);
if (skipped.length > 0) {
console.log(`Records skipped (already in PB): ${skipped.length}`);
}
if (dryRun) {
console.log("\nDry run — preview of what would be written:\n");
let i = 0;
for (const r of toIngest.slice(0, 5)) {
const patch = buildPatch(r);
console.log(
` [${++i}] ${patch.sku} ${patch.name} cost=R ${patch.cost_price} sell=R ${patch.selling_price} markup=${patch.markup}% cat=${patch.category}`,
);
}
if (toIngest.length > 5) {
console.log(` ... and ${toIngest.length - 5} more`);
}
console.log(`\n duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
return;
}
const pb = await getAdminPocketBase();
let created = 0;
let failed = 0;
const failures: Array<{ sku: string; error: string }> = [];
for (const r of toIngest) {
try {
const patch = buildPatch(r);
await pb.collection("stock_items").create(patch);
created++;
if (created % 50 === 0 || created === toIngest.length) {
console.log(
` ... ${created}/${toIngest.length} written (last: ${patch.sku})`,
);
}
} catch (err) {
failed++;
const message = err instanceof Error ? err.message : String(err);
failures.push({ sku: r.sku, error: message });
}
}
console.log("\n=== Ingest summary ===");
console.log(` Created: ${created}`);
console.log(` Failed: ${failed}`);
console.log(` Skipped: ${skipped.length} (already in PB)`);
console.log(` Duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
if (failures.length > 0) {
console.log("\nFailures:");
for (const f of failures) {
console.log(` ${f.sku} → ${f.error}`);
}
process.exitCode = 1;
}
}
main().catch((err) => {
console.error("FATAL:", err);
process.exit(1);
});