#!/usr/bin/env python3 """Create Facturapi organizations for tenants that don't have one. Requires FACTURAPI_USER_KEY environment variable (user key with permission to manage organizations). Usage: export MASTER_DB_URL=postgresql://user:pass@host/nexus_autoparts export TENANT_DB_URL_TEMPLATE="postgresql://user:pass@host/{db_name}" export FACTURAPI_USER_KEY=sk_user_xxxxxxxxxxxxxxxx python3 scripts/setup_facturapi_orgs.py # Preview only python3 scripts/setup_facturapi_orgs.py --dry-run """ import argparse import os import sys POS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "pos") sys.path.insert(0, POS_DIR) import psycopg2 # noqa: E402 from services import facturapi_service # noqa: E402 def get_tenants(master_dsn: str): conn = psycopg2.connect(master_dsn) try: cur = conn.cursor() cur.execute( """ SELECT t.id, t.db_name, t.name, t.subdomain FROM tenants t WHERE t.is_active = true ORDER BY t.id """ ) rows = cur.fetchall() cur.close() return rows finally: conn.close() def get_tenant_fiscal_data(db_name: str, template_dsn: str) -> dict: dsn = template_dsn.format(db_name=db_name) conn = psycopg2.connect(dsn) try: cur = conn.cursor() cur.execute( """ SELECT key, value FROM tenant_config WHERE key IN ('tenant_rfc', 'tenant_razon_social', 'tenant_cp', 'cfdi_regimen_fiscal') """ ) config = {row[0]: row[1] or "" for row in cur.fetchall()} cur.execute( """ SELECT rfc, razon_social, regimen_fiscal, codigo_postal FROM branches WHERE is_main = true LIMIT 1 """ ) branch = cur.fetchone() if branch: config["rfc"] = (branch[0] or config.get("tenant_rfc", "")).strip() config["razon_social"] = (branch[1] or config.get("tenant_razon_social", "")).strip() config["regimen_fiscal"] = (branch[2] or config.get("cfdi_regimen_fiscal", "")).strip() config["cp"] = (branch[3] or config.get("tenant_cp", "")).strip() else: config["rfc"] = config.get("tenant_rfc", "").strip() config["razon_social"] = config.get("tenant_razon_social", "").strip() config["regimen_fiscal"] = config.get("cfdi_regimen_fiscal", "").strip() config["cp"] = config.get("tenant_cp", "").strip() # Existing facturapi keys cur.execute( """ SELECT key, value FROM tenant_config WHERE key IN ('cfdi_facturapi_org_id', 'cfdi_facturapi_key') """ ) for key, value in cur.fetchall(): config[key] = value or "" cur.close() return config finally: conn.close() def save_facturapi_config(db_name: str, template_dsn: str, org_id: str, api_key: str): dsn = template_dsn.format(db_name=db_name) conn = psycopg2.connect(dsn) try: cur = conn.cursor() cur.execute( """ INSERT INTO tenant_config (key, value) VALUES ('cfdi_facturapi_org_id', %s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value """, (org_id,), ) cur.execute( """ INSERT INTO tenant_config (key, value) VALUES ('cfdi_facturapi_key', %s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value """, (api_key,), ) conn.commit() cur.close() finally: conn.close() def main(): parser = argparse.ArgumentParser(description="Create Facturapi organizations for tenants") parser.add_argument("--dry-run", action="store_true", help="Preview changes without applying") args = parser.parse_args() master_dsn = os.environ.get("MASTER_DB_URL") template_dsn = os.environ.get("TENANT_DB_URL_TEMPLATE") user_key = os.environ.get("FACTURAPI_USER_KEY") if not master_dsn or not template_dsn: print("Set MASTER_DB_URL and TENANT_DB_URL_TEMPLATE", file=sys.stderr) sys.exit(1) if not user_key: print("Set FACTURAPI_USER_KEY (user key required to manage organizations)", file=sys.stderr) sys.exit(1) tenants = get_tenants(master_dsn) if not tenants: print("No active tenants found.") return created = 0 skipped = 0 errors = 0 for tenant_id, db_name, name, _subdomain in tenants: print(f"\n[{tenant_id}] {name} ({db_name})") try: config = get_tenant_fiscal_data(db_name, template_dsn) if config.get("cfdi_facturapi_org_id") and config.get("cfdi_facturapi_key"): print(" SKIP: already has org_id and key") skipped += 1 continue if not config.get("rfc"): print(" ERROR: tenant RFC not configured") errors += 1 continue print(f" RFC: {config['rfc']}") print(f" Razon social: {config['razon_social'] or '(will use RFC)'}") if args.dry_run: print(" DRY-RUN: would create organization") continue result = facturapi_service.create_organization(config) save_facturapi_config(db_name, template_dsn, result["org_id"], result["api_key"]) print(f" CREATED: org_id={result['org_id']}") created += 1 except Exception as e: print(f" ERROR: {type(e).__name__}: {str(e)[:200]}") errors += 1 print(f"\nDone. Created: {created} | Skipped: {skipped} | Errors: {errors}") if __name__ == "__main__": main()