Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
234 lines
8.1 KiB
JavaScript
234 lines
8.1 KiB
JavaScript
// 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);
|
|
});
|