feat(pos): migrate CFDI timbrado from Horux to Facturapi
- 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
This commit is contained in:
96
scripts/apply_facturapi_to_all_tenants.py
Executable file
96
scripts/apply_facturapi_to_all_tenants.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user