- Add Facturapi REST service (invoices, customers, orgs, cancel, downloads) - Add JSON payload builder for ingreso/egreso/pago/global invoices - Replace XML queue with Facturapi JSON queue (payload_unsigned, external_id) - Update invoicing blueprint with Facturapi config and download endpoints - Update global invoice service to use Facturapi payloads - Add migration v4.3_facturapi.sql and tenant rollout script - Update invoicing UI: payload preview, PDF/XML downloads, PAC status panel - Add FACTURAPI_USER_KEY to .env.example
97 lines
2.8 KiB
Python
Executable File
97 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()
|