Workflow-App/Workflow/scripts/_create_vat_collection.js

235 lines
8.1 KiB
JavaScript
Raw Normal View History

// scripts/_create_vat_collection.js
// One-shot: create the ysterbaard_SARS_VAT_VAT201 collection via the
// PocketBase admin API. Safe to re-run: PB returns 400 on duplicate
// collection name, which we treat as "already exists".
const fs = require("node:fs");
const path = require("node:path");
// Load .env.local manually (no dotenv in node_modules)
const ENV_PATH = path.join(__dirname, "..", ".env.local");
for (const line of fs.readFileSync(ENV_PATH, "utf-8").split(/\r?\n/)) {
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
}
const PB_URL = process.env.PB_URL;
const PB_TOKEN = process.env.PB_TOKEN;
if (!PB_URL || !PB_TOKEN) {
console.error("PB_URL / PB_TOKEN missing in .env.local");
process.exit(1);
}
const COLLECTION_NAME = "ysterbaard_SARS_VAT_VAT201";
const body = {
name: COLLECTION_NAME,
type: "base",
schema: [
{
name: "period",
type: "text",
required: true,
options: { min: 6, max: 6, pattern: "^\\d{6}$" },
},
{
name: "Standard Rate 1",
type: "number",
required: false,
options: {},
},
{
name: "Zero Rate 2",
type: "number",
required: false,
options: {},
},
{
name: "Exempt and non Supplies 3",
type: "number",
required: false,
options: {},
},
{
name: "Capital goods and services supplied to you 14",
type: "number",
required: false,
options: {},
},
{
name: "Other goods and services supplied to you 15",
type: "number",
required: false,
options: {},
},
{ name: "total_payable", type: "number", required: false, options: {} },
{ name: "interest_d", type: "number", required: false, options: {} },
{ name: "penalties_d", type: "number", required: false, options: {} },
{
name: "status",
type: "select",
required: true,
options: { maxSelect: 1, values: ["draft", "submitted", "paid"] },
},
{ name: "date_submitted", type: "date", required: false, options: { min: "", max: "" } },
{ name: "date_paid", type: "date", required: false, options: { min: "", max: "" } },
{ name: "due_date", type: "date", required: false, options: { min: "", max: "" } },
{
name: "payment_reference",
type: "text",
required: false,
options: { min: 0, max: 100, pattern: "" },
},
{
// Hidden upsert key — mirrors EMP201
name: "company_period_key",
type: "text",
required: false,
options: { min: 0, max: 0, pattern: "" },
},
{
name: "company",
type: "relation",
required: true,
options: {
collectionId: "pbc_3748838150", // ysterbaard_companies
cascadeDelete: false,
minSelect: 1,
maxSelect: 1,
},
},
{ name: "Return_Filed", type: "bool", required: false, options: {} },
{ name: "last_updated", type: "date", required: false, options: { min: "", max: "" } },
{ name: "notes", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } },
// ── Contact fields (added 2026-09-13) ──────────────────────
{ name: "First Name", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } },
{ name: "Surname", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } },
{ name: "Capacity", type: "text", required: false, options: { min: 0, max: 0, pattern: "" } },
{ name: "Bus Tel no", type: "number", required: false, options: {} },
{ name: "Cell no", type: "number", required: false, options: {} },
{ name: "Contact Email", type: "email", required: false, options: { exceptDomains: [], onlyDomains: [] } },
// ── End contact fields ─────────────────────────────────────
{ name: "is_active", type: "bool", required: false, options: {} },
{
name: "documents",
type: "file",
required: false,
options: {
maxSelect: 10,
maxSize: 10485760,
mimeTypes: ["application/pdf", "image/png", "image/jpeg"],
thumbs: null,
protected: false,
},
},
],
indexes: [
"CREATE UNIQUE INDEX idx_vat201_company_period ON ysterbaard_SARS_VAT_VAT201 (company_period_key)",
"CREATE INDEX idx_vat201_period ON ysterbaard_SARS_VAT_VAT201 (period)",
"CREATE INDEX idx_vat201_status ON ysterbaard_SARS_VAT_VAT201 (status)",
],
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
};
(async () => {
console.log(`Target: ${body.name} (${body.schema.length} user-defined fields + ${body.indexes.length} indexes)`);
// Step 1: POST the collection with schema only. PB will create the
// collection and its columns. Indexes are added separately because
// PB validates indexes against existing columns; if we POST them
// in the same call the columns don't exist yet and the whole
// transaction rolls back.
const indexes = body.indexes;
const createBody = { ...body, indexes: [] };
console.log(`\n[1/2] POST /api/collections (schema only)`);
const res = await fetch(`${PB_URL}/api/collections`, {
method: "POST",
headers: {
Authorization: PB_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify(createBody),
});
const text = await res.text();
console.log(`HTTP ${res.status}`);
if (!res.ok) {
console.error(`Failed: ${text.slice(0, 500)}`);
if (res.status === 400 && /already exists/i.test(text)) {
console.error("→ Collection already exists. Skipping create; will patch indexes.");
} else {
process.exit(1);
}
}
const created = res.ok ? JSON.parse(text) : null;
const collectionId = created?.id;
if (created) {
console.log(`Created collection id: ${collectionId}`);
console.log(`Fields persisted: ${(created.schema || []).length}`);
} else {
// Look up the existing collection id by name
const list = await fetch(`${PB_URL}/api/collections?perPage=200`, {
headers: { Authorization: PB_TOKEN },
});
const all = (await list.json()).items || [];
const existing = all.find((c) => c.name === body.name);
if (!existing) {
console.error("Could not find existing collection. Aborting.");
process.exit(1);
}
console.log(`Found existing id: ${existing.id}`);
}
const targetId = collectionId || (await (async () => {
const list = await fetch(`${PB_URL}/api/collections?perPage=200`, { headers: { Authorization: PB_TOKEN } });
return ((await list.json()).items || []).find((c) => c.name === body.name)?.id;
})());
if (!targetId) {
console.error("No collection id resolved. Aborting.");
process.exit(1);
}
// Step 2: PATCH the collection to add the indexes. By now the
// columns exist, so the CREATE INDEX statements succeed.
console.log(`\n[2/2] PATCH /api/collections/${targetId} (add indexes)`);
const patch = await fetch(`${PB_URL}/api/collections/${targetId}`, {
method: "PATCH",
headers: {
Authorization: PB_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({ indexes }),
});
const patchText = await patch.text();
console.log(`HTTP ${patch.status}`);
if (!patch.ok) {
console.error(`Failed: ${patchText.slice(0, 500)}`);
process.exit(1);
}
const final = JSON.parse(patchText);
console.log(`Final indexes: ${(final.indexes || []).length}`);
for (const ix of final.indexes || []) console.log(` - ${ix}`);
// Step 3: verify field count
console.log(`\nVerify: GET /api/collections/${targetId}`);
const verify = await fetch(`${PB_URL}/api/collections/${targetId}`, {
headers: { Authorization: PB_TOKEN },
});
const verifyJson = await verify.json();
console.log(` fields: ${(verifyJson.schema || []).length}`);
console.log(` indexes: ${(verifyJson.indexes || []).length}`);
console.log(` listRule: ${JSON.stringify(verifyJson.listRule)}`);
console.log(` name: ${verifyJson.name}`);
console.log(`\nDone.`);
process.exit(0);
})().catch((err) => {
console.error("Unexpected error:", err);
process.exit(1);
});