""" Audit Firm ERP -- UAT seed extension for Playwright v2.5.1 Deep SQLite suite. Purpose: Extend the existing core UAT seed with supporting records for the new v2.5.1 deep tests: email integration, marketplace leads, platform billing, RBAC, audit logs, document security, HR, billing rules, notice-case rules and financial-year locking. Run inside ERP container, AFTER the core seed: cd /app PYTHONPATH=/app python scripts/seed_uat_data.py PYTHONPATH=/app python scripts/seed_uat_data_v2_5_1.py Safe/idempotent for UAT: it inserts or updates only UAT-coded records where the matching tables/columns exist. If a module table is not present in this ERP build, the script skips that area and prints a warning instead of failing. Do NOT run against live production data. """ from __future__ import annotations import hashlib import json import os import sys from datetime import date, datetime, timedelta, timezone UAT_DOMAIN = os.getenv("UAT_EMAIL_DOMAIN", "vavalam.com") ACTIVE_FY = os.getenv("ACTIVE_FY", "2025-26") PREVIOUS_FY = os.getenv("PREVIOUS_FY", "2024-25") ASSESSMENT_YEAR = os.getenv("ASSESSMENT_YEAR", "2026-27") LSA_SECRET = os.getenv("UAT_LSA_SECRET", "uat-local-storage-agent-secret") SMTP_PASSWORD = os.getenv("UAT_SMTP_PASSWORD", "Pass@123##") def fail(msg: str) -> None: print(f"\n[v2.5.1 seed] ERROR: {msg}", file=sys.stderr) sys.exit(1) def utcnow() -> datetime: return datetime.now(timezone.utc) def today() -> date: return date.today() def main() -> None: try: from sqlalchemy import MetaData, and_, inspect, select, update from app.core.db.common import CommonSessionLocal except Exception as exc: fail( "Could not import ERP DB modules. Run this from ERP project root inside the app container. " f"Underlying import error: {exc!r}" ) db = CommonSessionLocal() engine = db.get_bind() meta = MetaData() meta.reflect(bind=engine) inspector = inspect(engine) table_names = set(inspector.get_table_names()) report: list[str] = [] ids: dict[str, object] = {} def norm(name: str) -> str: return name.lower().replace("_", "").replace("-", "") def find_table(*candidates: str): # exact match first for name in candidates: if name in meta.tables: return meta.tables[name] # normalized exact match normalized = {norm(t): t for t in table_names} for name in candidates: hit = normalized.get(norm(name)) if hit: return meta.tables[hit] # token contains fallback for name in candidates: tokens = [tok for tok in name.lower().replace("-", "_").split("_") if tok] for t in table_names: low = t.lower() if all(tok in low for tok in tokens): return meta.tables[t] return None def valid(table, data: dict) -> dict: if table is None: return {} cols = set(table.c.keys()) return {k: v for k, v in data.items() if k in cols} def one(table, lookup: dict): if table is None: return None data = valid(table, lookup) if not data: return None stmt = select(table).where(and_(*[table.c[k] == v for k, v in data.items()])) return db.execute(stmt).mappings().first() def first(table, **lookup): return one(table, lookup) def first_by_any(table, lookups: list[dict]): for lookup in lookups: row = one(table, lookup) if row: return row return None def upsert(table, lookup: dict, defaults: dict | None = None, label: str = "record"): if table is None: report.append(f"SKIP {label}: table missing") return None lookup_v = valid(table, lookup) if not lookup_v: report.append(f"SKIP {label}: no matching lookup columns in {table.name}") return None defaults_v = valid(table, defaults or {}) row = one(table, lookup_v) if row: if defaults_v: db.execute(update(table).where(and_(*[table.c[k] == v for k, v in lookup_v.items()])).values(**defaults_v)) db.flush() return one(table, lookup_v) data = {**lookup_v, **defaults_v} result = db.execute(table.insert().values(**data)) db.flush() pk = result.inserted_primary_key[0] if result.inserted_primary_key else None if pk and "id" in table.c: return one(table, {"id": pk}) return one(table, lookup_v) def update_existing(table, lookup: dict, values: dict, label: str): if table is None: report.append(f"SKIP {label}: table missing") return None lookup_v = valid(table, lookup) values_v = valid(table, values) if not lookup_v or not values_v: report.append(f"SKIP {label}: no matching columns in {table.name}") return None db.execute(update(table).where(and_(*[table.c[k] == v for k, v in lookup_v.items()])).values(**values_v)) db.flush() return one(table, lookup_v) def val(row, key: str, default=None): return row[key] if row and key in row else default # Core table references from earlier seed tenants = find_table("tenants", "tenant") branches = find_table("branches", "branch") users = find_table("users", "user") clients = find_table("clients", "client") employees = find_table("employees", "employee") financial_years = find_table("financial_years", "financial_year") documents = find_table("engagement_documents", "documents", "client_documents") document_versions = find_table("engagement_document_versions", "document_versions") storage_nodes = find_table("branch_storage_nodes", "storage_nodes") storage_jobs = find_table("document_storage_jobs", "storage_jobs") notice_cases = find_table("notice_cases", "cases") subscriptions = find_table("client_service_subscriptions", "subscriptions", "engagements") task_instances = find_table("client_service_task_instances", "task_instances", "tasks") services = find_table("service_catalogue", "service_catalogues", "services") roles = find_table("roles", "rbac_roles") permissions = find_table("permissions", "rbac_permissions") role_permissions = find_table("role_permissions", "rbac_role_permissions") tenant_a = first_by_any(tenants, [{"code": "UAT-A"}, {"tenant_code": "UAT-A"}, {"name": "UAT Tenant A"}]) tenant_b = first_by_any(tenants, [{"code": "UAT-B"}, {"tenant_code": "UAT-B"}, {"name": "UAT Tenant B"}]) branch_a = first_by_any(branches, [{"code": "UAT-BA"}, {"branch_code": "UAT-BA"}, {"name": "UAT Branch A"}]) branch_b = first_by_any(branches, [{"code": "UAT-BB"}, {"branch_code": "UAT-BB"}, {"name": "UAT Branch B"}]) firm_admin = first(users, email=f"uat.firmadmin@{UAT_DOMAIN}") system_admin = first(users, email=f"uat.admin@{UAT_DOMAIN}") partner = first(users, email=f"uat.partner@{UAT_DOMAIN}") manager = first(users, email=f"uat.manager@{UAT_DOMAIN}") staff = first(users, email=f"uat.staff@{UAT_DOMAIN}") staff2 = first(users, email=f"uat.staff2@{UAT_DOMAIN}") client_user = first(users, email=f"uat.client@{UAT_DOMAIN}") consultant = first(users, email=f"uat.consultant@{UAT_DOMAIN}") client_a = first_by_any(clients, [{"client_code": "UAT-CL-A"}, {"code": "UAT-CL-A"}, {"client_name": "UAT Client A Pvt Ltd"}]) client_b = first_by_any(clients, [{"client_code": "UAT-CL-B"}, {"code": "UAT-CL-B"}, {"client_name": "UAT Client B Pvt Ltd"}]) service_a = first_by_any(services, [{"service_code": "GST-GSTR3B-M"}, {"code": "GST-GSTR3B-M"}, {"service_name": "GSTR-3B Monthly Filing"}]) sub_a = first_by_any(subscriptions, [{"financial_year": ACTIVE_FY, "client_id": val(client_a, "id")}, {"client_id": val(client_a, "id")}]) task_a = first_by_any(task_instances, [{"client_id": val(client_a, "id")}, {"assigned_to_user_id": val(staff, "id")}]) if not tenant_a or not branch_a or not firm_admin: fail("Core UAT seed appears missing. Run seed_uat_data.py first.") tenant_a_id = val(tenant_a, "id") tenant_b_id = val(tenant_b, "id") branch_a_id = val(branch_a, "id") branch_b_id = val(branch_b, "id") firm_admin_id = val(firm_admin, "id") system_admin_id = val(system_admin, "id", firm_admin_id) partner_id = val(partner, "id", firm_admin_id) manager_id = val(manager, "id", firm_admin_id) staff_id = val(staff, "id", firm_admin_id) staff2_id = val(staff2, "id", staff_id) client_user_id = val(client_user, "id", firm_admin_id) consultant_id = val(consultant, "id", firm_admin_id) client_a_id = val(client_a, "id") client_b_id = val(client_b, "id") service_a_id = val(service_a, "id") sub_a_id = val(sub_a, "id") task_a_id = val(task_a, "id") common_scope = { "tenant_id": tenant_a_id, "branch_id": branch_a_id, "created_by_user_id": firm_admin_id, "updated_by_user_id": firm_admin_id, "created_by_id": firm_admin_id, "updated_by_id": firm_admin_id, "created_at": utcnow(), "updated_at": utcnow(), "is_active": True, } # 1. Email integration seed email_settings = find_table("email_settings", "firm_email_settings", "smtp_settings", "mail_settings") smtp = upsert(email_settings, {"tenant_id": tenant_a_id, "setting_code": "UAT-SMTP-A"}, { **common_scope, "name": "UAT SMTP Settings", "smtp_host": "mail.vavalam.com", "smtp_port": 587, "smtp_username": "no-reply@vavalam.com", "smtp_password": SMTP_PASSWORD, "from_email": "no-reply@vavalam.com", "from_name": "ARRR ERP UAT", "use_tls": True, "imap_host": "mail.vavalam.com", "imap_port": 993, "imap_username": f"uat.firmadmin@{UAT_DOMAIN}", "imap_password": SMTP_PASSWORD, "status": "active", "is_default": True, }, "email settings") ids["EMAIL_SETTINGS_ID"] = val(smtp, "id") email_templates = find_table("email_templates", "mail_templates") for code, subject, body in [ ("UAT_OTP", "Your ERP OTP", "Your OTP is {{ otp }}"), ("UAT_INVITE", "ERP Invitation", "Please accept invitation: {{ invite_url }}"), ("UAT_NOTICE", "Notice update", "Notice {{ reference_no }} requires attention"), ]: upsert(email_templates, {"tenant_id": tenant_a_id, "template_code": code}, { **common_scope, "name": code.replace("_", " "), "subject": subject, "body": body, "body_html": body, "template_type": "system", "is_active": True, }, f"email template {code}") email_queue = find_table("email_queue", "queued_emails", "mail_queue") upsert(email_queue, {"tenant_id": tenant_a_id, "queue_code": "UAT-EMAIL-Q-001"}, { **common_scope, "to_email": f"uat.client@{UAT_DOMAIN}", "from_email": "no-reply@vavalam.com", "subject": "UAT queued email", "body": "Queued email for UAT", "status": "pending", "priority": 5, "scheduled_at": utcnow(), "attempts": 0, }, "email queue") email_logs = find_table("email_logs", "mail_logs", "email_delivery_logs") upsert(email_logs, {"tenant_id": tenant_a_id, "message_id": "UAT-EMAIL-LOG-001"}, { **common_scope, "to_email": f"uat.client@{UAT_DOMAIN}", "from_email": "no-reply@vavalam.com", "subject": "UAT delivered email", "status": "sent", "sent_at": utcnow(), "provider_response": "UAT seeded", }, "email log") inbox = find_table("email_inbox_messages", "email_inbox", "inbound_emails", "mail_inbox") upsert(inbox, {"tenant_id": tenant_a_id, "message_id": "UAT-INBOX-001"}, { **common_scope, "from_email": f"uat.client@{UAT_DOMAIN}", "to_email": "support@vavalam.com", "subject": "UAT client document submission", "body": "Please map this email to the UAT client.", "received_at": utcnow(), "status": "unmapped", "client_id": client_a_id, }, "email inbox") # 2. Marketplace / public lead seed leads = find_table("marketplace_leads", "leads", "public_leads") lead1 = upsert(leads, {"tenant_id": tenant_a_id, "lead_code": "UAT-LEAD-001"}, { **common_scope, "source": "public_form", "lead_type": "compliance", "name": "UAT Marketplace Lead", "company_name": "UAT Lead Pvt Ltd", "contact_name": "UAT Lead Contact", "email": "lead@example.com", "mobile": "9000000101", "service_interest": "GST", "status": "new", "assigned_to_user_id": manager_id, "notes": "Seeded marketplace lead for v2.5.1 tests", }, "marketplace lead new") lead2 = upsert(leads, {"tenant_id": tenant_a_id, "lead_code": "UAT-LEAD-CONVERTED"}, { **common_scope, "source": "referral", "name": "UAT Converted Lead", "email": "converted@example.com", "mobile": "9000000102", "status": "converted", "assigned_to_user_id": partner_id, "converted_client_id": client_a_id, }, "marketplace lead converted") ids["MARKETPLACE_LEAD_ID"] = val(lead1, "id") # 3. Platform billing seed plans = find_table("platform_billing_plans", "billing_plans", "plans") plan = upsert(plans, {"plan_code": "UAT-PLAN-PRO"}, { "name": "UAT Professional Plan", "description": "Seed plan for platform billing tests", "monthly_price": 9999, "annual_price": 99990, "currency": "INR", "max_users": 25, "max_clients": 500, "is_active": True, "created_at": utcnow(), "updated_at": utcnow(), }, "platform plan") accounts = find_table("platform_billing_accounts", "billing_accounts", "accounts") account = upsert(accounts, {"tenant_id": tenant_a_id, "account_code": "UAT-PLAT-ACC-A"}, { **common_scope, "account_name": "UAT Tenant A Billing Account", "billing_email": "support@vavalam.com", "gstin": "33AABCU1111A1Z5", "status": "active", }, "platform billing account") plat_subs = find_table("platform_subscriptions", "platform_billing_subscriptions", "audit_firm_subscriptions", "subscriptions") plat_sub = upsert(plat_subs, {"tenant_id": tenant_a_id, "subscription_code": "UAT-PLAT-SUB-A"}, { **common_scope, "account_id": val(account, "id"), "plan_id": val(plan, "id"), "status": "active", "start_date": today(), "end_date": today() + timedelta(days=365), "billing_cycle": "monthly", }, "platform subscription") plat_invoices = find_table("platform_invoices", "platform_billing_invoices", "invoices") p_inv = upsert(plat_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-PLAT-INV-001"}, { **common_scope, "account_id": val(account, "id"), "subscription_id": val(plat_sub, "id"), "invoice_date": today(), "due_date": today() + timedelta(days=15), "status": "issued", "subtotal": 9999, "tax_amount": 1799.82, "total_amount": 11798.82, "balance_amount": 11798.82, }, "platform invoice") payments = find_table("platform_payments", "invoice_payments", "payments") upsert(payments, {"tenant_id": tenant_a_id, "payment_ref": "UAT-PLAT-PAY-001"}, { **common_scope, "invoice_id": val(p_inv, "id"), "payment_date": today(), "amount": 5000, "payment_mode": "bank_transfer", "status": "posted", "remarks": "Seed partial platform payment", }, "platform payment") ids["PLATFORM_INVOICE_ID"] = val(p_inv, "id") # 4. RBAC seed perm_codes = [ "uat.clients.read", "uat.documents.read", "uat.billing.read", "uat.email.manage", "uat.marketplace.manage", ] for code in perm_codes: upsert(permissions, {"code": code}, { "name": code, "description": f"UAT permission {code}", "module": "UAT", "is_active": True, "created_at": utcnow(), "updated_at": utcnow(), }, f"permission {code}") role = upsert(roles, {"name": "UAT Limited Tester"}, { "description": "Seeded limited role for RBAC tests", "is_system": False, "is_active": True, "tenant_id": tenant_a_id, "created_at": utcnow(), "updated_at": utcnow(), }, "rbac role") if role_permissions is not None and role is not None: for code in perm_codes[:2]: perm = first(permissions, code=code) if permissions is not None else None upsert(role_permissions, {"role_id": val(role, "id"), "permission_id": val(perm, "id")}, { "created_at": utcnow(), "created_by_user_id": firm_admin_id, }, f"role permission {code}") # 5. Audit logs seed audit_logs = find_table("audit_logs", "activity_logs", "system_audit_logs") for code, action, entity in [ ("UAT-AUD-LOGIN-FAIL", "login_failed", "User"), ("UAT-AUD-DOC-DOWNLOAD", "document_download", "EngagementDocument"), ("UAT-AUD-PERM-DENIED", "permission_denied", "RBAC"), ]: upsert(audit_logs, {"tenant_id": tenant_a_id, "event_code": code}, { **common_scope, "user_id": firm_admin_id, "action": action, "event_type": action, "entity_type": entity, "entity_id": client_a_id or 1, "ip_address": "127.0.0.1", "user_agent": "UAT seed", "details": json.dumps({"seed": True, "no_secret": True}), "message": f"Seed audit event {action}", }, f"audit log {code}") # 6. Financial-year states update_existing(financial_years, {"tenant_id": tenant_a_id, "year_code": PREVIOUS_FY}, { "is_locked": True, "locked_at": utcnow(), "locked_by_user_id": firm_admin_id, "lock_reason": "UAT locked FY for v2.5.1 tests", }, "previous FY lock") update_existing(financial_years, {"tenant_id": tenant_a_id, "year_code": ACTIVE_FY}, { "is_locked": False, "is_current": True, }, "active FY current/unlocked") backups = find_table("financial_year_backups", "fy_backups", "backup_exports") fy = first(financial_years, tenant_id=tenant_a_id, year_code=PREVIOUS_FY) backup = upsert(backups, {"tenant_id": tenant_a_id, "backup_code": "UAT-FY-BACKUP-001"}, { **common_scope, "financial_year_id": val(fy, "id"), "year_code": PREVIOUS_FY, "status": "completed", "file_name": "uat-fy-backup-001.zip", "file_size_bytes": 1024, "created_by_user_id": firm_admin_id, }, "FY backup") ids["YEAR_BACKUP_EXPORT_ID"] = val(backup, "id") ids["LOCKED_FY"] = PREVIOUS_FY # 7. Document security/deep seed secret_hash = hashlib.sha256(LSA_SECRET.encode()).hexdigest() node = upsert(storage_nodes, {"tenant_id": tenant_a_id, "branch_id": branch_a_id, "node_code": "UAT-LSA-SEC-A"}, { **common_scope, "node_name": "UAT Security Storage Agent", "secret_key_hash": secret_hash, "storage_root_path": "D:/UAT/Security", "storage_mode": "pull_jobs", "status": "active", "quota_limit_bytes": 1024 * 1024 * 1024, "used_storage_bytes": 0, }, "security storage node") ids["STORAGE_NODE_SECURITY_ID"] = val(node, "id") doc_sec = upsert(documents, {"tenant_id": tenant_a_id, "document_code": "UAT-DOC-SEC-001"}, { **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "engagement_id": sub_a_id, "task_instance_id": task_a_id, "financial_year": ACTIVE_FY, "assessment_year": ASSESSMENT_YEAR, "document_type": "SECURITY", "title": "UAT Client Visible Document", "description": "Client visible UAT doc", "current_version_no": 2, "status": "active", "is_deleted": False, "is_client_visible": True, }, "client visible document") ids["CLIENT_VISIBLE_DOCUMENT_ID"] = val(doc_sec, "id") for version, filename in [(1, "uat-sec-v1.txt"), (2, "uat-sec-v2.txt")]: content = f"UAT secure version {version}\n".encode() upsert(document_versions, {"document_id": val(doc_sec, "id"), "version_no": version}, { **common_scope, "tenant_id": tenant_a_id, "branch_id": branch_a_id, "client_id": client_a_id, "engagement_id": sub_a_id, "original_filename": filename, "stored_filename": filename, "content_type": "text/plain", "file_size_bytes": len(content), "file_hash_sha256": hashlib.sha256(content).hexdigest(), "storage_backend": "LOCAL_YEAR_WISE", "local_relative_path": f"UAT-A/UAT-CL-A/{ACTIVE_FY}/{filename}", "storage_status": "stored", "uploaded_by_user_id": staff_id, }, f"document version {version}") doc_deleted = upsert(documents, {"tenant_id": tenant_a_id, "document_code": "UAT-DOC-DELETED-001"}, { **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "engagement_id": sub_a_id, "financial_year": ACTIVE_FY, "document_type": "SECURITY", "title": "UAT Deleted Document", "status": "deleted", "is_deleted": True, "deleted_at": utcnow(), "deleted_by_user_id": firm_admin_id, }, "deleted document") ids["DELETED_DOCUMENT_ID"] = val(doc_deleted, "id") storage_job = upsert(storage_jobs, {"tenant_id": tenant_a_id, "job_code": "UAT-STORAGE-JOB-SEC-001"}, { **common_scope, "storage_node_id": val(node, "id"), "document_id": val(doc_sec, "id"), "job_type": "store_version", "status": "pending", "priority": 1, "staging_relative_path": "staging/uat-sec-v2.txt", "target_relative_path": f"UAT-A/UAT-CL-A/{ACTIVE_FY}/uat-sec-v2.txt", "file_size_bytes": 21, "expected_hash_sha256": hashlib.sha256(b"UAT secure version 2\n").hexdigest(), "attempts": 0, }, "storage job security") ids["STORAGE_JOB_SECURITY_ID"] = val(storage_job, "id") # 8. HR seed attendance = find_table("employee_attendance", "attendance", "employee_attendances") upsert(attendance, {"tenant_id": tenant_a_id, "employee_id": val(first(employees, user_id=staff_id), "id"), "attendance_date": today()}, { **common_scope, "user_id": staff_id, "status": "present", "check_in_time": utcnow(), "source": "seed", "remarks": "Seed attendance for duplicate/manager tests", }, "attendance") leave_balances = find_table("employee_leave_balances", "leave_balances") leave_balance = upsert(leave_balances, {"tenant_id": tenant_a_id, "user_id": staff_id, "leave_type": "casual", "financial_year": ACTIVE_FY}, { **common_scope, "employee_id": val(first(employees, user_id=staff_id), "id"), "opening_balance": 12, "availed": 2, "balance": 10, "is_active": True, }, "leave balance") leave_requests = find_table("employee_leave_requests", "leave_requests", "leaves") leave_req = upsert(leave_requests, {"tenant_id": tenant_a_id, "leave_code": "UAT-LEAVE-PENDING-001"}, { **common_scope, "user_id": staff_id, "employee_id": val(first(employees, user_id=staff_id), "id"), "leave_type": "casual", "from_date": today() + timedelta(days=5), "to_date": today() + timedelta(days=6), "days": 2, "status": "pending", "reason": "UAT seeded pending leave", "approver_user_id": manager_id, "balance_id": val(leave_balance, "id"), }, "leave request pending") ids["PENDING_LEAVE_ID"] = val(leave_req, "id") payroll_runs = find_table("payroll_runs", "employee_payroll_runs", "payroll") payroll = upsert(payroll_runs, {"tenant_id": tenant_a_id, "run_code": "UAT-PAYROLL-001"}, { **common_scope, "financial_year": ACTIVE_FY, "period_month": today().month, "period_year": today().year, "status": "generated", "gross_amount": 50000, "deduction_amount": 5000, "net_amount": 45000, }, "payroll run") payslips = find_table("payslips", "employee_payslips") upsert(payslips, {"tenant_id": tenant_a_id, "payslip_code": "UAT-PAYSLIP-001"}, { **common_scope, "payroll_run_id": val(payroll, "id"), "employee_id": val(first(employees, user_id=staff_id), "id"), "user_id": staff_id, "gross_amount": 50000, "deduction_amount": 5000, "net_amount": 45000, "status": "generated", "file_name": "uat-payslip-001.pdf", }, "payslip") # 9. Firm billing business seed firm_invoices = find_table("billing_invoices", "firm_invoices", "client_invoices", "invoices") draft_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-DRAFT-001"}, { **common_scope, "client_id": client_a_id, "invoice_date": today(), "due_date": today() + timedelta(days=15), "status": "draft", "subtotal": 10000, "tax_amount": 1800, "total_amount": 11800, "balance_amount": 11800, "financial_year": ACTIVE_FY, }, "billing draft invoice") issued_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-ISSUED-001"}, { **common_scope, "client_id": client_a_id, "invoice_date": today(), "due_date": today() + timedelta(days=15), "status": "issued", "subtotal": 20000, "tax_amount": 3600, "total_amount": 23600, "balance_amount": 18600, "financial_year": ACTIVE_FY, }, "billing issued invoice") cancel_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-CANCELLED-001"}, { **common_scope, "client_id": client_a_id, "invoice_date": today(), "status": "cancelled", "subtotal": 1000, "tax_amount": 180, "total_amount": 1180, "balance_amount": 0, "financial_year": ACTIVE_FY, }, "billing cancelled invoice") firm_payments = find_table("billing_payments", "client_payments", "invoice_payments", "payments") upsert(firm_payments, {"tenant_id": tenant_a_id, "payment_ref": "UAT-BILL-PAY-001"}, { **common_scope, "invoice_id": val(issued_inv, "id"), "client_id": client_a_id, "payment_date": today(), "amount": 5000, "payment_mode": "upi", "status": "posted", }, "billing partial payment") ids["BILLING_DRAFT_INVOICE_ID"] = val(draft_inv, "id") ids["BILLING_ISSUED_INVOICE_ID"] = val(issued_inv, "id") ids["BILLING_CANCELLED_INVOICE_ID"] = val(cancel_inv, "id") # 10. Notice case business seed if notice_cases is not None: nc = upsert(notice_cases, {"tenant_id": tenant_a_id, "case_code": "UAT-NC-DEEP-001"}, { **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "reference_no": "UAT-NOTICE-DEEP-001", "department": "GST", "case_type": "Notice", "title": "UAT Deep GST Notice", "status": "Hearing Scheduled", "notice_date": today() - timedelta(days=10), "hearing_date": today() + timedelta(days=10), "order_date": None, "due_date": today() + timedelta(days=20), "assigned_to_user_id": staff_id, "partner_user_id": partner_id, "internal_notes": "Internal note must not be client-visible", "client_visible_notes": "Client visible notice update", "issue_summary": "Seeded v2.5.1 notice case for business-rule testing", }, "deep notice case") ids["NOTICE_CASE_DEEP_ID"] = val(nc, "id") nc_events = find_table("notice_case_events", "case_events", "notice_case_activities") upsert(nc_events, {"tenant_id": tenant_a_id, "event_code": "UAT-NC-EVT-001"}, { **common_scope, "case_id": val(nc, "id"), "event_type": "hearing_scheduled", "event_date": today(), "notes": "Seeded hearing event", "is_client_visible": True, "created_by_user_id": staff_id, }, "notice event") db.commit() print("\n[v2.5.1 seed] Completed seed extension.") print("[v2.5.1 seed] Created/updated data for email, marketplace, platform billing, RBAC, audit, FY, documents, HR, billing and notice-case tests.") print("\nUseful IDs for Playwright .env if needed:\n") for k, v in ids.items(): if v is not None: print(f"{k}={v}") print("\nSkipped/missing-module notes:") for line in report: print(f"- {line}") print("\nIf many rows were skipped, that means the corresponding ERP module/table is not present yet; tests for those modules should then fail/skip as ERP gaps, not seed failures.") db.close() if __name__ == "__main__": try: main() except Exception: raise