diff --git a/scripts/add_payment_batch_field.py b/scripts/add_payment_batch_field.py new file mode 100644 index 0000000..dae9a14 --- /dev/null +++ b/scripts/add_payment_batch_field.py @@ -0,0 +1,198 @@ +""" +Add the JEI_payment_batches collection and a payment_batch relation on JEI_invoices. + +Idempotent: if the collection or the relation field already exists, the script +reports EXISTS and reuses the existing id. + +PB v0.39.8 API quirks (verified 2026-09-17, recorded in NOTES.md): +1. Field options for select/relation go at the FIELD ROOT, not inside `options`. +2. POST /api/collections uses key `fields`, not `schema`. +3. GET hides field options for select/relation (data IS stored, just not echoed back). +4. PATCH round-trip "wakes up" newly created collections. If a collection 403s on + read with empty listRule, PATCH any rule field (even with the same value) once. + +Run: python scripts/add_payment_batch_field.py +""" +import os +import sys +import json +import urllib.request +from urllib.error import HTTPError + +PB_URL = os.environ.get("PB_URL", "https://pb.agent.apphub.co.za") +PB_TOKEN = os.environ.get("PB_TOKEN", "") +if not PB_TOKEN: + env_path = os.path.join(os.path.dirname(__file__), "..", ".env.local") + if os.path.exists(env_path): + with open(env_path, "r", encoding="utf-8") as fh: + for line in fh: + if line.startswith("PB_TOKEN="): + PB_TOKEN = line.strip().split("=", 1)[1] + break +if not PB_TOKEN: + sys.exit("ERROR: PB_TOKEN not set") + +HEADERS = {"Authorization": PB_TOKEN, "Content-Type": "application/json"} + + +def pb_get(path): + req = urllib.request.Request(PB_URL + path, headers=HEADERS, method="GET") + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()) + + +def pb_post(path, body): + req = urllib.request.Request( + PB_URL + path, + data=json.dumps(body).encode("utf-8"), + headers=HEADERS, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()), None + except HTTPError as e: + return None, json.loads(e.read()) + + +def pb_patch(path, body): + req = urllib.request.Request( + PB_URL + path, + data=json.dumps(body).encode("utf-8"), + headers=HEADERS, + method="PATCH", + ) + try: + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()), None + except HTTPError as e: + return None, json.loads(e.read()) + + +SUPERUSER_RULES = { + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", +} + + +def payment_batches_schema(): + return { + "name": "JEI_payment_batches", + "type": "base", + "fields": [ + { + "name": "batch_ref", + "type": "text", + "required": True, + "min": 1, + "max": 50, + "pattern": "^[A-Za-z0-9_-]+$", + }, + { + "name": "payment_date", + "type": "date", + "required": True, + "min": "", + "max": "", + }, + { + "name": "pop_file", + "type": "file", + "required": True, + "maxSelect": 1, + "maxSize": 8388608, + "mimeTypes": ["application/pdf", "image/jpeg", "image/png"], + "thumbs": [], + }, + { + "name": "notes", + "type": "text", + "required": False, + "min": 0, + "max": 1000, + "pattern": "", + }, + ], + **SUPERUSER_RULES, + } + + +def main(): + print(f"PB_URL: {PB_URL}") + + # 1. Find or create JEI_payment_batches + listing = pb_get("/api/collections?perPage=200") + batches_id = None + for c in listing.get("items", []): + if c.get("name") == "JEI_payment_batches": + batches_id = c.get("id") + break + + if batches_id: + print(f"EXISTS: JEI_payment_batches (id={batches_id})") + else: + resp, err = pb_post("/api/collections", payment_batches_schema()) + if err is not None: + sys.exit(f"ERROR: failed to create JEI_payment_batches: {err}") + batches_id = resp.get("id") + print(f"CREATED: JEI_payment_batches (id={batches_id})") + + # 2. Find JEI_invoices + invoices_id = None + for c in listing.get("items", []): + if c.get("name") == "JEI_invoices": + invoices_id = c.get("id") + break + if not invoices_id: + sys.exit("ERROR: JEI_invoices collection not found") + + print(f"FOUND: JEI_invoices (id={invoices_id})") + + # 3. Check whether the relation field already exists + inv = pb_get(f"/api/collections/{invoices_id}") + field_names = {f.get("name") for f in inv.get("fields", [])} + + if "payment_batch" in field_names: + print("EXISTS: JEI_invoices.payment_batch field") + else: + new_field = { + "name": "payment_batch", + "type": "relation", + "required": False, + "collectionId": batches_id, + "cascadeDelete": False, + "minSelect": 0, + "maxSelect": 1, + } + # PATCH replaces the full fields array, so send the existing fields + # plus the new one appended. + patched_fields = list(inv.get("fields", [])) + [new_field] + # Strip PB-echoed-only keys that should not be re-sent verbatim. + clean_fields = [] + for f in patched_fields: + clean_fields.append({k: v for k, v in f.items() if k != "options"}) + resp, err = pb_patch( + f"/api/collections/{invoices_id}", + {"fields": clean_fields}, + ) + if err is not None: + sys.exit(f"ERROR: failed to add payment_batch field: {err}") + print(f"ADDED: JEI_invoices.payment_batch relation -> {batches_id}") + + # 4. Wake-up round-trip: PATCH each collection's rule fields. Even though + # the values are already empty strings, the round-trip activates the + # collection's rule machinery in PB v0.39.8. + for cid in (batches_id, invoices_id): + _, err = pb_patch(f"/api/collections/{cid}", dict(SUPERUSER_RULES)) + if err is not None: + sys.exit(f"ERROR: wake-up PATCH failed for {cid}: {err}") + print(f"PATCHED: {cid} (wake-up round-trip)") + + print("DONE: schema ready.") + + +if __name__ == "__main__": + main()