dodil/erp-brick-contracts

tests/seed.py

"""The standard paper every test and scenario starts from — written THROUGH THE DOORS (use cases), never as rows:
the gb pack, our legal name and default signatory, a small clause library (Preferred / Alternate / Fallback) and three
approved templates (nda-std, freelance-std, employment-std). Invented wording: a customer's own is its
configuration, kept by its own session.

`post(path, body)` is whatever reaches the contract routes — the brick's own app (paths as is) or a composition (paths
under /contract) — called as contract_admin; `approve(path, body)` as ANOTHER admin (an author may not approve their
own template). Each raises on any refusal.
"""
OUR_NAME = "Acme Studio Ltd"
DIRECTOR = {"full_name": "Dana Director", "email": "director.1@scenario.test", "title": "Director",
            "authority_basis": "board resolution of 1 May 2026", "signing_order": 2}

CLAUSES = [
    ("conf_std", "confidentiality", "PREFERRED", 0, "Confidentiality",
     "Each party keeps the other's confidential information confidential, during this agreement and for three years "
     "after it ends."),
    ("liability_std", "liability_cap", "PREFERRED", 0, "Liability",
     "Each party's total liability under this agreement is capped at the fees paid in the twelve months before the "
     "claim."),
    ("liability_alt", "liability_cap", "ALTERNATE", 0, "Liability",
     "Each party's total liability under this agreement is capped at the fees paid under it."),
    ("liability_fb", "liability_cap", "FALLBACK", 1, "Liability",
     "Each party's total liability under this agreement is capped at twice the fees paid in the twelve months before "
     "the claim."),
    ("services_std", "services", "PREFERRED", 0, "Services",
     "{{counterparty_name}} will provide {{services}} from {{start_date}} to {{end_date}} at a rate of {{rate_amount}} "
     "per {{rate_basis}}."),
    ("ip_full", "ip_assignment", "PREFERRED", 0, "Intellectual property",
     "You assign to us, with full title guarantee, all copyright and other rights in everything you make for us under "
     "this agreement, on creation."),
    ("ip_on_payment", "ip_assignment", "FALLBACK", 1, "Intellectual property",
     "You assign to us all copyright and other rights in everything you make for us under this agreement, on payment "
     "of the fees for it."),
    ("psc_warranty", "psc", "PREFERRED", 0, "Your company",
     "You provide the services through {{intermediary_name}}, which warrants that it is responsible for the tax and "
     "national insurance due on what it pays you."),
    ("notice_std", "notice", "PREFERRED", 0, "Ending this agreement",
     "Either party may end this agreement by giving {{notice_days}} days' written notice."),
    ("employment_terms", "employment", "PREFERRED", 0, "Your employment",
     "{{our_name}} employs {{legal_name}} as {{job_title}} from {{start_date}} (continuous employment from "
     "{{continuous_employment_date}}). Pay: {{rate_amount}} per {{rate_basis}}, paid monthly. Hours: {{hours}}. "
     "Holiday: {{holiday_entitlement}}. Place of work: {{place_of_work}}. Duration: {{duration}}. Probation: "
     "{{probation}}. Benefits: {{benefits}}. Training: {{training}}. Sick pay: {{sick_pay}}. Other paid leave: "
     "{{other_paid_leave}}. Notice: {{notice}}."),
]

MONEY_BAND = {"min": "300", "max": "600", "currency": "GBP"}


def nda_template() -> dict:
    return {"template_id": "nda-std", "type_code": "nda", "name": "Mutual NDA",
            "slots": [{"slot_no": 1, "family": "confidentiality", "clause_id": "conf_std", "clause_version": 1},
                      {"slot_no": 2, "family": "liability_cap", "clause_id": "liability_std", "clause_version": 1}],
            "variables": [{"name": "counterparty_name", "type": "text", "source": "COUNTERPARTY"},
                          {"name": "start_date", "type": "date", "source": "DATES", "validation": {"term": "start"}},
                          {"name": "end_date", "type": "date", "source": "DATES", "required": False,
                           "validation": {"term": "end"}},
                          {"name": "notice_days", "type": "int", "source": "CALLER", "required": False,
                           "validation": {"term": "notice_days"}}]}


def freelance_template(template_id="freelance-std", type_code="freelance_agreement") -> dict:
    return {"template_id": template_id, "type_code": type_code, "name": "Freelance services agreement",
            "slots": [{"slot_no": 1, "family": "services", "clause_id": "services_std", "clause_version": 1},
                      {"slot_no": 2, "family": "ip_assignment", "clause_id": "ip_full", "clause_version": 1},
                      {"slot_no": 3, "family": "liability_cap", "clause_id": "liability_std", "clause_version": 1},
                      {"slot_no": 4, "family": "psc", "kind": "optional", "clause_id": "psc_warranty",
                       "clause_version": 1, "condition": {"var": "engagement_via", "eq": "PSC"}},
                      {"slot_no": 5, "family": "notice", "clause_id": "notice_std", "clause_version": 1}],
            "variables": [
                {"name": "counterparty_name", "type": "text", "source": "COUNTERPARTY"},
                {"name": "services", "type": "text", "source": "ENGAGEMENT",
                 "default_value": "the creative services each statement of work describes"},
                {"name": "start_date", "type": "date", "source": "DATES", "validation": {"term": "start"}},
                {"name": "end_date", "type": "date", "source": "DATES", "validation": {"term": "end"}},
                {"name": "rate_amount", "type": "rate", "source": "PAY", "validation": MONEY_BAND, "label": "day rate"},
                {"name": "currency", "type": "text", "source": "PAY", "default_value": "GBP"},
                {"name": "rate_basis", "type": "enum", "source": "PAY", "enum_values": ["DAY", "HOUR", "FIXED"],
                 "aliases": {"day": "DAY", "hour": "HOUR", "fixed": "FIXED"}, "default_value": "DAY"},
                {"name": "notice_days", "type": "int", "source": "CALLER", "default_value": "30",
                 "validation": {"term": "notice_days"}},
                {"name": "engagement_via", "type": "enum", "source": "ENGAGEMENT", "required": False,
                 "enum_values": ["SOLE_TRADER", "PSC", "UMBRELLA", "AGENCY"]},
                {"name": "intermediary_name", "type": "text", "source": "ENGAGEMENT", "required": True,
                 "condition": {"var": "engagement_via", "eq": "PSC"}},
                {"name": "client_size_regime", "type": "enum", "source": "CALLER", "required": False,
                 "enum_values": ["SMALL", "MEDIUM_LARGE", "PUBLIC"],
                 "aliases": {"small": "SMALL", "medium": "MEDIUM_LARGE", "large": "MEDIUM_LARGE", "public": "PUBLIC"}},
                {"name": "ir35_determination", "type": "enum", "source": "ENGAGEMENT", "required": False,
                 "enum_values": ["INSIDE", "OUTSIDE", "NOT_CLIENT_DECISION"]},
                {"name": "sds_issued_on", "type": "date", "source": "ENGAGEMENT", "required": False},
                {"name": "sds_reference", "type": "text", "source": "ENGAGEMENT", "required": False}]}


ERA_DEFAULTS = {"hours": "37.5 hours a week, Monday to Friday", "holiday_entitlement":
                "28 days a year including bank holidays", "duration": "permanent", "probation": "three months",
                "benefits": "none beyond those the law requires", "training": "none required",
                "sick_pay": "statutory sick pay", "other_paid_leave": "as the law provides",
                "notice": "one week for each year of service, up to twelve weeks"}


def employment_template(template_id="employment-std", type_code="employment_contract", defaults=None) -> dict:
    d = ERA_DEFAULTS if defaults is None else defaults
    plain = [{"name": n, "type": "text", "source": "TEMPLATE", **({"default_value": d[n]} if n in d else {})}
             for n in ("hours", "holiday_entitlement", "duration", "probation", "benefits", "training", "sick_pay",
                       "other_paid_leave", "notice")]
    return {"template_id": template_id, "type_code": type_code, "name": "Employment contract",
            "slots": [{"slot_no": 1, "family": "employment", "clause_id": "employment_terms", "clause_version": 1},
                      {"slot_no": 2, "family": "confidentiality", "clause_id": "conf_std", "clause_version": 1}],
            "variables": [
                {"name": "legal_name", "type": "text", "source": "COUNTERPARTY", "validation": {"bp_field": "legal_name"}},
                {"name": "job_title", "type": "text", "source": "ENGAGEMENT"},
                {"name": "start_date", "type": "date", "source": "DATES", "validation": {"term": "start"}},
                {"name": "continuous_employment_date", "type": "date", "source": "DATES",
                 "validation": {"default_from": "start_date"}},
                {"name": "rate_amount", "type": "rate", "source": "PAY", "label": "pay"},
                {"name": "currency", "type": "text", "source": "PAY", "default_value": "GBP"},
                {"name": "rate_basis", "type": "enum", "source": "PAY", "enum_values": ["YEAR", "MONTH", "DAY", "HOUR"],
                 "aliases": {"year": "YEAR", "month": "MONTH", "day": "DAY", "hour": "HOUR"}},
                {"name": "location_name", "type": "text", "source": "ENGAGEMENT", "required": False},
                {"name": "place_of_work", "type": "text", "source": "ENGAGEMENT",
                 "validation": {"default_from": "location_name"}, "default_value": "our London office"}] + plain}


def seed(post, approve, prefix: str = "") -> None:
    def ok(fn, path, body):
        status, out = fn(prefix + path, body)
        assert 200 <= status < 300, (path, status, out)
        return out
    ok(post, "/localisation-packs/gb", {})
    ok(post, "/settings/setting", {"rows": [{"key": "our_legal_name", "value": OUR_NAME},
                                            {"key": "mail_from", "value": "contracts@acme.example"},
                                            {"key": "default_signer", "value": __import__("json").dumps(DIRECTOR)}]})
    for cid, fam, pos, lvl, title, body in CLAUSES:
        ok(post, "/clauses", {"clause_id": cid, "family": fam, "position": pos, "deviation_level": lvl,
                              "title": title, "body": body})
    for tmpl in (nda_template(), freelance_template(), employment_template()):
        out = ok(post, "/templates", tmpl)
        ok(approve, f"/templates/{tmpl['template_id']}/versions/{out['version']}/approve", {})