Six telecom platforms. 4.5 million subscribers. Weekly billing discrepancies surfaced by an automated pipeline built on BAT, FTP, Oracle SQL and SQL Server — before they became silent revenue losses.
WOM Chile was growing fast — new subscribers, new rate plans, constant network expansion across BSCS, HSS, ICC, SYMSOFT, DWH and PCRF. Each platform maintained its own subscriber records. None of them talked to each other automatically.
A subscriber active in the network (HSS) but not billed correctly (BSCS) is silent revenue leakage. Without a cross-platform view, the only way to find those mismatches was to notice them — usually too late, usually by accident.
The problem wasn't missing data. It was no process that connected it weekly.
Platform discrepancies discovered ad hoc, weeks after the fact. No systematic view across BSCS · HSS · ICC · SYMSOFT · PCRF. Escalation by email when someone noticed an anomaly.
Weekly automated run. 1 hour of execution: FTP extraction, parsing, SQL Server load, stored-procedure cross-join, dashboard view. Discrepancies routed to Backend and IT Monitoring teams with context.
The pipeline ran from a single Windows BAT file (SCRIPT_CONCILIACION_PARQUE.bat), which was the conductor. It handled FTP via WinSCP to pull compressed dumps from HSS and PCRF, decompressed them with 7-zip, parsed them using gawk + sed (the Unix tools running on Windows), then fired Oracle SQL*Plus to extract BSCS and Netezza SQL to extract DWH traffic data. All of it landed in SQL Server, where stored procedures ran the cross-platform joins and built the reconciliation view.
The insight buried in the design: this was not a database problem — it was a runtime integration problem. No single database could talk to all six sources. The BAT layer was the ETL.
LK_PLN_PLAN. Defines what billing should see.SPOOL dump. Source of most discrepancies.BT_TRAFICO_SMSC_SYMSOFT. Validated against active subscriber park.-- PARQUE_BSCS.sql — Oracle SQL*Plus
-- Extracts all active subscribers from BSCS billing system.
-- CH_STATUS != 'd' → excludes deactivated contracts.
-- CS_DEACTIV_DATE IS NULL → only lines with no deactivation date (active).
-- Output: pipe-delimited flat file for SQL Server import.
WHENEVER SQLERROR EXIT SQL.SQLCODE
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYYMMDD';
SET colsep '|';
SPOOL D:\03_DUMP\BSCS\DUMP_BSCS.txt;
SELECT
CA.CUSTOMER_ID -- unique customer
, CA.CUSTCODE
, CA.CSCOMPTAXNO -- RUT / tax ID
, CNT.CO_ID -- contract ID
, CNT.CO_ACTIVATED
, CNT.TMCODE -- rate plan code (joins to 658-plan catalog)
, CNT.RATEPLAN_CHANGE_DATE
, CNT.TMCODE_OLD
, CNT.CO_CODE
, CNT.CH_STATUS
, CSC.DN_ID
, TO_CHAR(CSC.CS_ACTIV_DATE, 'YYYYMMDD')
, TO_CHAR(CSC.CS_DEACTIV_DATE, 'YYYYMMDD')
, DN.DN_NUM -- MSISDN / phone number
, BCY.BILLCYCLE
, CNT.CH_STATUS_VALIDFROM
FROM CUSTOMER_ALL CA
LEFT JOIN CONTRACT_ALL CNT ON CA.CUSTOMER_ID = CNT.CUSTOMER_ID
LEFT JOIN CONTR_SERVICES_CAP CSC ON CNT.CO_ID = CSC.CO_ID
LEFT JOIN DIRECTORY_NUMBER DN ON CSC.DN_ID = DN.DN_ID
LEFT JOIN CUSTOMER_BCH BCY ON CA.CUSTCODE = BCY.CUSTCODE
WHERE
TO_CHAR(CSC.CS_DEACTIV_DATE, 'YYYYMMDD') IS NULL -- no deactivation date = still active
AND CNT.CH_STATUS != 'd'; -- exclude deleted contracts
SPOOL OFF;-- DUMP_DWH — Netezza SQL
-- Pulls yesterday's subscriber park from DWH with full plan classification.
-- Joins to LK_PLN_PLAN for plan type, market segment, commercial objective.
-- This is the source of truth for what BSCS and HSS should reflect.
SELECT
A.NUMERO_ABONADO,
A.ESTADO_CONTRATO,
A.ID_PLAN,
B.DESC_CORTA_PLAN,
CASE
WHEN B.ID_TIPO_PLAN = 3 THEN 'UNLIMITED'
WHEN B.ID_TIPO_PLAN = 5 THEN 'Prepago'
WHEN B.ID_TIPO_PLAN = 6 THEN 'Controlado'
ELSE 'NA'
END AS TIPO_PLAN,
CASE
WHEN B.ID_MERCADO_PLAN = 2 THEN 'Consumer'
WHEN B.ID_MERCADO_PLAN = 5 THEN 'Business'
WHEN B.ID_MERCADO_PLAN = 4 THEN 'Prepago'
ELSE 'NA'
END AS MERCADO,
A.ICCID, A.IMSI, A.FECHA_ALTA
FROM
nz_prod.DWH_WOM.BT_CLE_PARQUE A
LEFT JOIN NZ_PROD.DWH_WOM.LK_PLN_PLAN B ON A.ID_PLAN = B.ID_PLAN
WHERE
A.PERIODO_PARQUE = TO_CHAR(NOW()-1, 'YYYYMM')
AND A.estado_contrato != 'd';Every week the reconciliation produced one number that mattered: the delta between what each platform believed was active. HSS — the network authentication layer — showed only 22 records diverging from DWH. That meant provisioning was nearly clean: subscribers were correctly registered in the network.
BSCS — the billing system — showed 30,513 records diverging. Subscribers active in the network, correctly in DWH, but with billing records that didn't align: wrong plan codes, missing contracts, status mismatches. Every one of those records was a line item that billing couldn't process correctly.
The network provisioned correctly. The billing system didn't follow. Without the cross-platform view, these 30,513 records would have been invisible — appearing as "active" in every platform audit, while silently failing to generate correct billing.
This is a conservative reference estimate, not a recovered revenue figure. The actual monetary impact depended on plan mix, billing cycle timing and whether records were corrected within the same period. What the pipeline guaranteed was that the discrepancy was found weekly — not quarterly.
Beyond the static park reconciliation, the pipeline joined 30 days of voice, data, SMS and VAS traffic to the subscriber base. A record showing a billing mismatch becomes much more actionable when you can also say: this line had 340 minutes of voice and 2.8GB of data this month — it's not dormant, it's being used without being correctly billed.
AGG_TRF_VOZ_MED_ABONADO. Minutes of voice per subscriber, last traffic date. FULL JOIN with data traffic for unified view.AGG_TRF_DATOS_ABONADO (KB_DOWN + KB_UP / 1024). Aggregated per subscriber, last active date.BT_TRAFICO_SMSC_SYMSOFT. State: 'Delivered' or 'Delivered direct'. Hit count + last delivery date.BT_TRAFICO_SHRTC. Net amount extracted via SUBSTR + INSTR position parsing on ADDITIONAL_ORIGCHARGEINFO.-- TRAFICO_NZ — Netezza SQL (NZ_PROD)
-- FULL JOIN voice + data so a subscriber with ONLY voice or ONLY data is not lost.
-- DISTINCT + CASE WHEN handles the NULL from either side of the join.
SELECT DISTINCT
CASE WHEN A.NUMERO_ABONADO IS NULL
THEN B.NUMERO_ABONADO
ELSE A.NUMERO_ABONADO
END AS NUM_ABONADO,
SUM(A.CANTIDAD_VOZ) MIN_VOZ,
A.ULTIMO_TRAFICO_VOZ,
SUM(B.CANTIDAD_MB) MB_DATOS,
B.ULTIMO_TRAFICO_DATOS
FROM
(SELECT
Z.NUMERO_ABONADO,
SUM(CAST(Z.DURACION AS INT) / 60) CANTIDAD_VOZ,
MAX(Z.ID_DWH_DIA) ULTIMO_TRAFICO_VOZ
FROM NZ_PROD.STG_TRAF.AGG_TRF_VOZ_MED_ABONADO Z
WHERE Z.ID_DWH_DIA >= TO_CHAR(NOW()-30, 'YYYYMMDD')
GROUP BY Z.NUMERO_ABONADO) A
FULL JOIN
(SELECT
X.NUMERO_ABONADO,
ROUND(SUM((X.KB_DOWN_IX + X.KB_UP_IX) / 1024), 2) CANTIDAD_MB,
MAX(X.ID_DWH_DIA) ULTIMO_TRAFICO_DATOS
FROM NZ_PROD.STG_TRAF.AGG_TRF_DATOS_ABONADO X
WHERE X.ID_DWH_DIA >= TO_CHAR(NOW()-30, 'YYYYMMDD')
GROUP BY X.NUMERO_ABONADO) B
ON A.NUMERO_ABONADO = B.NUMERO_ABONADO
GROUP BY NUM_ABONADO, A.ULTIMO_TRAFICO_VOZ, B.ULTIMO_TRAFICO_DATOS;.bat file sequencing WinSCP → 7-zip → gawk/sed → SQL*Plus → sqlcmd was the right tool: auditable, runnable by anyone, no dependencies to install.
SPOOL to |-delimited text, then BULK INSERT in SQL Server, was the only format that crossed both runtimes reliably.
"A 30,000-record billing gap doesn't announce itself. Someone has to build the system that looks for it every week."
Revenue assurance pipelines for telecom, AI adoption analytics for aviation — different domains, same discipline: build the instrumentation before you need it.
Contact Javier