91 lines
3.5 KiB
Python
91 lines
3.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Create dated employee hourly-rate history without changing worker records.
|
||
|
|
|
||
|
|
Run: python3 scripts/pb-add-employee-rates.py
|
||
|
|
Credentials are loaded by pb_env from .env.local or the environment.
|
||
|
|
No rates are seeded: the previous/new payroll list must supply actual values.
|
||
|
|
Existing compatible collections are verified and left untouched.
|
||
|
|
"""
|
||
|
|
import sys
|
||
|
|
|
||
|
|
from pb_env import http, superuser_token
|
||
|
|
|
||
|
|
|
||
|
|
def definition(employees):
|
||
|
|
return {
|
||
|
|
"name": "EMPLOYEE_RATES",
|
||
|
|
"type": "base",
|
||
|
|
"fields": [
|
||
|
|
{"name": "employee", "type": "relation", "required": True,
|
||
|
|
"collectionId": employees["id"], "maxSelect": 1,
|
||
|
|
"cascadeDelete": False},
|
||
|
|
{"name": "effective_from", "type": "date", "required": True},
|
||
|
|
{"name": "rate_per_hour", "type": "number", "required": True,
|
||
|
|
"min": 0},
|
||
|
|
{"name": "notes", "type": "text", "required": False},
|
||
|
|
{"name": "created", "type": "autodate", "onCreate": True,
|
||
|
|
"onUpdate": False},
|
||
|
|
{"name": "updated", "type": "autodate", "onCreate": True,
|
||
|
|
"onUpdate": True},
|
||
|
|
],
|
||
|
|
"indexes": [
|
||
|
|
"CREATE UNIQUE INDEX `idx_employee_rates_employee_day` "
|
||
|
|
"ON `EMPLOYEE_RATES` (`employee`, substr(`effective_from`, 1, 10))",
|
||
|
|
"CREATE INDEX `idx_employee_rates_employee_date` "
|
||
|
|
"ON `EMPLOYEE_RATES` (`employee`, `effective_from`)",
|
||
|
|
],
|
||
|
|
# Inherit employee read visibility; all rate writes require superuser.
|
||
|
|
"listRule": employees.get("listRule"),
|
||
|
|
"viewRule": employees.get("viewRule"),
|
||
|
|
"createRule": None,
|
||
|
|
"updateRule": None,
|
||
|
|
"deleteRule": None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def verify(actual, expected):
|
||
|
|
fields = {f["name"]: f for f in actual.get("fields", [])}
|
||
|
|
for wanted in expected["fields"]:
|
||
|
|
got = fields.get(wanted["name"], {})
|
||
|
|
for key, value in wanted.items():
|
||
|
|
if got.get(key) != value:
|
||
|
|
raise RuntimeError(f"Incompatible field: {wanted['name']}.{key}")
|
||
|
|
for index in expected["indexes"]:
|
||
|
|
if index not in actual.get("indexes", []):
|
||
|
|
raise RuntimeError("Missing expected rate-history index")
|
||
|
|
for rule in ("listRule", "viewRule", "createRule", "updateRule", "deleteRule"):
|
||
|
|
if actual.get(rule) != expected[rule]:
|
||
|
|
raise RuntimeError(f"Incompatible access rule: {rule}")
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
token = superuser_token()
|
||
|
|
code, employees = http("GET", "/api/collections/employees", token=token)
|
||
|
|
if code != 200:
|
||
|
|
raise RuntimeError(f"Cannot read employees schema: HTTP {code}")
|
||
|
|
expected = definition(employees)
|
||
|
|
code, existing = http("GET", "/api/collections/employee_rates", token=token)
|
||
|
|
if code == 404:
|
||
|
|
code, created = http("POST", "/api/collections", expected, token)
|
||
|
|
if code != 200:
|
||
|
|
raise RuntimeError(f"Create failed: HTTP {code}: {created}")
|
||
|
|
print(f"Created EMPLOYEE_RATES: {created['id']}")
|
||
|
|
elif code == 200:
|
||
|
|
verify(existing, expected)
|
||
|
|
print("EMPLOYEE_RATES already exists; no changes made")
|
||
|
|
else:
|
||
|
|
raise RuntimeError(f"Cannot inspect employee_rates: HTTP {code}")
|
||
|
|
|
||
|
|
code, actual = http("GET", "/api/collections/employee_rates", token=token)
|
||
|
|
if code != 200:
|
||
|
|
raise RuntimeError(f"Cannot verify new schema: HTTP {code}")
|
||
|
|
verify(actual, expected)
|
||
|
|
print("Verified fields, indexes and access rules. No employee or rate records changed.")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
main()
|
||
|
|
except Exception as exc:
|
||
|
|
sys.exit(str(exc))
|