133 lines
6.2 KiB
TypeScript
133 lines
6.2 KiB
TypeScript
|
|
/**
|
||
|
|
* Merge YG-1 drafts.
|
||
|
|
*
|
||
|
|
* PO26073102STK (existing draft, 4 HSS drill-bit lines, 2026-07-31)
|
||
|
|
* PO26080301STK (new draft, 16 tap lines, loaded 2026-08-03 from quote SOQ078608)
|
||
|
|
*
|
||
|
|
* Both are still in `draft` status (neither has been sent to the supplier),
|
||
|
|
* so the merge is purely local — the supplier hasn't seen either PO number.
|
||
|
|
*
|
||
|
|
* ## Direction
|
||
|
|
*
|
||
|
|
* Keep the older PO number (`PO26073102STK`) — it's the operator's
|
||
|
|
* existing accumulated draft per the system's "one draft per supplier"
|
||
|
|
* design (see `stock-quick-draft-service.ts`). The new draft was created
|
||
|
|
* by `2026-08-03-load-yg1-quote-soq078608.ts`; merging its lines back
|
||
|
|
* into the existing draft collapses the two into a single draft.
|
||
|
|
*
|
||
|
|
* ## What this does
|
||
|
|
*
|
||
|
|
* 1. Updates all 16 `stock_purchase_items` rows on `PO26080301STK`
|
||
|
|
* to point at `PO26073102STK` (just flip the `purchase` field).
|
||
|
|
* 2. Deletes the now-empty `PO26080301STK` header.
|
||
|
|
* 3. Updates `PO26073102STK`'s `notes` to record both quote sources.
|
||
|
|
*
|
||
|
|
* The line `id`s, `created` timestamps, `description`, `quantity`,
|
||
|
|
* `cost_price`, `notes` (including the "No stock" flag on TS2352932),
|
||
|
|
* and `stock_item` references are all preserved — this is a pure
|
||
|
|
* parent-pointer swap on the line rows.
|
||
|
|
*
|
||
|
|
* ## Preconditions
|
||
|
|
*
|
||
|
|
* - Both purchase ids exist and are in `status="draft"`.
|
||
|
|
* - Neither has been sent (`email_message_id` is empty, no `documents`).
|
||
|
|
* - The new draft has 16 lines, the old draft has 4.
|
||
|
|
*
|
||
|
|
* Run: `npx tsx scripts/2026-08-03-merge-yg1-drafts.ts`
|
||
|
|
*/
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
|
||
|
|
const PB_URL = "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io";
|
||
|
|
const ADMIN_EMAIL = "david.els1@outlook.com";
|
||
|
|
const ADMIN_PASSWORD = "1234567890";
|
||
|
|
|
||
|
|
const EXISTING_DRAFT_ID = "2a6qo4nxb2ni4tm"; // PO26073102STK — keep this one
|
||
|
|
const NEW_DRAFT_ID = "cj0h9jdsas2s4gk"; // PO26080301STK — drain into the above
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const pb = new PocketBase(PB_URL);
|
||
|
|
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||
|
|
console.log("Authenticated as admin");
|
||
|
|
|
||
|
|
// ── 1. Pre-flight checks ─────────────────────────────────────────
|
||
|
|
console.log("\n=== Pre-flight ===");
|
||
|
|
const existing = await pb.collection("stock_purchases").getOne(EXISTING_DRAFT_ID);
|
||
|
|
const newDraft = await pb.collection("stock_purchases").getOne(NEW_DRAFT_ID);
|
||
|
|
console.log(` PO26073102STK status=${existing.status} notes="${existing.notes}"`);
|
||
|
|
console.log(` PO26080301STK status=${newDraft.status} notes="${newDraft.notes}"`);
|
||
|
|
if (existing.status !== "draft" || newDraft.status !== "draft") {
|
||
|
|
throw new Error(`Refusing to merge: both drafts must be in 'draft' status (existing=${existing.status}, new=${newDraft.status})`);
|
||
|
|
}
|
||
|
|
if (existing.email_message_id || newDraft.email_message_id) {
|
||
|
|
throw new Error("Refusing to merge: one or both drafts already have an email_message_id (already sent?)");
|
||
|
|
}
|
||
|
|
const existingLines = await pb.collection("stock_purchase_items").getList(1, 1, {
|
||
|
|
filter: `purchase = "${EXISTING_DRAFT_ID}"`,
|
||
|
|
});
|
||
|
|
const newLines = await pb.collection("stock_purchase_items").getList(1, 1, {
|
||
|
|
filter: `purchase = "${NEW_DRAFT_ID}"`,
|
||
|
|
});
|
||
|
|
console.log(` PO26073102STK lines: ${existingLines.totalItems}`);
|
||
|
|
console.log(` PO26080301STK lines: ${newLines.totalItems}`);
|
||
|
|
|
||
|
|
// ── 2. Re-parent the new draft's lines ───────────────────────────
|
||
|
|
console.log("\n=== Re-parenting lines ===");
|
||
|
|
const newLinesList = await pb
|
||
|
|
.collection("stock_purchase_items")
|
||
|
|
.getFullList({ filter: `purchase = "${NEW_DRAFT_ID}"`, fields: "id,description,quantity" });
|
||
|
|
for (const ln of newLinesList) {
|
||
|
|
await pb.collection("stock_purchase_items").update(ln.id, {
|
||
|
|
purchase: EXISTING_DRAFT_ID,
|
||
|
|
});
|
||
|
|
console.log(` ${ln.id} → PO26073102STK (${ln.description.slice(0, 40)} qty=${ln.quantity})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 3. Delete the empty new draft ────────────────────────────────
|
||
|
|
console.log("\n=== Deleting empty draft PO26080301STK ===");
|
||
|
|
await pb.collection("stock_purchases").delete(NEW_DRAFT_ID);
|
||
|
|
console.log(` deleted ${NEW_DRAFT_ID}`);
|
||
|
|
|
||
|
|
// ── 4. Update notes on the surviving draft ───────────────────────
|
||
|
|
console.log("\n=== Updating notes on PO26073102STK ===");
|
||
|
|
const mergedNotes =
|
||
|
|
"YG-1 stock draft. Sources: existing draft since 2026-07-31 (4 HSS drill-bit lines) + YG-1 quote SOQ078608 dated 2026-08-03 for customer CNL001 (16 line items, merged 2026-08-03).";
|
||
|
|
await pb.collection("stock_purchases").update(EXISTING_DRAFT_ID, {
|
||
|
|
notes: mergedNotes,
|
||
|
|
});
|
||
|
|
console.log(` set notes: "${mergedNotes}"`);
|
||
|
|
|
||
|
|
// ── 5. Verify ────────────────────────────────────────────────────
|
||
|
|
console.log("\n=== Post-merge verification ===");
|
||
|
|
const finalDraft = await pb.collection("stock_purchases").getOne(EXISTING_DRAFT_ID, {
|
||
|
|
expand: "supplier",
|
||
|
|
});
|
||
|
|
const finalLines = await pb.collection("stock_purchase_items").getList(1, 1, {
|
||
|
|
filter: `purchase = "${EXISTING_DRAFT_ID}"`,
|
||
|
|
});
|
||
|
|
console.log(` Purchase: ${finalDraft.purchase_number} (${finalDraft.expand?.supplier?.suppliername})`);
|
||
|
|
console.log(` Status: ${finalDraft.status}`);
|
||
|
|
console.log(` Lines: ${finalLines.totalItems}`);
|
||
|
|
const totalCost = await pb
|
||
|
|
.collection("stock_purchase_items")
|
||
|
|
.getFullList({ filter: `purchase = "${EXISTING_DRAFT_ID}"` });
|
||
|
|
const total = totalCost.reduce((s, l) => s + l.quantity * l.cost_price, 0);
|
||
|
|
console.log(` Total cost (post-discount, excl VAT): R ${total.toFixed(2)}`);
|
||
|
|
|
||
|
|
// Confirm the new-draft id is gone.
|
||
|
|
try {
|
||
|
|
await pb.collection("stock_purchases").getOne(NEW_DRAFT_ID);
|
||
|
|
throw new Error("New draft still exists after delete!");
|
||
|
|
} catch (err) {
|
||
|
|
if (err instanceof Error && err.message.includes("not found")) {
|
||
|
|
console.log(` PO26080301STK (${NEW_DRAFT_ID}): deleted ✓`);
|
||
|
|
} else {
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error("FATAL:", err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|