- Normalize Facturapi key/org_id resolution (supports both cfdi_ prefixed tenant_config keys and short names used by invoicing_bp). - Add CSD upload end-to-end (backend + frontend). - Add helper scripts: setup_facturapi_orgs.py and check_facturapi_tenants.py. - Add 20 unit tests with mocks (pos/tests/test_facturapi_service.py). - Add CI workflow for lint + console tests on Python 3.11/3.13. - Add pyproject.toml and requirements-dev.txt with ruff/pytest config. - Update FASES_IMPLEMENTADAS.md with FASE 8 documentation. Tests: 81 passing (61 console + 20 Facturapi).
96 lines
2.8 KiB
Python
Executable File
96 lines
2.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Apply Facturapi configuration to all active tenant databases.
|
|
|
|
Reads FACTURAPI_SECRET_KEY from the environment and stores it in each
|
|
tenant's tenant_config table as cfdi_facturapi_key. Also runs the
|
|
v4.3_facturapi.sql migration against every tenant DB.
|
|
|
|
Usage:
|
|
export MASTER_DB_URL=postgresql://...
|
|
export TENANT_DB_URL_TEMPLATE="postgresql://.../{db_name}"
|
|
export FACTURAPI_SECRET_KEY=sk_user_xxxxxxxxxxxxxxxx
|
|
python3 scripts/apply_facturapi_to_all_tenants.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import psycopg2
|
|
|
|
MIGRATION_SQL = """
|
|
ALTER TABLE cfdi_queue RENAME COLUMN xml_unsigned TO payload_unsigned;
|
|
|
|
COMMENT ON COLUMN cfdi_queue.payload_unsigned IS 'Facturapi JSON payload (previously unsigned XML for Horux)';
|
|
COMMENT ON COLUMN cfdi_queue.xml_signed IS 'Signed+stamped XML returned by Facturapi';
|
|
|
|
ALTER TABLE cfdi_queue ADD COLUMN IF NOT EXISTS external_id VARCHAR(64);
|
|
COMMENT ON COLUMN cfdi_queue.external_id IS 'Facturapi invoice id';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cfdi_queue_external_id ON cfdi_queue(external_id);
|
|
|
|
INSERT INTO tenant_config (key, value)
|
|
VALUES
|
|
('cfdi_facturapi_key', ''),
|
|
('cfdi_facturapi_org_id', ''),
|
|
('cfdi_facturapi_customer_sync', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
UPDATE tenant_config SET value = %s WHERE key = 'cfdi_facturapi_key';
|
|
"""
|
|
|
|
|
|
def get_tenant_db_names(master_dsn):
|
|
conn = psycopg2.connect(master_dsn)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id, db_name FROM tenants WHERE is_active = true ORDER BY id")
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return rows
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def apply_to_tenant(tenant_id, db_name, template_dsn, api_key):
|
|
dsn = template_dsn.format(db_name=db_name)
|
|
conn = psycopg2.connect(dsn)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute(MIGRATION_SQL, (api_key,))
|
|
conn.commit()
|
|
cur.close()
|
|
print(f"[OK] tenant {tenant_id} ({db_name})")
|
|
except Exception as e:
|
|
print(f"[ERROR] tenant {tenant_id} ({db_name}): {e}")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def main():
|
|
master_dsn = os.environ.get("MASTER_DB_URL")
|
|
template_dsn = os.environ.get("TENANT_DB_URL_TEMPLATE")
|
|
api_key = os.environ.get("FACTURAPI_SECRET_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 api_key:
|
|
print("Set FACTURAPI_SECRET_KEY", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
tenants = get_tenant_db_names(master_dsn)
|
|
if not tenants:
|
|
print("No active tenants found.")
|
|
return
|
|
|
|
print(f"Applying Facturapi config to {len(tenants)} tenant(s)...")
|
|
for tenant_id, db_name in tenants:
|
|
apply_to_tenant(tenant_id, db_name, template_dsn, api_key)
|
|
|
|
print("Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|