diff --git a/scratchpad/gen_docs.py b/scratchpad/gen_docs.py new file mode 100644 index 00000000..3a4c60e5 --- /dev/null +++ b/scratchpad/gen_docs.py @@ -0,0 +1,313 @@ +"""Genera la carpeta de documentacion de linaje en el Escritorio de Windows. + +A partir del grafo trazado (scratchpad/lineage_graph.json) escribe: + 00_INDICE.txt resumen + mapa de capas + tabla de todos los objetos + 01_marts/.txt una por vista de customer_marts: que es + arbol de linaje + SQL + 02_intermedio_clientes_intel/*.txt tablas base del pipeline de inteligencia de clientes + 03_producto/*.txt cadena de catalogo de producto (vistas con SQL + bases) + 04_fuentes/*.txt tablas fuente (replica Postgres, Navision, imagenes, tasas) + +Todos los .txt se escriben con CRLF para abrirse limpios en Bloc de notas de Windows. +""" +import json +import os +import textwrap + +DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts" +DATA = json.load(open("scratchpad/lineage_graph.json")) +G = DATA["graph"] +PROJECT = DATA["project"] + +# --------------------------------------------------------------------------- +# Descripciones ("que es") por objeto. La SQL/DDL incluida en cada archivo es la +# fuente de verdad; estas lineas son un resumen para orientar al lector. +# --------------------------------------------------------------------------- +DESC = { + # ---- customer_marts (marts finales, grano = persona_id / cliente) ---- + "customer_marts.customer_profile": + "Ficha maestra 360 del cliente: identidad + features agregadas + score CLV + segmento. Vista de perfil que consolida todo lo demas.", + "customer_marts.customer_monetary": + "Metricas monetarias del cliente (gasto total, ticket medio, recencia/frecuencia/valor). Componente M del RFM.", + "customer_marts.customer_channel": + "Canal del cliente: canal preferido transaccional, mix aurgi/motortown/web/servicio, canal de entrada (canal8) y fuentes de origen.", + "customer_marts.customer_contactability": + "Contactabilidad del cliente: disponibilidad de email/telefono y consentimientos, a partir de la dimension persona + features + segmento.", + "customer_marts.customer_category_spend": + "Gasto del cliente desglosado por categoria de producto, a partir de la tabla de hechos de transaccion.", + "customer_marts.customer_brand_affinity": + "Afinidad de marca del cliente: que marcas compra y con que peso, cruzando transacciones con el catalogo de producto (Objeto_productos).", + "customer_marts.customer_product": + "Productos comprados por el cliente (detalle de que ha adquirido) desde la tabla de hechos de transaccion.", + "customer_marts.customer_store_spend": + "Gasto del cliente por centro/tienda desde la tabla de hechos de transaccion.", + "customer_marts.customer_temporal": + "Patrones temporales de compra del cliente (estacionalidad, recencia, frecuencia) desde transacciones + features.", + "customer_marts.customer_vehicles": + "Vehiculos asociados al cliente: dimension vehiculo + features de vehiculo + mapping N:N persona-vehiculo.", + "customer_marts.customer_payment_method": + "Metodo de pago del cliente reconstruido desde los pedidos TPV (orders/invoice/payment/payment_types).", + "customer_marts.customer_promo_usage": + "Uso de promociones/descuentos por el cliente (pedidos con descuento) desde transacciones + pedidos TPV + segmento.", + "customer_marts.customer_promo_tolerance": + "Tolerancia del cliente a promociones: respuesta a campanas + sensibilidad a descuentos en pedidos.", + "customer_marts.customer_predictive": + "Senales predictivas del cliente: score CLV, proxima mejor accion (recomendaciones) y segmento.", + + # ---- clientes_intel (capa intermedia; tablas base del pipeline de inteligencia de clientes) ---- + "clientes_intel.dim_persona": + "Dimension PERSONA: identidad de cliente consolidada (una fila por persona_id). Nucleo de la doble identidad persona+vehiculo.", + "clientes_intel.dim_vehiculo": + "Dimension VEHICULO: una fila por vehiculo (matricula/bastidor) con sus atributos.", + "clientes_intel.fact_transaccion": + "Tabla de HECHOS de transaccion: linea/venta por cliente. Base de casi todos los marts monetarios y de producto.", + "clientes_intel.fact_campana_respuesta": + "Tabla de HECHOS de respuesta a campanas de marketing (envio/apertura/conversion) por cliente.", + "clientes_intel.feat_cliente_persona": + "Features agregadas a nivel PERSONA (RFM, mix de canal, indicadores derivados). Alimenta perfil, monetary, channel, temporal, contactability.", + "clientes_intel.feat_cliente_vehiculo": + "Features agregadas a nivel VEHICULO. Alimenta customer_vehicles.", + "clientes_intel.seg_cliente_360": + "Segmentacion 360 del cliente (segmentos de negocio / clusters). Alimenta perfil, channel, contactability, predictive, promo_usage.", + "clientes_intel.score_clv": + "Score de valor de vida del cliente (CLV). Alimenta perfil y predictive.", + "clientes_intel.reco_acciones": + "Recomendaciones / proxima mejor accion (NBA) por cliente. Alimenta customer_predictive.", + "clientes_intel.map_persona_canal8": + "Mapeo persona -> canal8 (canal de entrada). Puente para customer_channel.", + "clientes_intel.map_persona_fuente": + "Mapeo persona -> fuente(s) de origen (de que sistema/canal proviene el cliente). Puente para customer_channel.", + "clientes_intel.map_persona_vehiculo": + "Mapeo N:N persona <-> vehiculo. Puente para customer_vehicles.", + + # ---- cadena de catalogo de producto ---- + "anjana_bi_datamart.Objeto_productos": + "Vista maestra de PRODUCTO: catalogo Navision + categorias CGQ + imagenes + tasa/margen por material. Se usa para afinidad de marca.", + "anjana_bi_datamart.Cruce_16_07_cgq": + "Tabla de cruce de categorias CGQ (categoria/subcategoria/tipo) usada por Objeto_productos.", + "claude_bi.productos_tasa_mat": + "Tabla de tasa/margen por material de producto. La consume Objeto_productos.", + "external_datasets.product_object_images": + "Imagenes de producto (imagen principal/secundaria). Dataset externo. La consume Objeto_productos.", + "stg_anjana_bi.producto": + "Staging de producto: cruza item de Navision con equivalencias de matriculas (SAF). Capa de preparacion sobre las tablas de SQL Server.", + + # ---- fuentes base ---- + "psql_dcpublic.products": + "Catalogo de productos. Replica en BigQuery de la BBDD Postgres ANJANA (DCPublic).", + "psql_dcpublic.product_categories": + "Categorias de producto. Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.product_groups": + "Grupos de producto. Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.tpv_orders_order": + "Pedidos TPV (cabecera de pedido). Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.tpv_orders_orderitem": + "Lineas de pedido TPV. Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.tpv_orders_invoice": + "Facturas TPV. Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.tpv_orders_payment": + "Pagos de pedidos TPV. Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.tpv_payment_types": + "Tipos de pago TPV (catalogo). Replica Postgres ANJANA (DCPublic).", + "mssql2022_dbo.item": + "Catalogo de articulos de Navision (SQL Server 2022, esquema dbo).", + "mssql2022_dbo.equivalencias_matriculas_saf": + "Equivalencias de matriculas (SAF) en Navision (SQL Server 2022, esquema dbo).", +} + +TYPE_ES = { + "VIEW": "VISTA (tiene SQL propio)", + "MATERIALIZED VIEW": "VISTA MATERIALIZADA (tiene SQL propio)", + "BASE TABLE": "TABLA BASE (datos materializados; sin SQL de definicion, solo esquema)", + "EXTERNAL": "TABLA EXTERNA", + "UNKNOWN": "DESCONOCIDO", +} + +# Carpeta destino por objeto. +def folder_of(key: str) -> str: + ds = key.split(".", 1)[0] + if ds == "customer_marts": + return "01_marts" + if ds == "clientes_intel": + return "02_intermedio_clientes_intel" + if ds in ("anjana_bi_datamart", "claude_bi", "external_datasets", "stg_anjana_bi"): + return "03_producto" + return "04_fuentes" + +def fname_of(key: str) -> str: + return key.replace(".", "__") + ".txt" + +def relpath_of(key: str) -> str: + return f"{folder_of(key)}/{fname_of(key)}" + +def desc_of(key: str) -> str: + return DESC.get(key, "(sin descripcion)") + +# --------------------------------------------------------------------------- +# Arbol de linaje recursivo (para los marts). +# --------------------------------------------------------------------------- +def render_tree(key: str, prefix: str | None = None, is_last: bool = True, seen=None) -> list[str]: + if seen is None: + seen = set() + tag = {"VIEW": "[vista]", "MATERIALIZED VIEW": "[vista mat]", + "BASE TABLE": "[TABLA BASE/FUENTE]", "EXTERNAL": "[externa]", + "UNKNOWN": "[?]"}.get(G.get(key, {"type": "UNKNOWN"})["type"], "") + + if prefix is None: # raiz + lines = [f"{key} {tag}"] + child_prefix = "" + else: + connector = "└── " if is_last else "├── " + lines = [f"{prefix}{connector}{key} {tag}"] + child_prefix = prefix + (" " if is_last else "│ ") + + if key in seen: + lines[-1] += " (ya expandido arriba)" + return lines + seen.add(key) + refs = G.get(key, {"refs": []}).get("refs", []) + for i, r in enumerate(refs): + lines += render_tree(r, child_prefix, i == len(refs) - 1, seen) + return lines + +# --------------------------------------------------------------------------- +# Escritura. +# --------------------------------------------------------------------------- +def w(path: str, text: str): + full = os.path.join(DEST, path) + os.makedirs(os.path.dirname(full), exist_ok=True) + with open(full, "w", newline="\r\n", encoding="utf-8") as f: + f.write(text) + +SEP = "=" * 78 + "\n" + +def object_file(key: str, include_tree: bool) -> str: + node = G[key] + out = [] + out.append(SEP) + out.append(f"OBJETO : {PROJECT}.{key}\n") + out.append(f"TIPO : {TYPE_ES.get(node['type'], node['type'])}\n") + out.append(f"DATASET: {key.split('.',1)[0]}\n") + out.append(SEP) + out.append("\nQUE ES\n------\n") + out.append(textwrap.fill(desc_of(key), width=78) + "\n") + + if node.get("refs"): + out.append("\nDEPENDE DIRECTAMENTE DE\n-----------------------\n") + for r in node["refs"]: + out.append(f" - {PROJECT}.{r} -> ver {relpath_of(r)}\n") + + if include_tree: + out.append("\nLINAJE COMPLETO (hasta la fuente)\n---------------------------------\n") + out.append("\n".join(render_tree(key)) + "\n") + + out.append("\nSQL / DDL\n---------\n") + if node["type"] in ("VIEW", "MATERIALIZED VIEW"): + out.append("(Definicion de la vista. Este es el SQL que puedes copiar.)\n\n") + else: + out.append("(Tabla base: no tiene SQL de transformacion. Se incluye el CREATE TABLE\n" + " con el esquema de columnas para referencia.)\n\n") + out.append(node["ddl"].strip() + "\n") + return "".join(out) + +# Marts: incluir arbol de linaje. +marts = sorted(k for k in G if k.startswith("customer_marts.")) +for k in marts: + w(f"01_marts/{fname_of(k)}", object_file(k, include_tree=True)) + +# Resto de objetos: sin arbol (o arbol solo si es vista con dependencias). +for k in sorted(G): + if k.startswith("customer_marts."): + continue + include_tree = G[k]["type"] in ("VIEW", "MATERIALIZED VIEW") and bool(G[k].get("refs")) + w(relpath_of(k), object_file(k, include_tree=include_tree)) + +# --------------------------------------------------------------------------- +# INDICE. +# --------------------------------------------------------------------------- +idx = [] +idx.append(SEP) +idx.append("INDICE - LINAJE DEL DATASET customer_marts\n") +idx.append(f"Proyecto BigQuery: {PROJECT}\n") +idx.append(SEP) +idx.append(""" +QUE ES ESTA CARPETA +------------------- +Documenta, para cada tabla/vista del dataset `customer_marts`, de donde salen sus +datos: la cadena completa desde el mart final hasta las tablas fuente, con el SQL +de cada vista listo para copiar y compartir. + +Cada objeto tiene su propio .txt con: + - QUE ES (resumen de una linea; la SQL es la fuente de verdad) + - DE QUE DEPENDE (dependencias directas, con la ruta a su archivo) + - LINAJE COMPLETO (arbol hasta la fuente) -- solo en los marts y vistas + - SQL / DDL (el codigo: definicion de la vista, o el esquema si es tabla base) + +MAPA DE CAPAS +------------- + customer_marts (VISTAS finales, grano = cliente/persona_id) + | + v + clientes_intel (TABLAS BASE: capa intermedia construida por el pipeline de + | inteligencia de clientes -- dim_*, feat_*, seg_*, score_*, + | reco_*, fact_*, map_*) + v + Fuentes: + - psql_dcpublic.* Replica en BigQuery de la BBDD Postgres ANJANA (TPV + catalogo) + - anjana_bi_datamart / claude_bi / external_datasets / stg_anjana_bi + Cadena de catalogo de PRODUCTO (Objeto_productos y sus fuentes) + - mssql2022_dbo.* Navision (SQL Server 2022, esquema dbo) + +NOTA: las tablas de `clientes_intel` son TABLAS BASE: no son vistas, sino tablas que +un pipeline reconstruye cada dia con sentencias CREATE TABLE AS SELECT (CTAS). Su +esquema esta en 02_intermedio_clientes_intel/. El SQL REAL que las construye (y que +baja hasta TPV / customers / users / Navision / Salesforce) esta en la carpeta +05_construccion_clientes_intel/ -- ver tambien 00b_FUENTES_DE_CLIENTE.txt. + +""") + +idx.append(SEP) +idx.append("CARPETAS\n") +idx.append(SEP) +idx.append(""" + 01_marts/ Las 14 vistas de customer_marts (con arbol de linaje) + 02_intermedio_clientes_intel/ Las 12 tablas base intermedias (esquema) + 03_producto/ Cadena de catalogo de producto (vistas + bases) + 04_fuentes/ Tablas fuente (replica Postgres, Navision, imagenes, tasas) + 05_construccion_clientes_intel/ El SQL (CTAS) que construye cada tabla de clientes_intel + 00b_FUENTES_DE_CLIENTE.txt Que consulta lee cada fuente de cliente (TPV/customers/ + users/Navision/Salesforce) + +""") + +def index_block(title, keys): + lines = [SEP, title + "\n", SEP, "\n"] + for k in keys: + t = {"VIEW": "vista", "MATERIALIZED VIEW": "vista_mat", "BASE TABLE": "tabla", + "EXTERNAL": "externa", "UNKNOWN": "?"}.get(G[k]["type"], "") + lines.append(f"[{t:9s}] {k}\n") + lines.append(f" {desc_of(k)}\n") + lines.append(f" archivo: {relpath_of(k)}\n\n") + return "".join(lines) + +idx.append(index_block("1) MARTS FINALES (customer_marts)", marts)) +idx.append(index_block("2) CAPA INTERMEDIA (clientes_intel)", + sorted(k for k in G if k.startswith("clientes_intel.")))) +idx.append(index_block("3) CADENA DE PRODUCTO", + sorted(k for k in G if folder_of(k) == "03_producto"))) +idx.append(index_block("4) FUENTES BASE", + sorted(k for k in G if folder_of(k) == "04_fuentes"))) + +w("00_INDICE.txt", "".join(idx)) + +# Conteo final +n_files = sum(len(files) for _, _, files in os.walk(DEST)) +print(f"Escrito en: {DEST}") +print(f"Archivos .txt generados: {n_files}") +print("Estructura:") +for root, dirs, files in sorted(os.walk(DEST)): + rel = os.path.relpath(root, DEST) + if rel == ".": + for f in sorted(files): + print(f" {f}") + else: + print(f" {rel}/ ({len(files)} archivos)") diff --git a/scratchpad/gen_intel.py b/scratchpad/gen_intel.py new file mode 100644 index 00000000..88e49b4c --- /dev/null +++ b/scratchpad/gen_intel.py @@ -0,0 +1,164 @@ +"""Genera 05_construccion_clientes_intel/ (SQL CTAS de cada tabla de clientes_intel) +y 00b_FUENTES_DE_CLIENTE.txt (mapa fuente-de-cliente -> consulta que la lee). + +Fuente de datos: scratchpad/intel_build.json (SQL de construccion capturado de +INFORMATION_SCHEMA.JOBS) y scratchpad/intel_lineage.json (tablas implicadas). +""" +import json +import os +import textwrap + +DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts" +PROJECT = "autingo-159109" +builds = json.load(open("scratchpad/intel_build.json")) +lin = json.load(open("scratchpad/intel_lineage.json")) + +# Tablas para las que escribimos el SQL de construccion: las del linaje de customer_marts +# + las que leen fuentes de cliente/Salesforce. +EXTRA = ["seg_vega_persona", "fact_campana_respuesta__sfnew"] +want = sorted(set(lin["intel_involved"]) | set(EXTRA)) +want = [t for t in want if t in builds] # solo las que tienen SQL capturado + +DESC = { + "_persona_records": + "IDENTIDAD DEL CLIENTE (nucleo). UNION de 7 fuentes -> normaliza DNI/NIE/CIF, email y " + "telefono -> resuelve persona_id (FARM_FINGERPRINT de persona_key) con nivel de confianza. " + "AQUI es donde se juntan TPV customers, customers web, OTR, Navision, citaprevia, users y " + "Salesforce contacts_latest.", + "dim_persona": + "Dimension PERSONA final: una fila por persona_id, elegida desde _persona_records " + "(prioriza el mejor registro por fuente/confianza) + banderas de contacto.", + "dim_vehiculo": + "Dimension VEHICULO: una fila por vehiculo (matricula/bastidor) desde TPV vehicles, OTR, " + "citaprevia matriculas y calibrado de ano de matricula.", + "map_persona_fuente": + "Mapeo persona -> fuente(s) de origen (tpv/web/otr/navision/citaprevia/users/salesforce). " + "Registra de que sistemas proviene cada persona.", + "map_persona_vehiculo": + "Mapeo N:N persona <-> vehiculo (quien conduce/posee que coche) desde OTR, TPV vehicleowner " + "y citaprevia matriculas.", + "map_persona_canal8": + "Mapeo persona -> canal8 (canal de entrada del cliente).", + "fact_transaccion": + "Tabla de HECHOS de transaccion (linea/venta por persona). Base de los marts monetarios.", + "fact_visita": + "Tabla de HECHOS de visita (visitas del cliente al taller/tienda).", + "fact_campana_respuesta": + "HECHOS de respuesta a campanas: cruza envios/aperturas/clics/sms de Salesforce con personas.", + "fact_campana_respuesta__sfnew": + "Variante de fact_campana_respuesta con el esquema nuevo de Salesforce (email_sent/opened/clicked/sms).", + "feat_cliente_persona": + "Features agregadas por PERSONA (RFM, mix de canal, ticket medio, margen, recencia...).", + "feat_cliente_vehiculo": + "Features agregadas por VEHICULO.", + "seg_cliente_360": + "Segmentacion 360 del cliente (segmentos/clusters de negocio).", + "seg_vega_persona": + "Segmentacion VEGA por persona (contactabilidad/valor); lee fuentes de cliente para calcular " + "disponibilidad de contacto.", + "seg_cluster_persona": + "Clustering de personas (asignacion de cluster) que alimenta la segmentacion.", + "reco_acciones": + "Recomendaciones / proxima mejor accion (NBA) por cliente.", + "data_points_contacto": + "Puntos de dato de contacto (email/telefono) consolidados y calidad por persona.", + "_margen_rate_producto": + "Tasa de margen por producto (auxiliar para features monetarias).", + "_plate_year_calib": + "Calibrado del ano a partir de la matricula (auxiliar para dim_vehiculo).", + "dim_cp_provincia": + "Diccionario codigo postal -> provincia/CCAA.", + "tipologia_cliente": + "Tipologia de cliente (clasificacion de negocio).", +} + +# Descripcion corta de cada fuente de cliente. +SRC_DESC = { + "psql_dcpublic.tpv_customers": "Clientes del TPV (mostrador). Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.customers": "Clientes web (e-commerce). Replica Postgres ANJANA (DCPublic).", + "psql_dcpublic.otr_customers": "Clientes de OTR (ordenes de reparacion/taller). Replica Postgres ANJANA.", + "psql_dcpublic.users": "Usuarios (cuentas). Replica Postgres ANJANA (DCPublic).", + "mssql2022_dbo.anjana_customer": "Cliente de NAVISION (SQL Server 2022, esquema dbo). Campos no_/e_mail/movil/name/post_code.", + "salesforce_ew1.contacts_latest": "Contactos de SALESFORCE (ultima version). Dataset en europe-west1.", + "salesforce_ew1.email_sent": "Envios de email de Salesforce (Marketing Cloud).", + "salesforce_ew1.email_opened": "Aperturas de email de Salesforce.", + "salesforce_ew1.email_clicked": "Clics de email de Salesforce.", + "salesforce_ew1.sms": "SMS de Salesforce.", + "citaprevia_aurphcp.clientes": "Clientes de CITA PREVIA (aurphcp).", + "citaprevia_aurphcp.clientes_matriculas": "Matriculas por cliente en cita previa.", + "psql_dcpublic.tpv_vehicles_vehicle": "Vehiculos del TPV. Replica Postgres ANJANA.", + "psql_dcpublic.tpv_vehicles_vehicleowner": "Propietarios de vehiculo del TPV (N:N). Replica Postgres ANJANA.", +} + +CUST_SOURCES = list(SRC_DESC.keys()) + +SEP = "=" * 78 + "\n" + +def w(path, text): + full = os.path.join(DEST, path) + os.makedirs(os.path.dirname(full), exist_ok=True) + with open(full, "w", newline="\r\n", encoding="utf-8") as f: + f.write(text) + +def build_file(tbl): + b = builds[tbl] + out = [SEP, f"OBJETO : {PROJECT}.clientes_intel.{tbl}\n", + f"TIPO : TABLA BASE construida por {b['stmt']} (se reconstruye periodicamente)\n", + f"ULTIMA EJECUCION CAPTURADA: {b['last_run']}\n", SEP, + "\nQUE ES\n------\n", + textwrap.fill(DESC.get(tbl, "(sin descripcion)"), width=78) + "\n"] + if b["refs"]: + out.append("\nLEE DE (tablas fuente / intermedias)\n------------------------------------\n") + for r in b["refs"]: + note = " << FUENTE DE CLIENTE" if r in SRC_DESC else "" + out.append(f" - {PROJECT}.{r}{note}\n") + out.append("\nSQL DE CONSTRUCCION (copiable)\n------------------------------\n\n") + out.append(b["query"].strip() + "\n") + return "".join(out) + +for t in want: + w(f"05_construccion_clientes_intel/{t}.txt", build_file(t)) + +# 00b_FUENTES_DE_CLIENTE.txt +f = [SEP, "FUENTES DE CLIENTE -> QUE CONSULTA DE clientes_intel LAS USA\n", SEP, + "\nResponde a: de donde salen los clientes (TPV, web, OTR, Navision, Salesforce, cita\n" + "previa) y en que consulta se juntan. El punto de union de identidades es\n" + "_persona_records (ver 05_construccion_clientes_intel/_persona_records.txt).\n\n"] + +f.append(SEP + "RESUMEN: LO QUE PEDISTE\n" + SEP + "\n") +mapping = [ + ("TPV customers", "psql_dcpublic.tpv_customers"), + ("customers (web)", "psql_dcpublic.customers"), + ("customers (OTR / taller)", "psql_dcpublic.otr_customers"), + ("users", "psql_dcpublic.users"), + ("customer de NAVISION", "mssql2022_dbo.anjana_customer"), + ("SALESFORCE (contactos)", "salesforce_ew1.contacts_latest"), +] +for label, src in mapping: + f.append(f" {label:26s} -> {PROJECT}.{src}\n") +f.append("\n SI: tenemos Salesforce. El dataset es `salesforce_ew1` (europe-west1):\n" + " contactos en contacts_latest; marketing en email_sent/opened/clicked y sms.\n\n") + +for src in CUST_SOURCES: + consumers = sorted(t for t, b in builds.items() if src in b["refs"]) + f.append(SEP) + f.append(f"{PROJECT}.{src}\n") + f.append(SEP) + f.append(f" {SRC_DESC[src]}\n") + f.append(" La leen estas tablas de clientes_intel (con su SQL en 05_construccion_...):\n") + if consumers: + for t in consumers: + star = " [SQL disponible]" if t in want else "" + f.append(f" - {t} ({builds[t]['stmt']}){star}\n") + else: + f.append(" (ninguna la referencia directamente)\n") + f.append("\n") + +w("00b_FUENTES_DE_CLIENTE.txt", "".join(f)) + +print("Generado:") +print(f" 05_construccion_clientes_intel/ -> {len(want)} archivos con SQL de construccion") +print(f" 00b_FUENTES_DE_CLIENTE.txt") +print("\nTablas con SQL de construccion escrito:") +for t in want: + print(f" - {t}") diff --git a/scratchpad/gen_verify.py b/scratchpad/gen_verify.py new file mode 100644 index 00000000..d7a03d53 --- /dev/null +++ b/scratchpad/gen_verify.py @@ -0,0 +1,126 @@ +"""Genera 00c_VERIFICACION.txt (chequeo de completitud del linaje) y +06_otros_outputs_clientes_intel/ (SQL de las tablas de clientes_intel que NO acaban +en customer_marts, para no dejar ninguna atras). +""" +import json +import os +import textwrap + +DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts" +PROJECT = "autingo-159109" +builds = json.load(open("scratchpad/intel_build.json")) +lin = json.load(open("scratchpad/intel_lineage.json")) +involved = set(lin["intel_involved"]) + +# Catalogo completo de clientes_intel (40 objetos) reconstruido: involved + leftovers conocidos. +LEFTOVER = [ + "_presupuesto_persona", "_veh_cluster_feat", "_veh_tec_feat", "audit_persona_divergencias", + "calidad_email_snapshot", "f0_audit_keys", "fact_impacto_campana", "map_mutualista_particular", + "reco_promo_personalizada", "reco_promo_segmento", "rpt_campana", "rpt_campana_lift", + "rpt_campana_usuario", "rpt_impacto_persona", "seg_audiencia", "seg_vega_persona", + "sf_contact_map", "tipologia_cliente_resumen", "veh_cluster", +] + +# Clasificacion por proposito (a donde va cada leftover). +CATEGORY = { + "rpt_campana": "Informe de campanas (BI / dashboards de marketing)", + "rpt_campana_lift": "Informe de campanas: lift (BI / dashboards)", + "rpt_campana_usuario": "Informe de campanas por usuario (BI / dashboards)", + "rpt_impacto_persona": "Informe de impacto por persona (BI / dashboards)", + "fact_impacto_campana": "Hechos de impacto de campana (base de los informes)", + "reco_promo_personalizada": "Recomendacion de promo personalizada (activacion)", + "reco_promo_segmento": "Recomendacion de promo por segmento (activacion)", + "seg_audiencia": "Audiencias para activacion (probable push a Salesforce/Marketing)", + "sf_contact_map": "Mapa de contactos Salesforce (sincronizacion de IDs)", + "audit_persona_divergencias": "Auditoria de calidad: divergencias en resolucion de persona", + "calidad_email_snapshot": "Auditoria de calidad: snapshot de emails", + "f0_audit_keys": "Auditoria de claves (control interno del pipeline)", + "_presupuesto_persona": "Auxiliar: presupuestos por persona (interim)", + "_veh_cluster_feat": "Auxiliar: features para clustering de vehiculo (interim)", + "_veh_tec_feat": "Auxiliar: features tecnicas de vehiculo (interim)", + "veh_cluster": "Clustering de vehiculo (resultado; no lo usan los marts hoy)", + "tipologia_cliente_resumen": "Resumen de tipologia de cliente (BI)", + "map_mutualista_particular": "Vista auxiliar: mapa mutualista/particular", + "seg_vega_persona": "Segmentacion VEGA por persona (contactabilidad; lee fuentes de cliente)", +} + +SEP = "=" * 78 + "\n" + +def w(path, text): + full = os.path.join(DEST, path) + os.makedirs(os.path.dirname(full), exist_ok=True) + with open(full, "w", newline="\r\n", encoding="utf-8") as f: + f.write(text) + +# --- 06: SQL de los leftovers que tengan build capturado --- +written = [] +for t in LEFTOVER: + b = builds.get(t) + if not b: + continue + out = [SEP, f"OBJETO : {PROJECT}.clientes_intel.{t}\n", + f"TIPO : {b['stmt']} (NO alimenta customer_marts)\n", + f"ULTIMA EJECUCION CAPTURADA: {b['last_run']}\n", SEP, + "\nQUE ES / A DONDE VA\n-------------------\n", + textwrap.fill(CATEGORY.get(t, "(sin clasificar)"), width=78) + "\n"] + if b["refs"]: + out.append("\nLEE DE\n------\n") + for r in b["refs"]: + out.append(f" - {PROJECT}.{r}\n") + out.append("\nSQL DE CONSTRUCCION (copiable)\n------------------------------\n\n") + out.append(b["query"].strip() + "\n") + w(f"06_otros_outputs_clientes_intel/{t}.txt", "".join(out)) + written.append(t) + +# --- 00c: verificacion de completitud --- +v = [SEP, "VERIFICACION DE COMPLETITUD DEL LINAJE\n", SEP, "\n"] +v.append("PREGUNTA: todo esto acaba en customer_marts? Comprobado.\n\n") +v.append("""RESPUESTA CORTA +--------------- +La cadena customer_marts -> fuentes esta COMPLETA (todas las referencias resueltas, +0 tablas sin identificar). PERO customer_marts NO es el unico destino: es UNO de los +consumidores de la capa clientes_intel. + + - clientes_intel tiene 40 objetos. + - 21 de ellos alimentan (directa o indirectamente) las 14 vistas de customer_marts. + - 19 NO van a customer_marts: son OTRAS salidas del mismo pipeline (informes de + campana, recomendaciones de promo, audiencias, auditorias, auxiliares). + +El unico dataset MODELADO que lee clientes_intel es customer_marts. El resto de lo que +lee clientes_intel y customer_marts son consultas de BI / ad-hoc (tablas temporales +_hexhash / anon...), es decir Metabase u otros lo consumen directamente. En ese sentido +customer_marts SI es terminal en el modelo (aguas abajo solo hay BI). + +""") + +v.append(SEP + "1) LAS 21 TABLAS DE clientes_intel QUE SI ALIMENTAN customer_marts\n" + SEP + "\n") +for t in sorted(involved): + b = builds.get(t, {}) + v.append(f" - {t} ({b.get('stmt','(sin job)')})\n") + +v.append("\n" + SEP + "2) LAS 19 TABLAS DE clientes_intel QUE NO VAN A customer_marts\n" + SEP + "\n") +v.append(" (SQL de cada una en 06_otros_outputs_clientes_intel/)\n\n") +for t in LEFTOVER: + sql_note = "" if t in written else " [sin SQL de job capturado]" + v.append(f" - {t:28s} {CATEGORY.get(t,'')}{sql_note}\n") + +v.append("\n" + SEP + "3) FUENTES BASE ALCANZADAS (fin del linaje)\n" + SEP + "\n") +v.append(" Fuera de clientes_intel, el pipeline lee de:\n\n") +for s in sorted(lin["external_sources"]): + v.append(f" - {PROJECT}.{s}\n") + +v.append("\n" + SEP + "4) NOTAS DE COBERTURA\n" + SEP + "\n") +v.append(""" - score_clv y seg_cluster_vehiculo: usadas por customer_marts pero sin CTAS reciente + en el historial de jobs (son modelos ML / cargas antiguas). Su esquema esta en + 02_intermedio_clientes_intel/; no hay un SQL de un solo job que las reconstruya. + - El SQL de construccion se tomo del ULTIMO job exitoso de cada tabla + (INFORMATION_SCHEMA.JOBS, region europe-west1, ventana 120 dias). Si una tabla se + reconstruye con otra logica fuera de esa ventana, no se captura aqui. + - customer_marts: 14 vistas = el dataset entero (no falta ninguna). +""") + +w("00c_VERIFICACION.txt", "".join(v)) + +print(f"06_otros_outputs_clientes_intel/ -> {len(written)} archivos") +print("00c_VERIFICACION.txt -> escrito") +print("\nLeftovers sin SQL capturado:", [t for t in LEFTOVER if t not in written] or "ninguno") diff --git a/scratchpad/intel_build.json b/scratchpad/intel_build.json new file mode 100644 index 00000000..67b1207a --- /dev/null +++ b/scratchpad/intel_build.json @@ -0,0 +1,472 @@ +{ + "_margen_rate_producto": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._margen_rate_producto` AS\n WITH clean AS (\n SELECT prod_nav_id, venta_n, compra_n, tasas\n FROM `autingo-159109.anjana_bi_amg.base_margenes_aa`\n WHERE venta_n > 0 AND compra_n <= 3 * venta_n\n )\n SELECT\n prod_nav_id,\n ROUND((SUM(venta_n) - SUM(compra_n) - SUM(tasas)) / SUM(venta_n), 4) AS margen_rate\n FROM clean\n GROUP BY prod_nav_id\n HAVING SUM(venta_n) > 0", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 09:42:03.770000+00:00", + "refs": [ + "anjana_bi_amg.margenes_mat" + ] + }, + "_persona_records": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._persona_records` AS\n WITH records AS (\n \n SELECT 'tpv' AS src, CAST(id AS STRING) AS source_id,\n document_number AS doc_raw, entity_email AS email_raw, entity_phone_number AS phone_raw,\n TRIM(CONCAT(COALESCE(entity_name,''),' ',COALESCE(entity_last_name,''))) AS name,\n CAST(NULL AS STRING) AS cp, customer_type,\n salesforce_customer_id AS salesforce_id, nav_id,\n CAST(NULL AS BOOL) AS email_optout, CAST(NULL AS BOOL) AS sms_optout,\n CAST(NULL AS BOOL) AS wa_optout, CAST(NULL AS BOOL) AS donotcall\n FROM `autingo-159109.psql_dcpublic.tpv_customers`\n WHERE deleted_at IS NULL\n \nUNION ALL\n\n SELECT 'web', CAST(id AS STRING),\n document, CAST(NULL AS STRING), phone_number,\n name, postal_code, CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS STRING),\n NULL, NULL, NULL, NULL\n FROM `autingo-159109.psql_dcpublic.customers`\n WHERE deleted_at IS NULL AND document != CAST(id AS STRING)\n \nUNION ALL\n\n SELECT 'otr', CAST(id AS STRING),\n dni, CAST(NULL AS STRING), telephone,\n full_name, CAST(NULL AS STRING), CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS STRING),\n NULL, NULL, NULL, NULL\n FROM `autingo-159109.psql_dcpublic.otr_customers`\n \nUNION ALL\n\n SELECT 'navision', no_,\n no_ AS doc_raw, e_mail, movil,\n name, post_code, CAST(NULL AS INT64),\n CAST(NULL AS STRING), no_,\n NULL, NULL, NULL, NULL\n FROM `autingo-159109.mssql2022_dbo.anjana_customer`\n WHERE _fivetran_deleted IS NOT TRUE\n \nUNION ALL\n\n SELECT 'citaprevia', CAST(id AS STRING),\n numero_doc_identidad, email, telefono1,\n nombre, codigo_postal, CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS STRING),\n NULL, NULL, NULL, NULL\n FROM `autingo-159109.citaprevia_aurphcp.clientes`\n WHERE _fivetran_deleted IS NOT TRUE\n \nUNION ALL\n\n SELECT 'users', CAST(id AS STRING),\n CAST(NULL AS STRING), email, CAST(NULL AS STRING),\n CAST(NULL AS STRING), CAST(NULL AS STRING), CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS STRING),\n NULL, NULL, NULL, NULL\n FROM `autingo-159109.psql_dcpublic.users`\n \nUNION ALL\n\n SELECT 'salesforce', ContactId,\n CAST(NULL AS STRING), Email, Phone,\n TRIM(CONCAT(COALESCE(FirstName,''),' ',COALESCE(LastName,''))), CAST(NULL AS STRING), CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS STRING),\n SAFE_CAST(EmailOptOut AS BOOL), SAFE_CAST(SMSOptOut AS BOOL),\n SAFE_CAST(WhatsappOptOut AS BOOL), SAFE_CAST(DoNotCall AS BOOL)\n FROM `autingo-159109.salesforce_ew1.contacts_latest`\n \n ),\n norm AS (\n SELECT\n src, source_id, name, cp, customer_type, salesforce_id, nav_id,\n email_optout, sms_optout, wa_optout, donotcall,\n IF(((REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[0-9]{8}[A-Z]$') AND SUBSTR('TRWAGMYFPDXBNJZSQVHLCKE', MOD(CAST(SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 1, 8) AS INT64), 23) + 1, 1) = SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 9, 1)) OR (REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[XYZ][0-9]{7}[A-Z]$') AND SUBSTR('TRWAGMYFPDXBNJZSQVHLCKE', MOD(CAST(CONCAT(CASE SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 1, 1) WHEN 'X' THEN '0' WHEN 'Y' THEN '1' ELSE '2' END, SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 2, 7)) AS INT64), 23) + 1, 1) = SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 9, 1)) OR (REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[ABCDEFGHJNPQRSUVW][0-9]{7}[0-9A-J]$'))), CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, NULL) AS doc,\n IF(REGEXP_CONTAINS(LOWER(TRIM(email_raw)), r'^[a-z0-9._%+\\-]+@[a-z0-9.\\-]+\\.[a-z]{2,}$')\n AND NOT REGEXP_CONTAINS(SPLIT(LOWER(TRIM(email_raw)),'@')[OFFSET(0)], r'(noquiere|nokiere|sincorreo|scorreo|nocorreo|sinemail|noemail|nomail|sinmail|notiene|notengo|nofacilita|nointeresa|noprocede|nocliente|nocomras|nodispone|sininformar|sinnombre|sinquerer|^no(lo|se|me|le)?(sabe|desea|quiere|quiero|quier|posee|hay|usa|da|tien|tine|tene|consta|aporta|facilita|funciona|informa|movil|telefono|contesta|contacto|email|mail|correo))')\n AND NOT REGEXP_CONTAINS(SPLIT(LOWER(TRIM(email_raw)),'@')[OFFSET(0)], r'^(no|nose|nada|ninguno|ninguna|test|prueba|asdf|qwerty|xxx+|aaa+|na|nn|n)$')\n AND SPLIT(LOWER(TRIM(email_raw)),'@')[OFFSET(1)] NOT IN ('correo.es','correo.com','no.com','no.es','no.no','notiene.com','notiene.es','noquiere.com','noquiere.es','sincorreo.es','sincorreo.com','tiene.com','n.com','nop.com','nmo.com','noi.com','x.com','xxx.com','test.com','prueba.com','email.com','aurgi.com','nodesea.com')\n AND NOT REGEXP_CONTAINS(SPLIT(LOWER(TRIM(email_raw)),'@')[OFFSET(1)], r'^(aurgi\\.com|correo\\.|no\\.|notiene\\.|noquiere\\.|sincorreo\\.|nodesea\\.)'),\n LOWER(TRIM(email_raw)), NULL) AS email,\n IF(REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^0034[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 5) WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^34[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 3) ELSE REGEXP_REPLACE(phone_raw, r'[^0-9]', '') END, r'^[6789][0-9]{8}$') AND REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^0034[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 5) WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^34[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 3) ELSE REGEXP_REPLACE(phone_raw, r'[^0-9]', '') END, r'^[67][0-9]{8}$'), CASE WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^0034[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 5) WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^34[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 3) ELSE REGEXP_REPLACE(phone_raw, r'[^0-9]', '') END, NULL) AS phone_fuse,\n IF(REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^0034[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 5) WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^34[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 3) ELSE REGEXP_REPLACE(phone_raw, r'[^0-9]', '') END, r'^[6789][0-9]{8}$'), CASE WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^0034[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 5) WHEN REGEXP_CONTAINS(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), r'^34[6789][0-9]{8}$') THEN SUBSTR(REGEXP_REPLACE(phone_raw, r'[^0-9]', ''), 3) ELSE REGEXP_REPLACE(phone_raw, r'[^0-9]', '') END, NULL) AS phone_any,\n CASE WHEN REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[0-9]{8}[A-Z]$') AND SUBSTR('TRWAGMYFPDXBNJZSQVHLCKE', MOD(CAST(SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 1, 8) AS INT64), 23) + 1, 1) = SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 9, 1) THEN 'DNI' WHEN REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[XYZ][0-9]{7}[A-Z]$') AND SUBSTR('TRWAGMYFPDXBNJZSQVHLCKE', MOD(CAST(CONCAT(CASE SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 1, 1) WHEN 'X' THEN '0' WHEN 'Y' THEN '1' ELSE '2' END, SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 2, 7)) AS INT64), 23) + 1, 1) = SUBSTR(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, 9, 1) THEN 'NIE' WHEN REGEXP_CONTAINS(CASE WHEN REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')), r'^[0-9]{7}[A-Z]$') THEN CONCAT('0', UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', ''))) ELSE UPPER(REGEXP_REPLACE(TRIM(doc_raw), r'[\\s\\.\\-]', '')) END, r'^[ABCDEFGHJNPQRSUVW][0-9]{7}[0-9A-J]$') THEN 'CIF' ELSE 'INVALIDO' END AS doc_kind\n FROM records\n ),\n email_stats AS (\n SELECT email, COUNT(DISTINCT doc) AS ndoc, MIN(doc) AS doc1, COUNT(*) AS nrec\n FROM norm WHERE email IS NOT NULL GROUP BY email\n ),\n phone_stats AS (\n SELECT phone_fuse, COUNT(DISTINCT doc) AS ndoc, MIN(doc) AS doc1, COUNT(*) AS nrec\n FROM norm WHERE phone_fuse IS NOT NULL GROUP BY phone_fuse\n ),\n assigned AS (\n SELECT\n n.*,\n CASE\n WHEN n.doc IS NOT NULL THEN CONCAT('DOC:', n.doc)\n WHEN n.email IS NOT NULL AND es.ndoc = 1 THEN CONCAT('DOC:', es.doc1)\n WHEN n.phone_fuse IS NOT NULL AND ps.ndoc = 1 THEN CONCAT('DOC:', ps.doc1)\n WHEN n.email IS NOT NULL AND es.ndoc = 0 AND es.nrec <= 5\n THEN CONCAT('EMAIL:', n.email)\n WHEN n.phone_fuse IS NOT NULL AND ps.ndoc = 0 AND ps.nrec <= 5\n THEN CONCAT('PHONE:', n.phone_fuse)\n ELSE CONCAT('REC:', n.src, ':', n.source_id)\n END AS persona_key,\n CASE\n WHEN n.doc IS NOT NULL THEN 'alta'\n WHEN (n.email IS NOT NULL AND es.ndoc = 1)\n OR (n.phone_fuse IS NOT NULL AND ps.ndoc = 1) THEN 'media'\n WHEN (n.email IS NOT NULL AND es.ndoc = 0 AND es.nrec <= 5)\n OR (n.phone_fuse IS NOT NULL AND ps.ndoc = 0 AND ps.nrec <= 5)\n THEN 'media'\n ELSE 'baja'\n END AS confianza\n FROM norm n\n LEFT JOIN email_stats es ON n.email = es.email\n LEFT JOIN phone_stats ps ON n.phone_fuse = ps.phone_fuse\n )\n SELECT\n FARM_FINGERPRINT(persona_key) AS persona_id,\n persona_key, confianza,\n src, source_id,\n doc, doc_kind, email, phone_fuse, phone_any,\n name, cp, customer_type, salesforce_id, nav_id,\n email_optout, sms_optout, wa_optout, donotcall\n FROM assigned", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:47:03.046000+00:00", + "refs": [ + "citaprevia_aurphcp.clientes", + "mssql2022_dbo.anjana_customer", + "psql_dcpublic.customers", + "psql_dcpublic.otr_customers", + "psql_dcpublic.tpv_customers", + "psql_dcpublic.users", + "salesforce_ew1.contacts_latest" + ] + }, + "_plate_year_calib": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._plate_year_calib` AS\n WITH raw AS (\n SELECT ((STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),5,1))-1)*400 + (STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),6,1))-1)*20 + (STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),7,1))-1)) AS ord, year\n FROM `autingo-159109.clientes_intel.dim_vehiculo`\n WHERE REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') AND year BETWEEN 1998 AND EXTRACT(YEAR FROM CURRENT_DATE())\n ),\n per_ord AS (\n SELECT ord, APPROX_QUANTILES(year, 2)[OFFSET(1)] AS y, COUNT(*) AS n\n FROM raw GROUP BY ord\n ),\n grid AS (SELECT ord FROM UNNEST(GENERATE_ARRAY(0, 7999)) AS ord),\n smooth AS (\n SELECT g.ord,\n (SELECT SUM(p.y * p.n) / SUM(p.n) FROM per_ord p\n WHERE p.ord BETWEEN g.ord - 150 AND g.ord + 150) AS y_smooth\n FROM grid g\n ),\n filled AS (\n SELECT ord,\n COALESCE(\n y_smooth,\n LAST_VALUE(y_smooth IGNORE NULLS) OVER (ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),\n FIRST_VALUE(y_smooth IGNORE NULLS) OVER (ORDER BY ord ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)\n ) AS y_fill\n FROM smooth\n )\n SELECT\n ord AS serie_ord,\n CAST(ROUND(MAX(y_fill) OVER (ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS INT64) AS anio_estimado\n FROM filled", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 09:42:19.481000+00:00", + "refs": [ + "clientes_intel.dim_vehiculo" + ] + }, + "_presupuesto_persona": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._presupuesto_persona` AS\nWITH x AS (\n SELECT TRIM(nav) AS nav_id, customer_id\n FROM `autingo-159109.claude_bi.conversion_cqg_base_mat`,\n UNNEST(SPLIT(origin_otr_nav_ids, ',')) AS nav\n WHERE origin_otr_nav_ids IS NOT NULL AND customer_id IS NOT NULL\n)\nSELECT DISTINCT x.nav_id, m.persona_id\nFROM x JOIN `autingo-159109.clientes_intel.map_persona_fuente` m\n ON m.fuente='tpv' AND m.source_id = CAST(x.customer_id AS STRING)\nWHERE x.nav_id != ''", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-09 11:15:04.102000+00:00", + "refs": [ + "claude_bi.conversion_cqg_base_mat", + "clientes_intel.map_persona_fuente" + ] + }, + "_veh_cluster_feat": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._veh_cluster_feat` AS\nSELECT\n vehiculo_id,\n COALESCE(antiguedad_anios, 15) AS antiguedad_anios,\n COALESCE(n_visitas, 0) AS n_visitas,\n LN(1 + GREATEST(COALESCE(importe_total, 0), 0)) AS log_importe,\n COALESCE(recency_days, 3650) AS recency_days,\n COALESCE(n_categorias, 0) AS n_categorias,\n COALESCE(pct_servicio, 0) AS pct_servicio,\n COALESCE(combustible, 'Desconocido') AS combustible\nFROM `autingo-159109.clientes_intel.feat_cliente_vehiculo`", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:46:06.819000+00:00", + "refs": [ + "clientes_intel.feat_cliente_vehiculo" + ] + }, + "_veh_km_k5": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_k5`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:01.905000+00:00", + "refs": [] + }, + "_veh_km_k6": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_k6`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:02.308000+00:00", + "refs": [] + }, + "_veh_km_k7": { + "query": "CREATE OR REPLACE MODEL `autingo-159109.clientes_intel._veh_km_k7`\n OPTIONS(model_type='kmeans', num_clusters=7,\n standardize_features=TRUE, kmeans_init_method='KMEANS++') AS\n SELECT * EXCEPT(vehiculo_id) FROM `autingo-159109.clientes_intel._veh_cluster_feat`", + "stmt": "CREATE_MODEL", + "last_run": "2026-06-11 14:38:19.112000+00:00", + "refs": [ + "clientes_intel._veh_cluster_feat" + ] + }, + "_veh_km_k8": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_k8`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:02.709000+00:00", + "refs": [] + }, + "_veh_km_tec_k4": { + "query": "CREATE OR REPLACE MODEL `autingo-159109.clientes_intel._veh_km_tec_k4`\n OPTIONS(model_type='kmeans', num_clusters=4,\n standardize_features=TRUE, kmeans_init_method='KMEANS++') AS\n SELECT * EXCEPT(vehiculo_id) FROM `autingo-159109.clientes_intel._veh_tec_feat`", + "stmt": "CREATE_MODEL", + "last_run": "2026-06-11 14:52:15.736000+00:00", + "refs": [ + "clientes_intel._veh_tec_feat" + ] + }, + "_veh_km_tec_k5": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_tec_k5`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:03.402000+00:00", + "refs": [] + }, + "_veh_km_tec_k6": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_tec_k6`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:03.789000+00:00", + "refs": [] + }, + "_veh_km_tec_k7": { + "query": "DROP MODEL IF EXISTS `autingo-159109.clientes_intel._veh_km_tec_k7`", + "stmt": "DROP_MODEL", + "last_run": "2026-06-11 14:57:04.478000+00:00", + "refs": [] + }, + "_veh_tec_assign": { + "query": "DROP TABLE IF EXISTS `autingo-159109.clientes_intel._veh_tec_assign`", + "stmt": "DROP_TABLE", + "last_run": "2026-06-11 14:57:05.227000+00:00", + "refs": [] + }, + "_veh_tec_feat": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel._veh_tec_feat` AS\nWITH med AS (\n SELECT APPROX_QUANTILES(tec_potencia_kw, 2)[OFFSET(1)] AS kw_med,\n APPROX_QUANTILES(tec_co2, 2)[OFFSET(1)] AS co2_med\n FROM `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n WHERE tec_cilindrada IS NOT NULL\n)\nSELECT\n f.vehiculo_id,\n f.antiguedad_anios,\n f.tec_cilindrada,\n COALESCE(f.tec_potencia_kw, med.kw_med) AS potencia_kw,\n COALESCE(f.tec_co2, med.co2_med) AS co2,\n f.tec_plazas,\n COALESCE(f.combustible, 'Desconocido') AS combustible,\n COALESCE(f.tec_segmento, 'Desconocido') AS segmento,\n COALESCE(f.tec_tipo, 'Desconocido') AS tipo\nFROM `autingo-159109.clientes_intel.feat_cliente_vehiculo` f, med\nWHERE f.tec_cilindrada IS NOT NULL", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:46:08.832000+00:00", + "refs": [ + "clientes_intel.feat_cliente_vehiculo" + ] + }, + "_xr_test": { + "query": "DROP TABLE `autingo-159109.clientes_intel._xr_test`", + "stmt": "DROP_TABLE", + "last_run": "2026-06-17 13:36:12.380000+00:00", + "refs": [] + }, + "audit_persona_divergencias": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.audit_persona_divergencias` AS\n WITH dim_docs AS (\n SELECT DISTINCT document_number AS doc FROM `autingo-159109.clientes_intel.dim_persona` WHERE document_number IS NOT NULL\n ),\n uni_docs AS (\n SELECT DISTINCT UPPER(REGEXP_REPLACE(TRIM(document_number), r'[\\s\\.\\-]', '')) AS doc\n FROM `autingo-159109.claude_bi.unified_customers`\n WHERE document_number IS NOT NULL AND TRIM(document_number) != ''\n )\n SELECT\n COALESCE(d.doc, u.doc) AS doc,\n d.doc IS NOT NULL AS en_dim_persona,\n u.doc IS NOT NULL AS en_unified\n FROM dim_docs d\n FULL OUTER JOIN uni_docs u USING (doc)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:47:31.137000+00:00", + "refs": [ + "claude_bi.unified_customers", + "clientes_intel.dim_persona" + ] + }, + "calidad_email_snapshot": { + "query": "MERGE `autingo-159109.clientes_intel.calidad_email_snapshot` T\nUSING (\n SELECT CURRENT_DATE() AS fecha,\n COUNT(*) AS total_clientes,\n COUNTIF(email IS NOT NULL) AS email_validos,\n COUNTIF(contactable_email) AS email_contactables,\n COUNTIF(email IS NOT NULL AND email_optout) AS email_optout,\n COUNTIF(email IS NULL) AS sin_email,\n ROUND(COUNTIF(email IS NOT NULL)/COUNT(*),4) AS pct_email_valido,\n ROUND(COUNTIF(contactable_email)/COUNT(*),4) AS pct_email_contactable,\n COUNTIF(phone IS NOT NULL) AS tel_validos,\n COUNTIF(contactable_telefono) AS tel_contactables\n FROM `autingo-159109.clientes_intel.seg_cliente_360`\n) S\nON T.fecha = S.fecha\nWHEN MATCHED THEN UPDATE SET\n total_clientes=S.total_clientes, email_validos=S.email_validos,\n email_contactables=S.email_contactables, email_optout=S.email_optout,\n sin_email=S.sin_email, pct_email_valido=S.pct_email_valido,\n pct_email_contactable=S.pct_email_contactable,\n tel_validos=S.tel_validos, tel_contactables=S.tel_contactables\nWHEN NOT MATCHED THEN INSERT ROW;\n", + "stmt": "MERGE", + "last_run": "2026-07-01 08:40:01.538000+00:00", + "refs": [ + "clientes_intel.seg_cliente_360" + ] + }, + "data_points_contacto": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.data_points_contacto`\n CLUSTER BY es_b2c, persona_id AS\n WITH\n src_agg AS (\n SELECT persona_id,\n COUNT(DISTINCT IF(email IS NOT NULL, src, NULL)) AS n_src_email,\n COUNT(DISTINCT IF(phone_any IS NOT NULL, src, NULL)) AS n_src_phone,\n LOGICAL_OR(phone_fuse IS NOT NULL) AS tiene_movil\n FROM `autingo-159109.clientes_intel._persona_records` GROUP BY persona_id\n ),\n email_share AS (\n SELECT email, COUNT(DISTINCT persona_id) AS np\n FROM `autingo-159109.clientes_intel._persona_records` WHERE email IS NOT NULL GROUP BY email\n ),\n phone_share AS (\n SELECT phone_any, COUNT(DISTINCT persona_id) AS np\n FROM `autingo-159109.clientes_intel._persona_records` WHERE phone_any IS NOT NULL GROUP BY phone_any\n ),\n veh AS (SELECT persona_id, COUNT(*) AS nv FROM `autingo-159109.clientes_intel.map_persona_vehiculo` GROUP BY persona_id),\n feat AS (\n SELECT persona_id, NOT es_excluido AS es_b2c, canal_preferido AS canal_entrada,\n CAST(NULL AS STRING) AS tipo_entrada,\n COALESCE(frequency, 0) AS frequency\n FROM `autingo-159109.clientes_intel.feat_cliente_persona`\n ),\n base AS (\n SELECT\n dp.persona_id,\n dp.document_number IS NOT NULL AS punto_dni,\n dp.email IS NOT NULL AS punto_email,\n dp.phone IS NOT NULL AS punto_telefono,\n dp.full_name IS NOT NULL AS punto_nombre,\n dp.postal_code IS NOT NULL AS punto_cp,\n COALESCE(v.nv, 0) > 0 AS punto_vehiculo,\n f.canal_entrada IS NOT NULL AS punto_canal,\n dp.num_fuentes >= 2 AS punto_multifuente,\n COALESCE(f.frequency, 0) >= 1 AS punto_historial,\n COALESCE(f.es_b2c, FALSE) AS es_b2c,\n f.canal_entrada, f.tipo_entrada,\n COALESCE(v.nv, 0) AS n_vehiculos,\n dp.document_kind, dp.confianza AS confianza_identidad, dp.num_fuentes,\n COALESCE(sa.n_src_email, 0) AS n_src_email,\n COALESCE(sa.n_src_phone, 0) AS n_src_phone,\n COALESCE(sa.tiene_movil, FALSE) AS tiene_movil,\n COALESCE(es.np, 0) AS email_n_personas,\n COALESCE(ps.np, 0) AS phone_n_personas,\n dp.email_optout, dp.sms_optout, dp.whatsapp_optout, dp.do_not_call\n FROM `autingo-159109.clientes_intel.dim_persona` dp\n LEFT JOIN src_agg sa USING (persona_id)\n LEFT JOIN email_share es ON dp.email = es.email\n LEFT JOIN phone_share ps ON dp.phone = ps.phone_any\n LEFT JOIN veh v USING (persona_id)\n LEFT JOIN feat f USING (persona_id)\n ),\n scored AS (\n SELECT *,\n CASE WHEN NOT punto_email THEN 0.0 ELSE ROUND(LEAST(1.0,\n 0.5 + 0.25*IF(n_src_email >= 2, 1, 0) + 0.25*IF(email_n_personas <= 1, 1, 0)), 2) END AS fiabilidad_email,\n CASE WHEN NOT punto_telefono THEN 0.0 ELSE ROUND(LEAST(1.0,\n 0.4 + 0.3*IF(tiene_movil, 1, 0) + 0.15*IF(n_src_phone >= 2, 1, 0)\n + 0.15*IF(phone_n_personas <= 1, 1, 0)), 2) END AS fiabilidad_telefono,\n CAST(punto_dni AS INT64) + CAST(punto_email AS INT64) + CAST(punto_telefono AS INT64) + CAST(punto_nombre AS INT64) + CAST(punto_cp AS INT64) + CAST(punto_vehiculo AS INT64) + CAST(punto_canal AS INT64) + CAST(punto_multifuente AS INT64) + CAST(punto_historial AS INT64) AS data_points\n FROM base\n )\n SELECT\n *,\n ROUND(0.4*CAST(punto_dni AS INT64) + 0.3*fiabilidad_email + 0.3*fiabilidad_telefono, 3) AS score_confianza_norm,\n CAST(ROUND(40*CAST(punto_dni AS INT64) + 30*fiabilidad_email + 30*fiabilidad_telefono) AS INT64) AS score_confianza,\n -- contactable (headline): B2C con EMAIL fiable y sin opt-out (canal de marketing\n -- escaso y accionable). El movil es casi universal -> se reporta aparte, no infla.\n es_b2c AND punto_email AND fiabilidad_email >= 0.75 AND NOT email_optout AS contactable,\n es_b2c AND punto_email AND fiabilidad_email >= 0.75 AND NOT email_optout AS contactable_email,\n es_b2c AND tiene_movil AND fiabilidad_telefono >= 0.75 AND NOT do_not_call AS contactable_telefono\n FROM scored;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 16:23:09.214000+00:00", + "refs": [ + "clientes_intel._persona_records", + "clientes_intel.dim_persona", + "clientes_intel.feat_cliente_persona", + "clientes_intel.map_persona_vehiculo" + ] + }, + "dim_cp_provincia": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.dim_cp_provincia` AS\nSELECT * FROM UNNEST([STRUCT('01','Araba/Alava','Pais Vasco'),('02','Albacete','Castilla-La Mancha'),('03','Alicante','Comunidad Valenciana'),('04','Almeria','Andalucia'),('05','Avila','Castilla y Leon'),('06','Badajoz','Extremadura'),('07','Illes Balears','Illes Balears'),('08','Barcelona','Cataluna'),('09','Burgos','Castilla y Leon'),('10','Caceres','Extremadura'),('11','Cadiz','Andalucia'),('12','Castellon','Comunidad Valenciana'),('13','Ciudad Real','Castilla-La Mancha'),('14','Cordoba','Andalucia'),('15','A Coruna','Galicia'),('16','Cuenca','Castilla-La Mancha'),('17','Girona','Cataluna'),('18','Granada','Andalucia'),('19','Guadalajara','Castilla-La Mancha'),('20','Gipuzkoa','Pais Vasco'),('21','Huelva','Andalucia'),('22','Huesca','Aragon'),('23','Jaen','Andalucia'),('24','Leon','Castilla y Leon'),('25','Lleida','Cataluna'),('26','La Rioja','La Rioja'),('27','Lugo','Galicia'),('28','Madrid','Comunidad de Madrid'),('29','Malaga','Andalucia'),('30','Murcia','Region de Murcia'),('31','Navarra','Navarra'),('32','Ourense','Galicia'),('33','Asturias','Asturias'),('34','Palencia','Castilla y Leon'),('35','Las Palmas','Canarias'),('36','Pontevedra','Galicia'),('37','Salamanca','Castilla y Leon'),('38','Santa Cruz de Tenerife','Canarias'),('39','Cantabria','Cantabria'),('40','Segovia','Castilla y Leon'),('41','Sevilla','Andalucia'),('42','Soria','Castilla y Leon'),('43','Tarragona','Cataluna'),('44','Teruel','Aragon'),('45','Toledo','Castilla-La Mancha'),('46','Valencia','Comunidad Valenciana'),('47','Valladolid','Castilla y Leon'),('48','Bizkaia','Pais Vasco'),('49','Zamora','Castilla y Leon'),('50','Zaragoza','Aragon'),('51','Ceuta','Ceuta'),('52','Melilla','Melilla')])", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-09 11:09:15.055000+00:00", + "refs": [] + }, + "dim_persona": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.dim_persona` AS\n WITH r AS (\n SELECT *, CASE src WHEN 'tpv' THEN 1 WHEN 'web' THEN 2 WHEN 'citaprevia' THEN 3 WHEN 'otr' THEN 4 WHEN 'navision' THEN 5 WHEN 'salesforce' THEN 6 WHEN 'users' THEN 7 ELSE 99 END AS prio, CASE confianza WHEN 'alta' THEN 1 WHEN 'media' THEN 2 WHEN 'baja' THEN 3 ELSE 9 END AS crank FROM `autingo-159109.clientes_intel._persona_records`\n )\n SELECT\n persona_id,\n CASE MIN(crank) WHEN 1 THEN 'alta' WHEN 2 THEN 'media' ELSE 'baja' END AS confianza,\n CASE\n WHEN STARTS_WITH(ANY_VALUE(persona_key), 'DOC:') THEN 'documento'\n WHEN STARTS_WITH(ANY_VALUE(persona_key), 'EMAIL:') THEN 'email'\n WHEN STARTS_WITH(ANY_VALUE(persona_key), 'PHONE:') THEN 'telefono'\n ELSE 'singleton'\n END AS metodo_match,\n ANY_VALUE(doc) AS document_number,\n ANY_VALUE(doc_kind) AS document_kind,\n ARRAY_AGG(name IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS full_name,\n ARRAY_AGG(email IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS email,\n ARRAY_AGG(phone_any IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS phone,\n ARRAY_AGG(cp IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS postal_code,\n LOGICAL_OR(customer_type = 1) OR ANY_VALUE(doc_kind) = 'CIF' AS es_empresa,\n ARRAY_AGG(salesforce_id IGNORE NULLS LIMIT 1)[SAFE_OFFSET(0)] AS salesforce_customer_id,\n ARRAY_AGG(nav_id IGNORE NULLS LIMIT 1)[SAFE_OFFSET(0)] AS tpv_nav_id,\n COALESCE(LOGICAL_OR(email_optout), FALSE) AS email_optout,\n COALESCE(LOGICAL_OR(sms_optout), FALSE) AS sms_optout,\n COALESCE(LOGICAL_OR(wa_optout), FALSE) AS whatsapp_optout,\n COALESCE(LOGICAL_OR(donotcall), FALSE) AS do_not_call,\n COUNT(DISTINCT src) AS num_fuentes,\n LOGICAL_OR(src = 'tpv') AS in_tpv,\n LOGICAL_OR(src = 'web') AS in_web,\n LOGICAL_OR(src = 'otr') AS in_otr,\n LOGICAL_OR(src = 'navision') AS in_navision,\n LOGICAL_OR(src = 'citaprevia') AS in_citaprevia,\n LOGICAL_OR(src = 'salesforce') AS in_salesforce,\n LOGICAL_OR(src = 'users') AS in_users\n FROM r\n GROUP BY persona_id", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:47:15.836000+00:00", + "refs": [ + "clientes_intel._persona_records" + ] + }, + "dim_vehiculo": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.dim_vehiculo` AS\n WITH\n veh_tpv AS (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')) AS plate_norm, v.license_plate,\n cm.name AS make, cmod.name AS model, cmf.name AS model_family,\n cv.name AS version,\n -- v.fuel mezcla texto ('Diesel'...) y codigos numericos; nos quedamos solo el texto\n IF(REGEXP_CONTAINS(v.fuel, r'^[0-9]+$'), NULL, NULLIF(v.fuel,'')) AS fuel,\n SAFE_CAST(v.year AS INT64) AS year,\n v.car_frame, CAST(NULL AS INT64) AS km,\n cv.original_tyre_size AS tyre_size,\n 1 AS prio, 'tpv' AS src, v.created_at\n FROM `autingo-159109.psql_dcpublic.tpv_vehicles_vehicle` v\n LEFT JOIN `autingo-159109.psql_dcpublic.car_makes` cm ON v.car_make_id = cm.id\n LEFT JOIN `autingo-159109.psql_dcpublic.car_models` cmod ON v.car_model_id = cmod.id\n LEFT JOIN `autingo-159109.psql_dcpublic.car_model_families` cmf ON v.car_model_family_id = cmf.id\n LEFT JOIN `autingo-159109.psql_dcpublic.car_versions` cv ON v.car_version_id = cv.id\n WHERE (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n ),\n veh_otr AS (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')) AS plate_norm, ov.license_plate,\n COALESCE(ov.brand_description, ov.brand) AS make,\n COALESCE(ov.model_description, ov.model) AS model,\n CAST(NULL AS STRING) AS model_family, CAST(NULL AS STRING) AS version,\n CAST(NULL AS STRING) AS fuel,\n SAFE_CAST(ov.year AS INT64) AS year,\n CAST(NULL AS STRING) AS car_frame, SAFE_CAST(ov.km AS INT64) AS km,\n CAST(NULL AS STRING) AS tyre_size,\n 2 AS prio, 'otr' AS src, ov.created_at\n FROM `autingo-159109.psql_dcpublic.otr_vehicles` ov\n WHERE (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n ),\n veh_lineas AS (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(otrs_unicas_por_order__plate), r'[\\s\\-]', '')) AS plate_norm,\n ANY_VALUE(otrs_unicas_por_order__plate) AS license_plate,\n CAST(NULL AS STRING), CAST(NULL AS STRING), CAST(NULL AS STRING), CAST(NULL AS STRING),\n CAST(NULL AS STRING), CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS INT64), CAST(NULL AS STRING),\n 3, 'lineas', MAX(fecha)\n FROM `autingo-159109.claude_bi.todos_datos_lineas_mat`\n WHERE (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n GROUP BY plate_norm\n ),\n veh_citaprevia AS (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')) AS plate_norm,\n ANY_VALUE(cm.matricula) AS license_plate,\n CAST(NULL AS STRING), CAST(NULL AS STRING), CAST(NULL AS STRING), CAST(NULL AS STRING),\n CAST(NULL AS STRING), CAST(NULL AS INT64),\n CAST(NULL AS STRING), CAST(NULL AS INT64), CAST(NULL AS STRING),\n 4, 'citaprevia', MAX(CAST(NULL AS TIMESTAMP))\n FROM `autingo-159109.citaprevia_aurphcp.clientes_matriculas` cm\n WHERE cm._fivetran_deleted IS NOT TRUE AND (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n GROUP BY plate_norm\n ),\n todos AS (\n SELECT * FROM veh_tpv\n UNION ALL SELECT * FROM veh_otr\n UNION ALL SELECT * FROM veh_lineas\n UNION ALL SELECT * FROM veh_citaprevia\n ),\n agg AS (\n SELECT\n plate_norm,\n ARRAY_AGG(license_plate IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS license_plate,\n ARRAY_AGG(make IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS make,\n ARRAY_AGG(model IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS model,\n ARRAY_AGG(model_family IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS model_family,\n ARRAY_AGG(version IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS version,\n ARRAY_AGG(fuel IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS fuel,\n ARRAY_AGG(year IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS year,\n ARRAY_AGG(car_frame IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS car_frame,\n MAX(km) AS km,\n ARRAY_AGG(tyre_size IGNORE NULLS ORDER BY prio LIMIT 1)[SAFE_OFFSET(0)] AS tyre_size,\n LOGICAL_OR(src = 'tpv') AS in_tpv,\n LOGICAL_OR(src = 'otr') AS in_otr,\n LOGICAL_OR(src = 'lineas') AS in_lineas,\n LOGICAL_OR(src = 'citaprevia') AS in_citaprevia\n FROM todos\n GROUP BY plate_norm\n ),\n tecrmi AS (\n -- 1 fila por matricula: aglutina el mejor vin (no nulo), el mejor vin_data (con tech_data)\n -- y el mejor license_plate_data (con .data > make_text), aunque vengan de consultas TecRMI distintas.\n -- vin_data/lpd se guardan como STRING JSON (ARRAY_AGG de JSON no soportado); JSON_VALUE acepta STRING.\n SELECT\n plate_norm,\n ARRAY_AGG(vin IGNORE NULLS ORDER BY vin_rank LIMIT 1)[SAFE_OFFSET(0)] AS vin,\n ARRAY_AGG(vd_str IGNORE NULLS ORDER BY vd_rank LIMIT 1)[SAFE_OFFSET(0)] AS vin_data,\n ARRAY_AGG(lpd_str IGNORE NULLS ORDER BY lpd_rank LIMIT 1)[SAFE_OFFSET(0)] AS lpd\n FROM (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(license_plate), r'[\\s\\-]', '')) AS plate_norm,\n NULLIF(TRIM(vin), '') AS vin,\n IF(vin_data IS NULL, NULL, TO_JSON_STRING(vin_data)) AS vd_str,\n IF(license_plate_data IS NULL, NULL, TO_JSON_STRING(license_plate_data)) AS lpd_str,\n IF(NULLIF(TRIM(vin), '') IS NOT NULL, 0, 1) AS vin_rank,\n IF(JSON_EXTRACT(vin_data, '$.tech_data') IS NOT NULL, 0, 1) AS vd_rank,\n CASE\n WHEN JSON_EXTRACT(license_plate_data, '$.data') IS NOT NULL THEN 0\n WHEN JSON_VALUE(license_plate_data, '$.make_text') IS NOT NULL THEN 1\n WHEN license_plate_data IS NOT NULL THEN 2\n ELSE 3\n END AS lpd_rank\n FROM `autingo-159109.psql_dcpublic.tecrmi_license_plates`\n )\n GROUP BY plate_norm\n )\n SELECT\n agg.*,\n COALESCE(NULLIF(TRIM(agg.car_frame), ''), t.vin) AS vin,\n CASE\n WHEN agg.car_frame IS NOT NULL AND TRIM(agg.car_frame) != '' THEN 'tpv'\n WHEN t.vin IS NOT NULL THEN 'tecrmi'\n ELSE NULL\n END AS vin_source,\n -- Ficha del vehiculo: TODO lo extraible de las DOS columnas JSON de TecRMI.\n -- vin_data.tech_data: \"DATOS TÉCNICOS DGT\" / \"IDENTIFICACIÓN VEHÍCULO\" (claves acentuadas)\n -- license_plate_data.data: DATOSTECNICOSDGT / IDENTIFICACION_VEHICULO / MATRICULA (claves sin acentos)\n -- license_plate_data flat: make_text / model_text / fuel / date\n COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".MARCA'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.MARCA'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.MARCA'),\n JSON_VALUE(t.lpd, '$.data.MATRICULA.Marca'),\n JSON_VALUE(t.lpd, '$.make_text')\n ) AS tec_marca,\n COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".MODELO'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.MODELO'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.MODELO'),\n JSON_VALUE(t.lpd, '$.data.MATRICULA.Modelo'),\n JSON_VALUE(t.lpd, '$.model_text')\n ) AS tec_modelo,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".\"MODELO GENÉRICO\"'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.\"MODELO GENÉRICO\"')\n ), '-'), '') AS tec_modelo_generico,\n COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".COMBUSTIBLE'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.COMBUSTIBLE'),\n JSON_VALUE(t.lpd, '$.data.MATRICULA.Carburante'),\n JSON_VALUE(t.lpd, '$.fuel')\n ) AS tec_combustible,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".CILINDRADA'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.CILINDRADA')\n ), '-') AS INT64) AS tec_cilindrada,\n NULLIF(SAFE_CAST(NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".KW'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.KW')\n ), '-'), '*******') AS FLOAT64), 0) AS tec_potencia_kw,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".\"POTENCIA FISCAL\"'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.\"POTENCIA FISCAL\"')\n ), '-') AS FLOAT64) AS tec_potencia_fiscal,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".CO2'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.CO2')\n ), '-') AS FLOAT64) AS tec_co2,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".PLAZAS'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.PLAZAS')\n ), '-') AS INT64) AS tec_plazas,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".\"PESO MAXIMO\"'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.\"PESO MAXIMO\"')\n ), '-') AS INT64) AS tec_peso_maximo,\n SAFE_CAST(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".TARA'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.TARA')\n ), '-') AS INT64) AS tec_tara,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"DATOS TÉCNICOS DGT\".TIPO'),\n JSON_VALUE(t.lpd, '$.data.DATOSTECNICOSDGT.TIPO')\n ), '-'), '') AS tec_tipo,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".SEGMENTO'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.SEGMENTO')\n ), '-'), '') AS tec_segmento,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".MERCADO'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.MERCADO')\n ), '-'), '') AS tec_mercado,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".PROCEDENCIA'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.PROCEDENCIA')\n ), '-'), '') AS tec_procedencia,\n NULLIF(NULLIF(COALESCE(\n JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".\"PROVINCIA MATRÍCULA\"'),\n JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.\"PROVINCIA MATRÍCULA\"')\n ), '-'), '') AS tec_provincia_matricula,\n COALESCE(\n SAFE.PARSE_DATE('%d/%m/%Y', NULLIF(JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".\"FECHA 1ª MATRÍCULA\"'), '00/00/0000')),\n SAFE.PARSE_DATE('%d/%m/%Y', NULLIF(JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.\"FECHA 1ª MATRÍCULA\"'), '00/00/0000'))\n ) AS tec_fecha_1a_matricula,\n COALESCE(\n SAFE.PARSE_DATE('%d/%m/%Y', NULLIF(JSON_VALUE(t.vin_data, '$.tech_data.\"IDENTIFICACIÓN VEHÍCULO\".\"FECHA MATRÍCULA\"'), '00/00/0000')),\n SAFE.PARSE_DATE('%d/%m/%Y', NULLIF(JSON_VALUE(t.lpd, '$.data.IDENTIFICACION_VEHICULO.\"FECHA MATRÍCULA\"'), '00/00/0000')),\n SAFE_CAST(JSON_VALUE(t.lpd, '$.date') AS DATE)\n ) AS tec_fecha_matricula,\n JSON_EXTRACT_STRING_ARRAY(t.vin_data, '$.ktypes') AS ktypes,\n JSON_EXTRACT_STRING_ARRAY(t.lpd, '$.car_versions') AS car_versions\n FROM agg\n LEFT JOIN tecrmi t USING (plate_norm)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:00:13.035000+00:00", + "refs": [ + "citaprevia_aurphcp.clientes_matriculas", + "claude_bi.todos_datos_lineas_mat", + "psql_dcpublic.car_makes", + "psql_dcpublic.car_model_families", + "psql_dcpublic.car_models", + "psql_dcpublic.car_versions", + "psql_dcpublic.otr_vehicles", + "psql_dcpublic.tecrmi_license_plates", + "psql_dcpublic.tpv_vehicles_vehicle" + ] + }, + "fact_campana_respuesta": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.fact_campana_respuesta`\n CLUSTER BY canal, persona_id AS\n WITH\n sent AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana,\n COUNT(*) AS n_enviados,\n COUNTIF(DateUndeliverable IS NOT NULL AND CAST(DateUndeliverable AS STRING) != '') AS n_rebotados,\n COUNTIF(DateUnsubscribed IS NOT NULL AND CAST(DateUnsubscribed AS STRING) != '') AS n_baja,\n MIN(DATE(SENT_Eventdate)) AS primera_fecha,\n MAX(DATE(SENT_Eventdate)) AS ultima_fecha\n FROM `autingo-159109.salesforce_ew1.email_sent`\n WHERE SubscriberKey IS NOT NULL\n GROUP BY sk, campana\n ),\n opened AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana, COUNT(*) AS n_abiertos\n FROM `autingo-159109.salesforce_ew1.email_opened` WHERE SubscriberKey IS NOT NULL GROUP BY sk, campana\n ),\n clicked AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana, COUNT(*) AS n_clicados\n FROM `autingo-159109.salesforce_ew1.email_clicked` WHERE SubscriberKey IS NOT NULL GROUP BY sk, campana\n ),\n email AS (\n SELECT 'EMAIL' AS canal, s.sk, s.campana, s.n_enviados,\n COALESCE(o.n_abiertos, 0) AS n_abiertos,\n COALESCE(c.n_clicados, 0) AS n_clicados,\n s.n_enviados - s.n_rebotados AS n_entregados, s.n_rebotados, s.n_baja,\n s.primera_fecha, s.ultima_fecha\n FROM sent s\n LEFT JOIN opened o USING (sk, campana)\n LEFT JOIN clicked c USING (sk, campana)\n ),\n sms AS (\n SELECT 'SMS' AS canal, CAST(SubscriberKey AS STRING) AS sk,\n campana,\n COUNTIF(Sent) AS n_enviados, 0 AS n_abiertos, 0 AS n_clicados,\n COUNTIF(Delivered) AS n_entregados, COUNTIF(Undelivered) AS n_rebotados, 0 AS n_baja,\n MIN(DATE(CreateDateTime)) AS primera_fecha, MAX(DATE(CreateDateTime)) AS ultima_fecha\n FROM `autingo-159109.salesforce_ew1.sms`\n WHERE SubscriberKey IS NOT NULL AND Outbound\n GROUP BY sk, campana\n ),\n eventos AS (\n SELECT * FROM email\n UNION ALL SELECT * FROM sms\n )\n SELECT\n p.persona_id,\n e.canal, e.campana,\n e.n_enviados, e.n_entregados, e.n_rebotados, e.n_baja,\n e.n_abiertos, e.n_clicados,\n SAFE_DIVIDE(e.n_abiertos, e.n_enviados) AS tasa_apertura,\n SAFE_DIVIDE(e.n_clicados, e.n_enviados) AS tasa_click,\n e.n_abiertos > 0 AS abrio,\n e.n_clicados > 0 AS clico,\n e.n_baja > 0 AS baja,\n e.primera_fecha, e.ultima_fecha\n FROM eventos e\n JOIN `autingo-159109.clientes_intel.dim_persona` p ON p.salesforce_customer_id = e.sk;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 10:28:04.473000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "salesforce_ew1.email_clicked", + "salesforce_ew1.email_opened", + "salesforce_ew1.email_sent", + "salesforce_ew1.sms" + ] + }, + "fact_campana_respuesta__sfnew": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.fact_campana_respuesta__sfnew`\n CLUSTER BY canal, persona_id AS\n WITH\n sent AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana,\n COUNT(*) AS n_enviados,\n COUNTIF(DateUndeliverable IS NOT NULL AND CAST(DateUndeliverable AS STRING) != '') AS n_rebotados,\n COUNTIF(DateUnsubscribed IS NOT NULL AND CAST(DateUnsubscribed AS STRING) != '') AS n_baja,\n MIN(DATE(SENT_Eventdate)) AS primera_fecha,\n MAX(DATE(SENT_Eventdate)) AS ultima_fecha\n FROM `autingo-159109.salesforce_ew1.email_sent`\n WHERE SubscriberKey IS NOT NULL\n GROUP BY sk, campana\n ),\n opened AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana, COUNT(*) AS n_abiertos\n FROM `autingo-159109.salesforce_ew1.email_opened` WHERE SubscriberKey IS NOT NULL GROUP BY sk, campana\n ),\n clicked AS (\n SELECT CAST(SubscriberKey AS STRING) AS sk, EmailName AS campana, COUNT(*) AS n_clicados\n FROM `autingo-159109.salesforce_ew1.email_clicked` WHERE SubscriberKey IS NOT NULL GROUP BY sk, campana\n ),\n email AS (\n SELECT 'EMAIL' AS canal, s.sk, s.campana, s.n_enviados,\n COALESCE(o.n_abiertos, 0) AS n_abiertos,\n COALESCE(c.n_clicados, 0) AS n_clicados,\n s.n_enviados - s.n_rebotados AS n_entregados, s.n_rebotados, s.n_baja,\n s.primera_fecha, s.ultima_fecha\n FROM sent s\n LEFT JOIN opened o USING (sk, campana)\n LEFT JOIN clicked c USING (sk, campana)\n ),\n sms AS (\n SELECT 'SMS' AS canal, CAST(SubscriberKey AS STRING) AS sk,\n CAST(NULL AS STRING) AS campana,\n COUNTIF(Sent) AS n_enviados, 0 AS n_abiertos, 0 AS n_clicados,\n COUNTIF(Delivered) AS n_entregados, COUNTIF(Undelivered) AS n_rebotados, 0 AS n_baja,\n MIN(DATE(CreateDateTime)) AS primera_fecha, MAX(DATE(CreateDateTime)) AS ultima_fecha\n FROM `autingo-159109.salesforce_ew1.sms`\n WHERE SubscriberKey IS NOT NULL AND Outbound\n GROUP BY sk\n ),\n eventos AS (\n SELECT * FROM email\n UNION ALL SELECT * FROM sms\n )\n SELECT\n p.persona_id,\n e.canal, e.campana,\n e.n_enviados, e.n_entregados, e.n_rebotados, e.n_baja,\n e.n_abiertos, e.n_clicados,\n SAFE_DIVIDE(e.n_abiertos, e.n_enviados) AS tasa_apertura,\n SAFE_DIVIDE(e.n_clicados, e.n_enviados) AS tasa_click,\n e.n_abiertos > 0 AS abrio,\n e.n_clicados > 0 AS clico,\n e.n_baja > 0 AS baja,\n e.primera_fecha, e.ultima_fecha\n FROM eventos e\n JOIN `autingo-159109.clientes_intel.dim_persona` p ON p.salesforce_customer_id = e.sk;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-18 08:23:23.300000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "salesforce_ew1.email_clicked", + "salesforce_ew1.email_opened", + "salesforce_ew1.email_sent", + "salesforce_ew1.sms" + ] + }, + "fact_impacto_campana": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.fact_impacto_campana`\nCLUSTER BY canal AS\nWITH resp AS (\n SELECT persona_id, canal, campana, ultima_fecha AS fecha_envio, abrio\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta` WHERE ultima_fecha IS NOT NULL\n),\nper AS (\n SELECT r.canal, r.campana, r.persona_id, ANY_VALUE(r.abrio) AS abrio,\n MAX(IF(t.fecha > r.fecha_envio AND t.fecha <= DATE_ADD(r.fecha_envio, INTERVAL 30 DAY), 1, 0)) AS compro,\n SUM(IF(t.fecha > r.fecha_envio AND t.fecha <= DATE_ADD(r.fecha_envio, INTERVAL 30 DAY), t.importe, 0)) AS eur\n FROM resp r\n LEFT JOIN `autingo-159109.clientes_intel.fact_transaccion` t\n ON t.persona_id = r.persona_id AND t.fecha >= '2025-01-01'\n GROUP BY r.canal, r.campana, r.persona_id\n)\nSELECT canal, campana, COUNT(*) AS personas, COUNTIF(compro = 1) AS compradores,\n ROUND(AVG(CAST(abrio AS INT64)), 4) AS pct_abrio, ROUND(AVG(compro), 4) AS conversion,\n ROUND(AVG(IF(abrio, compro, NULL)), 4) AS conv_abridores,\n ROUND(AVG(IF(NOT abrio, compro, NULL)), 4) AS conv_no_abridores,\n ROUND(SUM(eur), 0) AS eur_atribuido,\n ROUND(SAFE_DIVIDE(SUM(eur), COUNT(*)), 2) AS eur_por_persona\nFROM per GROUP BY canal, campana;\n", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-18 09:42:09.008000+00:00", + "refs": [ + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion" + ] + }, + "fact_transaccion": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.fact_transaccion`\n PARTITION BY fecha\n CLUSTER BY persona_id, centro_nav_id AS\n WITH tpv_map AS (\n SELECT source_id, persona_id FROM `autingo-159109.clientes_intel.map_persona_fuente` WHERE fuente='tpv'\n ),\n base AS (\n SELECT\n l.orderitem_id AS transaccion_id,\n m.persona_id AS persona_orig, -- << era: AS persona_id\n IF((REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$')), UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')), NULL) AS vehiculo_id,\n l.Tpv_Customers___Customer__id AS customer_id_tpv,\n DATE(l.fecha) AS fecha,\n l.Centros___Center__nav_id AS centro_nav_id,\n l.Centros___Center__name AS centro_nombre,\n l.Centros___Center__Companies__name AS empresa,\n l.Centros___Center__Zones__name AS zona,\n IF(l.Centros___Center__Companies__name LIKE 'Digital%', 'web', 'tienda') AS canal,\n l.Productos___Product__Producto___Nav__idCategoria AS categoria,\n l.Productos___Product__Producto___Nav__idGrupo AS grupo,\n l.Productos___Product__nav_id AS producto_nav_id,\n l.Productos___Product__description AS producto_desc,\n l.Tpv_Orders_Orderitem__quantity AS cantidad,\n l.valor_linea AS importe,\n l.Total_Linea_con_Iva AS importe_con_iva,\n l.Coste_medio_de_producto_en_la_venta AS coste_unitario,\n ROUND(l.Coste_medio_de_producto_en_la_venta * l.Tpv_Orders_Orderitem__quantity, 4) AS coste_linea_aprox,\n l.otrs_unicas_por_order__Id_de_OTR AS id_otr,\n l.otrs_unicas_por_order__Status_de_Otr AS otr_status,\n l.nav_id AS id_factura_nav,\n l.otrs_unicas_por_order__nav_id AS otr_nav_id,\n 'TPV' AS origen\n FROM `autingo-159109.claude_bi.todos_datos_lineas_mat` l\n LEFT JOIN tpv_map m\n ON CAST(l.Tpv_Customers___Customer__id AS STRING) = m.source_id\n WHERE l.fecha IS NOT NULL\n )\n SELECT\n b.* EXCEPT(persona_orig),\n COALESCE(mm.particular_persona_id, b.persona_orig) AS persona_id -- << reasignacion solo-mutua\n FROM base b\n LEFT JOIN `autingo-159109.clientes_intel.map_mutualista_particular` mm\n ON mm.agregador_persona_id = b.persona_orig\n AND mm.plate_norm = b.vehiculo_id\n AND mm.es_mutualista", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:14:05.960000+00:00", + "refs": [ + "claude_bi.todos_datos_lineas_mat", + "clientes_intel.dim_persona", + "clientes_intel.map_persona_fuente", + "clientes_intel.map_persona_vehiculo" + ] + }, + "fact_transaccion_test": { + "query": "DROP TABLE IF EXISTS `autingo-159109.clientes_intel.fact_transaccion_test`", + "stmt": "DROP_TABLE", + "last_run": "2026-06-08 15:41:28.750000+00:00", + "refs": [] + }, + "fact_visita": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.fact_visita`\n PARTITION BY fecha\n CLUSTER BY persona_id AS\n SELECT\n persona_id,\n fecha,\n SUM(importe) AS importe_dia,\n COUNT(*) AS n_lineas,\n COUNT(DISTINCT id_otr) AS n_otr,\n COUNT(DISTINCT centro_nav_id) AS n_centros,\n COUNT(DISTINCT vehiculo_id) AS n_vehiculos,\n ARRAY_AGG(vehiculo_id IGNORE NULLS ORDER BY importe DESC LIMIT 1)[SAFE_OFFSET(0)] AS vehiculo_principal,\n ARRAY_AGG(centro_nav_id IGNORE NULLS ORDER BY importe DESC LIMIT 1)[SAFE_OFFSET(0)] AS centro_principal,\n ARRAY_AGG(empresa IGNORE NULLS ORDER BY importe DESC LIMIT 1)[SAFE_OFFSET(0)] AS empresa_principal\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id, fecha", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:14:28.749000+00:00", + "refs": [ + "clientes_intel.fact_transaccion" + ] + }, + "feat_cliente_persona": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.feat_cliente_persona`\n CLUSTER BY rfm_segment, persona_id AS\n WITH\n excluir_tpv AS (\n SELECT CAST(id AS STRING) AS source_id\n FROM `autingo-159109.psql_dcpublic.tpv_customers`\n WHERE id = 1 OR REGEXP_CONTAINS(\n UPPER(COALESCE(CASE WHEN id=12 THEN 'GESCAB' END, entity_name, '')), r'MUTUAMAD SHARING SLU|MOOVE|CENTAURO RENT|BOVIO|TELEFURGO|FLEXICAR|BANCO SANTANDER, S.A.|TALRENT|ARAIZ|AYVENS|SANTANDER DE LEASE|MUYCAR|GESCAB|NORDIC BUS|PIRELLI|ALPESUR|PANORAMA|OCASION PLUS|CAB CAR')\n ),\n excluir_personas AS (\n SELECT DISTINCT m.persona_id\n FROM `autingo-159109.clientes_intel.map_persona_fuente` m JOIN excluir_tpv e ON m.fuente='tpv' AND m.source_id = e.source_id\n UNION DISTINCT\n -- PARCHE solo-mutua: excluir las cuentas-paraguas de mutua de la base B2C\n SELECT DISTINCT agregador_persona_id\n FROM `autingo-159109.clientes_intel.map_mutualista_particular` WHERE tipo_origen='MUTUA'\n ),\n rfm AS (\n SELECT\n persona_id,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), MAX(fecha), DAY) AS recency_days,\n COUNT(*) AS frequency,\n ROUND(SUM(importe_dia), 2) AS monetary_total,\n ROUND(AVG(importe_dia), 2) AS ticket_medio,\n ROUND(STDDEV(importe_dia), 2) AS ticket_std,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), MIN(fecha), DAY) AS tenure_days,\n MIN(fecha) AS primera_compra,\n MAX(fecha) AS ultima_compra,\n ROUND(SUM(importe_dia * POW(0.5, SAFE_DIVIDE(DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), fecha, DAY), 548))), 2) AS valor_ponderado\n FROM `autingo-159109.clientes_intel.fact_visita`\n GROUP BY persona_id\n ),\n inter AS (\n SELECT persona_id,\n ROUND(AVG(gap), 2) AS mean_inter_visit_days,\n ROUND(STDDEV(gap), 2) AS std_inter_visit_days\n FROM (\n SELECT persona_id,\n DATE_DIFF(fecha, LAG(fecha) OVER (PARTITION BY persona_id ORDER BY fecha), DAY) AS gap\n FROM `autingo-159109.clientes_intel.fact_visita`\n )\n WHERE gap IS NOT NULL GROUP BY persona_id\n ),\n lineas AS (SELECT * FROM `autingo-159109.clientes_intel.fact_transaccion` WHERE persona_id IS NOT NULL),\n mix AS (\n SELECT persona_id,\n COUNT(DISTINCT categoria) AS n_categorias,\n COUNT(DISTINCT centro_nav_id) AS n_centros,\n ROUND(SAFE_DIVIDE(COUNTIF(empresa LIKE 'Aurgi%'), COUNT(*)), 4) AS pct_aurgi,\n ROUND(SAFE_DIVIDE(COUNTIF(empresa LIKE 'MotorTown%'), COUNT(*)), 4) AS pct_motortown,\n ROUND(SAFE_DIVIDE(COUNTIF(canal='web'), COUNT(*)), 4) AS pct_web,\n ROUND(SAFE_DIVIDE(\n SUM(IF(categoria IN ('SERV RAPID', 'REVISION', 'MECANICA'), importe, 0)),\n NULLIF(SUM(IF(categoria NOT IN ('DEVOLUCION') AND NOT (grupo LIKE 'TASA%'), importe, 0)), 0)\n ), 4) AS pct_servicio\n FROM lineas GROUP BY persona_id\n ),\n top_cat AS (\n SELECT persona_id, categoria AS categoria_top, ROUND(monto / NULLIF(tot, 0), 4) AS pct_top_cat\n FROM (\n SELECT persona_id, categoria, SUM(importe) AS monto,\n SUM(SUM(importe)) OVER (PARTITION BY persona_id) AS tot,\n ROW_NUMBER() OVER (PARTITION BY persona_id ORDER BY SUM(importe) DESC) AS rn\n FROM lineas GROUP BY persona_id, categoria\n ) WHERE rn = 1\n ),\n centro_top AS (\n SELECT persona_id, centro_nav_id AS centro_principal, empresa AS empresa_principal\n FROM (\n SELECT persona_id, centro_nav_id, ANY_VALUE(empresa) AS empresa, SUM(importe) AS s,\n ROW_NUMBER() OVER (PARTITION BY persona_id ORDER BY SUM(importe) DESC) AS rn\n FROM lineas GROUP BY persona_id, centro_nav_id\n ) WHERE rn = 1\n ),\n mes_top AS (\n SELECT persona_id, mes AS mes_top_compra FROM (\n SELECT persona_id, EXTRACT(MONTH FROM fecha) AS mes, SUM(importe) AS s,\n ROW_NUMBER() OVER (PARTITION BY persona_id ORDER BY SUM(importe) DESC) AS rn\n FROM lineas GROUP BY persona_id, mes\n ) WHERE rn = 1\n ),\n margen AS (\n SELECT l.persona_id,\n ROUND(SUM(l.importe * COALESCE(r.margen_rate, gf.g)), 2) AS margen_total\n FROM lineas l\n LEFT JOIN `autingo-159109.clientes_intel._margen_rate_producto` r ON l.producto_nav_id = r.prod_nav_id\n CROSS JOIN (SELECT AVG(margen_rate) AS g FROM `autingo-159109.clientes_intel._margen_rate_producto`) gf\n GROUP BY persona_id\n ),\n veh AS (\n SELECT persona_id, COUNT(*) AS n_vehiculos FROM `autingo-159109.clientes_intel.map_persona_vehiculo` GROUP BY persona_id\n ),\n feats AS (\n SELECT\n rfm.persona_id,\n rfm.recency_days, rfm.frequency, rfm.monetary_total, rfm.ticket_medio, rfm.ticket_std,\n rfm.tenure_days, rfm.primera_compra, rfm.ultima_compra, rfm.valor_ponderado,\n iv.mean_inter_visit_days, iv.std_inter_visit_days,\n mix.n_categorias, mix.n_centros, mix.pct_aurgi, mix.pct_motortown, mix.pct_web, mix.pct_servicio,\n tc.categoria_top, tc.pct_top_cat,\n cp.centro_principal, cp.empresa_principal,\n mt.mes_top_compra,\n mg.margen_total,\n COALESCE(v.n_vehiculos, 0) AS n_vehiculos,\n rfm.persona_id IN (SELECT persona_id FROM excluir_personas) AS es_excluido\n FROM rfm\n LEFT JOIN inter iv USING (persona_id)\n LEFT JOIN mix USING (persona_id)\n LEFT JOIN top_cat tc USING (persona_id)\n LEFT JOIN centro_top cp USING (persona_id)\n LEFT JOIN mes_top mt USING (persona_id)\n LEFT JOIN margen mg USING (persona_id)\n LEFT JOIN veh v USING (persona_id)\n ),\n scored AS (\n SELECT persona_id,\n NTILE(5) OVER (ORDER BY recency_days DESC) AS r_score,\n NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,\n NTILE(5) OVER (ORDER BY monetary_total ASC) AS m_score\n FROM feats\n WHERE NOT es_excluido AND frequency >= 1\n )\n SELECT\n f.*,\n s.r_score, s.f_score, s.m_score,\n s.r_score * 100 + s.f_score * 10 + s.m_score AS rfm_score,\n s.r_score + s.f_score + s.m_score AS rfm_sum,\n \n CASE\n WHEN f.recency_days > 730 THEN 'Dormidos perdidos'\n WHEN f.recency_days > 548 AND s.m_score >= 3 THEN 'Dormidos valiosos'\n WHEN f.recency_days > 548 THEN 'Dormidos perdidos'\n WHEN f.recency_days > 243 THEN 'En riesgo / necesitan atencion'\n WHEN s.f_score >= 4 AND s.m_score >= 4 THEN 'Campeones'\n WHEN s.f_score >= 3 THEN 'Fieles'\n WHEN f.tenure_days <= 180 THEN 'Nuevos'\n ELSE 'Ocasional'\n END\n AS rfm_segment,\n ROUND(SAFE_DIVIDE(f.margen_total, NULLIF(f.monetary_total, 0)), 4) AS margen_pct,\n IF(f.pct_web > 0.5, 'web', 'tienda') AS canal_preferido,\n -- contactabilidad / RGPD (de dim_persona)\n dp.full_name, dp.email, dp.phone, dp.es_empresa, dp.confianza AS confianza_identidad,\n dp.num_fuentes,\n dp.email IS NOT NULL AS tiene_email,\n dp.phone IS NOT NULL AS tiene_telefono,\n dp.email_optout, dp.sms_optout, dp.whatsapp_optout, dp.do_not_call,\n (dp.email IS NOT NULL AND NOT dp.email_optout) OR (dp.phone IS NOT NULL AND NOT dp.do_not_call) AS contactable\n FROM feats f\n LEFT JOIN scored s USING (persona_id)\n LEFT JOIN `autingo-159109.clientes_intel.dim_persona` dp USING (persona_id)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 09:42:05.485000+00:00", + "refs": [ + "clientes_intel._margen_rate_producto", + "clientes_intel.dim_persona", + "clientes_intel.fact_transaccion", + "clientes_intel.fact_visita", + "clientes_intel.map_persona_fuente", + "clientes_intel.map_persona_vehiculo", + "psql_dcpublic.tpv_customers" + ] + }, + "feat_cliente_vehiculo": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n CLUSTER BY make, vehiculo_id AS\n WITH dimv AS (\n SELECT *, IF(REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$'), ((STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),5,1))-1)*400 + (STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),6,1))-1)*20 + (STRPOS('BCDFGHJKLMNPRSTVWXYZ', SUBSTR(UPPER(REGEXP_REPLACE(TRIM(plate_norm), r'[\\s\\-]', '')),7,1))-1)), NULL) AS serie_ord FROM `autingo-159109.clientes_intel.dim_vehiculo`\n ),\n tx AS (\n SELECT\n vehiculo_id,\n COUNT(DISTINCT fecha) AS n_visitas,\n COUNT(*) AS n_lineas,\n ROUND(SUM(importe), 2) AS importe_total,\n MIN(fecha) AS primera_fecha,\n MAX(fecha) AS ultima_fecha,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), MAX(fecha), DAY) AS recency_days,\n COUNT(DISTINCT categoria) AS n_categorias,\n ROUND(SAFE_DIVIDE(\n SUM(IF(categoria IN ('SERV RAPID', 'REVISION', 'MECANICA'), importe, 0)),\n NULLIF(SUM(IF(categoria NOT IN ('DEVOLUCION') AND NOT (grupo LIKE 'TASA%'), importe, 0)), 0)\n ), 4) AS pct_servicio,\n MAX(IF(categoria='NEUMATICOS', fecha, NULL)) AS ult_neumatico,\n MAX(IF(categoria='REVISION', fecha, NULL)) AS ult_revision,\n MAX(IF(categoria='BATERIAS', fecha, NULL)) AS ult_bateria,\n MAX(IF(categoria='LUBRICA', fecha, NULL)) AS ult_aceite,\n MAX(IF(categoria='FRICCION', fecha, NULL)) AS ult_frenos,\n MAX(IF(REGEXP_CONTAINS(UPPER(producto_desc), r'ITV'), fecha, NULL)) AS ult_preitv,\n COUNTIF(categoria='NEUMATICOS') AS n_lineas_neumatico\n FROM `autingo-159109.clientes_intel.fact_transaccion` WHERE vehiculo_id IS NOT NULL\n GROUP BY vehiculo_id\n ),\n owners AS (\n -- PARCHE solo-mutua: en matriculas de mutua, el dueno es el conductor real recuperado\n SELECT o.vehiculo_id, o.n_personas,\n COALESCE(mm.particular_persona_id, o.persona_actual_orig) AS persona_actual\n FROM (\n SELECT plate_norm AS vehiculo_id,\n COUNT(DISTINCT persona_id) AS n_personas,\n ARRAY_AGG(persona_id ORDER BY ultima_fecha DESC NULLS LAST LIMIT 1)[SAFE_OFFSET(0)] AS persona_actual_orig\n FROM `autingo-159109.clientes_intel.map_persona_vehiculo` GROUP BY plate_norm\n ) o\n LEFT JOIN `autingo-159109.clientes_intel.map_mutualista_particular` mm\n ON mm.plate_norm = o.vehiculo_id AND mm.es_mutualista\n ),\n feat AS (\n SELECT\n d.plate_norm AS vehiculo_id, d.license_plate,\n -- marca/modelo/combustible: TPV/OTR rellenado con TecRMI (tec_*)\n COALESCE(d.make, d.tec_marca) AS make,\n COALESCE(d.model, d.tec_modelo) AS model,\n d.model_family,\n COALESCE(d.fuel, d.tec_combustible) AS fuel,\n d.tyre_size,\n -- year declarado (saneado): year crudo es escaso (~13%) y con basura\n IF(d.year BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), d.year, NULL) AS year_declarado,\n -- year estimado desde la serie de la matricula (cubre ~97% de modernas)\n pyc.anio_estimado AS anio_estimado_matricula,\n -- year real de matriculacion (TecRMI: 1a matricula, fallback matricula)\n IF(EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)) BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)), NULL) AS anio_real_matricula,\n -- year final: real TecRMI > declarado > estimado por matricula\n COALESCE(\n IF(EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)) BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)), NULL),\n IF(d.year BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), d.year, NULL),\n pyc.anio_estimado\n ) AS anio_matricula,\n CASE\n WHEN EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)) BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) THEN 'tecrmi'\n WHEN d.year BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) THEN 'declarado'\n WHEN pyc.anio_estimado IS NOT NULL THEN 'estimado_matricula'\n ELSE NULL\n END AS fuente_anio,\n EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - COALESCE(\n IF(EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)) BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), EXTRACT(YEAR FROM COALESCE(d.tec_fecha_1a_matricula, d.tec_fecha_matricula)), NULL),\n IF(d.year BETWEEN 1950 AND EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)), d.year, NULL),\n pyc.anio_estimado\n ) AS antiguedad_anios,\n tx.n_visitas, tx.n_lineas, tx.importe_total,\n tx.primera_fecha, tx.ultima_fecha, tx.recency_days,\n tx.n_categorias, tx.pct_servicio, tx.n_lineas_neumatico,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_neumatico, DAY) AS dias_desde_neumatico,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_revision, DAY) AS dias_desde_revision,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_bateria, DAY) AS dias_desde_bateria,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_aceite, DAY) AS dias_desde_aceite,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_frenos, DAY) AS dias_desde_frenos,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), tx.ult_preitv, DAY) AS dias_desde_preitv,\n COALESCE(o.n_personas, 0) AS n_personas,\n o.persona_actual,\n -- Ficha tecnica TecRMI (ejes de targeting de promos personalizadas)\n d.vin,\n d.tec_combustible,\n d.tec_modelo_generico,\n d.tec_cilindrada, d.tec_potencia_kw, d.tec_potencia_fiscal, d.tec_co2,\n d.tec_segmento, d.tec_tipo, d.tec_plazas,\n d.tec_fecha_matricula, d.tec_fecha_1a_matricula\n FROM dimv d\n LEFT JOIN tx ON tx.vehiculo_id = d.plate_norm\n LEFT JOIN owners o ON o.vehiculo_id = d.plate_norm\n LEFT JOIN `autingo-159109.clientes_intel._plate_year_calib` pyc ON pyc.serie_ord = d.serie_ord\n )\n SELECT\n feat.*,\n -- combustible normalizado para targeting (unifica casing TPV 'DIESEL' vs TecRMI 'Diesel')\n CASE\n WHEN UPPER(feat.fuel) LIKE '%DIESEL%' OR UPPER(feat.fuel) LIKE '%GASOLEO%' OR UPPER(feat.fuel) LIKE '%GASÓLEO%' OR UPPER(feat.fuel) = 'IESEL' THEN 'Diesel'\n WHEN UPPER(feat.fuel) LIKE '%GASOLINA%' OR UPPER(feat.fuel) = 'ASOLINA' OR UPPER(feat.fuel) = 'ETANOL' THEN 'Gasolina'\n WHEN UPPER(feat.fuel) LIKE '%HIBRID%' OR UPPER(feat.fuel) LIKE '%HÍBRID%' THEN 'Hibrido'\n WHEN UPPER(feat.fuel) LIKE '%ELECTRIC%' OR UPPER(feat.fuel) LIKE '%ELÉCTRIC%' THEN 'Electrico'\n WHEN UPPER(feat.fuel) LIKE '%LICUADO DE PETRO%' OR UPPER(feat.fuel) LIKE '%GLP%' OR UPPER(feat.fuel) LIKE '%BUTANO%' THEN 'GLP'\n WHEN UPPER(feat.fuel) LIKE '%GAS NATURAL%' OR UPPER(feat.fuel) LIKE '%GNC%' THEN 'GNC'\n ELSE NULL\n END AS combustible,\n -- ITV (calendario legal turismos): exento <4 anios, bienal 4-10, anual >10\n CASE\n WHEN anio_matricula IS NULL THEN NULL\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula < 4 THEN 'exento'\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula < 10 THEN 'bienal'\n ELSE 'anual'\n END AS itv_periodicidad,\n -- anio estimado de la proxima ITV (resolucion anual)\n CASE\n WHEN anio_matricula IS NULL THEN NULL\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula <= 4 THEN anio_matricula + 4\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula < 10 THEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) + MOD(EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula, 2)\n ELSE EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`))\n END AS proxima_itv_anio,\n CASE\n WHEN anio_matricula IS NULL THEN NULL\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula < 4 THEN FALSE\n WHEN EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula < 10 THEN MOD(EXTRACT(YEAR FROM (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)) - anio_matricula, 2) = 0\n ELSE TRUE\n END AS itv_due_este_anio\n FROM feat", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 09:42:22.391000+00:00", + "refs": [ + "clientes_intel._plate_year_calib", + "clientes_intel.dim_persona", + "clientes_intel.dim_vehiculo", + "clientes_intel.fact_transaccion", + "clientes_intel.map_persona_vehiculo" + ] + }, + "map_mutualista_particular": { + "query": "CREATE OR REPLACE VIEW `autingo-159109.clientes_intel.map_mutualista_particular`\nOPTIONS(\n description=\"Mapeo matricula -> conductor particular real para coches cuya persona principal es una cuenta agregadora (mutua/flota: es_empresa en dim_persona y >=20 matriculas en map_persona_vehiculo). Recupera al particular via citaprevia/otr/visita, eligiendo el de actividad mas reciente. es_mutualista=TRUE si el agregador es una mutua/aseguradora. NO depende de seg_cliente_360 (se puede usar dentro de F8). Generado a mano.\"\n) AS\nWITH veh_count AS (\n -- propiedad TPV cruda (estable: la construye F2 desde origen, NO la tocan los parches F3/F4).\n -- Evita la dependencia circular: si contaramos persona_actual, la reasignacion se autodesharia.\n SELECT persona_id, COUNTIF(via_owner_tpv) AS nveh\n FROM `autingo-159109.clientes_intel.map_persona_vehiculo`\n GROUP BY persona_id\n),\nagg AS (\n SELECT dp.persona_id AS agg_id, dp.full_name AS agg_nombre,\n CASE WHEN REGEXP_CONTAINS(UPPER(dp.full_name), r'MUTUA|ASEGURAD|\\bSEGUROS\\b|MAPFRE|ALLIANZ|\\bAXA\\b|GENERALI|ZURICH|REALE|ASITUR|MULTIASIST|LINEA DIRECTA|PELAYO SEG|CATALANA OCC') THEN 'MUTUA' ELSE 'FLOTA' END AS tipo_origen\n FROM `autingo-159109.clientes_intel.dim_persona` dp\n JOIN veh_count vc ON dp.persona_id = vc.persona_id\n WHERE dp.es_empresa AND vc.nveh >= 20\n),\nplates AS (\n SELECT DISTINCT m.persona_id AS agg_id, m.plate_norm\n FROM `autingo-159109.clientes_intel.map_persona_vehiculo` m\n JOIN agg ON m.persona_id = agg.agg_id\n),\ncand AS (\n SELECT\n p.agg_id, p.plate_norm,\n dp.persona_id AS particular_persona_id,\n dp.full_name AS particular_nombre,\n m.ultima_fecha,\n dp.phone IS NOT NULL AS tiene_tel,\n dp.email IS NOT NULL AS tiene_email,\n ROW_NUMBER() OVER (\n PARTITION BY p.plate_norm\n ORDER BY m.ultima_fecha DESC NULLS LAST, m.n_visitas DESC\n ) AS rn\n FROM plates p\n JOIN `autingo-159109.clientes_intel.map_persona_vehiculo` m USING(plate_norm)\n JOIN `autingo-159109.clientes_intel.dim_persona` dp ON m.persona_id = dp.persona_id\n WHERE m.persona_id <> p.agg_id\n AND NOT dp.es_empresa\n AND (m.via_citaprevia OR m.via_otr OR m.via_visita)\n AND dp.full_name IS NOT NULL\n AND TRIM(dp.full_name) NOT IN ('', '.', '-')\n)\nSELECT\n c.plate_norm,\n a.tipo_origen,\n c.agg_id AS agregador_persona_id,\n a.agg_nombre AS agregador_nombre,\n c.particular_persona_id,\n c.particular_nombre,\n c.ultima_fecha AS ultima_actividad_particular,\n c.tiene_tel,\n c.tiene_email,\n (a.tipo_origen = 'MUTUA') AS es_mutualista\nFROM cand c\nJOIN agg a ON c.agg_id = a.agg_id\nWHERE c.rn = 1\n", + "stmt": "CREATE_VIEW", + "last_run": "2026-06-08 15:38:24.674000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.map_persona_vehiculo" + ] + }, + "map_persona_canal8": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.map_persona_canal8` AS\nWITH cc_presup AS (\n SELECT DISTINCT RIGHT(REGEXP_REPLACE(telefonos, r'[^0-9]', ''), 9) AS tel9\n FROM `autingo-159109.psql_dcpublic.call_transactions`\n WHERE telefonos IS NOT NULL\n AND LENGTH(REGEXP_REPLACE(telefonos, r'[^0-9]', '')) >= 9\n AND LOWER(campaign_name) LIKE '%presupuesto%'\n),\nglass AS (\n SELECT DISTINCT persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE empresa IN ('Aurgi Glass', 'MotorTown Glass')\n),\nautingoc AS ( -- compra real en el centro web Autingo\n SELECT DISTINCT persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE centro_nombre = 'Autingo'\n),\nbase AS (\n SELECT s.persona_id, s.es_empresa, s.empresa_principal, s.es_mutualista,\n dp.in_web, dp.in_citaprevia,\n g.persona_id IS NOT NULL AS is_glass,\n au.persona_id IS NOT NULL AS is_autingo,\n RIGHT(REGEXP_REPLACE(COALESCE(s.phone, ''), r'[^0-9]', ''), 9) AS tel9\n FROM `autingo-159109.clientes_intel.seg_cliente_360` s\n LEFT JOIN `autingo-159109.clientes_intel.dim_persona` dp USING (persona_id)\n LEFT JOIN glass g USING (persona_id)\n LEFT JOIN autingoc au USING (persona_id)\n)\nSELECT b.persona_id,\n CASE\n WHEN b.es_empresa THEN 'B2B'\n WHEN b.is_glass THEN 'Lunas'\n WHEN b.es_mutualista = 'Mutualista' OR b.empresa_principal = 'Autoclub' THEN 'Mutuamas/AC'\n -- Autingo: registrado en web (definicion amplia, la mas parecida a la imagen: Drive-in 51 / Autingo 12,6)\n WHEN b.in_web THEN 'Autingo'\n WHEN b.empresa_principal = 'Digital' THEN 'Web'\n WHEN c.tel9 IS NOT NULL AND b.tel9 != '' THEN 'Call Center'\n WHEN b.empresa_principal IN ('Aurgi', 'MotorTown', 'Franquicia') THEN 'Drive-in'\n WHEN b.in_citaprevia THEN 'Citas'\n ELSE 'Drive-in'\n END AS canal8\nFROM base b\nLEFT JOIN cc_presup c ON c.tel9 = b.tel9 AND b.tel9 != ''", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 12:03:06.171000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.fact_transaccion", + "clientes_intel.seg_cliente_360", + "psql_dcpublic.call_transactions" + ] + }, + "map_persona_fuente": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.map_persona_fuente` AS\n SELECT DISTINCT persona_id, src AS fuente, source_id FROM `autingo-159109.clientes_intel._persona_records`", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:47:27.004000+00:00", + "refs": [ + "clientes_intel._persona_records" + ] + }, + "map_persona_vehiculo": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.map_persona_vehiculo` AS\n WITH\n tpv_map AS (SELECT source_id, persona_id FROM `autingo-159109.clientes_intel.map_persona_fuente` WHERE fuente='tpv'),\n otr_map AS (SELECT source_id, persona_id FROM `autingo-159109.clientes_intel.map_persona_fuente` WHERE fuente='otr'),\n cp_map AS (SELECT source_id, persona_id FROM `autingo-159109.clientes_intel.map_persona_fuente` WHERE fuente='citaprevia'),\n rel AS (\n -- 1) Visitas (lineas de venta): la mejor fuente de vigencia\n SELECT m.persona_id, UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')) AS plate_norm,\n DATE(l.fecha) AS fecha, 'visita' AS origen\n FROM `autingo-159109.claude_bi.todos_datos_lineas_mat` l\n JOIN tpv_map m ON CAST(l.Tpv_Customers___Customer__id AS STRING) = m.source_id\n WHERE l.fecha IS NOT NULL AND (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(l.otrs_unicas_por_order__plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n UNION ALL\n -- 2) Propiedad TPV\n SELECT m.persona_id, UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')), DATE(vo.created_at), 'owner_tpv'\n FROM `autingo-159109.psql_dcpublic.tpv_vehicles_vehicleowner` vo\n JOIN `autingo-159109.psql_dcpublic.tpv_vehicles_vehicle` v ON v.id = vo.vehicle_id\n JOIN tpv_map m ON CAST(vo.customer_id AS STRING) = m.source_id\n WHERE (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(v.license_plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n UNION ALL\n -- 3) OTR\n SELECT m.persona_id, UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')), DATE(ov.created_at), 'otr'\n FROM `autingo-159109.psql_dcpublic.otr_customers` oc\n JOIN `autingo-159109.psql_dcpublic.otr_vehicles` ov ON oc.car_id = ov.car_id\n JOIN otr_map m ON CAST(oc.id AS STRING) = m.source_id\n WHERE (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(ov.license_plate), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n UNION ALL\n -- 4) Cita previa (sin fecha)\n SELECT m.persona_id, UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')), CAST(NULL AS DATE), 'citaprevia'\n FROM `autingo-159109.citaprevia_aurphcp.clientes_matriculas` cm\n JOIN cp_map m ON CAST(cm.cliente_id AS STRING) = m.source_id\n WHERE cm._fivetran_deleted IS NOT TRUE AND (REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')), r'^[0-9]{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$') OR REGEXP_CONTAINS(UPPER(REGEXP_REPLACE(TRIM(cm.matricula), r'[\\s\\-]', '')), r'^[A-Z]{1,2}[0-9]{4}[A-Z]{1,2}$'))\n )\n SELECT\n persona_id, plate_norm,\n MIN(fecha) AS primera_fecha,\n MAX(fecha) AS ultima_fecha,\n COUNTIF(fecha IS NOT NULL AND origen='visita') AS n_visitas,\n COUNT(*) AS n_eventos,\n LOGICAL_OR(origen='visita') AS via_visita,\n LOGICAL_OR(origen='owner_tpv') AS via_owner_tpv,\n LOGICAL_OR(origen='otr') AS via_otr,\n LOGICAL_OR(origen='citaprevia') AS via_citaprevia\n FROM rel\n GROUP BY persona_id, plate_norm", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:00:21.242000+00:00", + "refs": [ + "citaprevia_aurphcp.clientes_matriculas", + "claude_bi.todos_datos_lineas_mat", + "clientes_intel.map_persona_fuente", + "psql_dcpublic.otr_customers", + "psql_dcpublic.otr_vehicles", + "psql_dcpublic.tpv_vehicles_vehicle", + "psql_dcpublic.tpv_vehicles_vehicleowner" + ] + }, + "reco_acciones": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.reco_acciones`\n CLUSTER BY accion, persona_id AS\n WITH\n -- CAR: check rojo/amarillo no resuelto, 1 accion por matricula (Rojo>Amarillo, mas reciente)\n car_dedup AS (\n SELECT * EXCEPT(rn) FROM (\n SELECT\n UPPER(REGEXP_REPLACE(TRIM(license_plate), r'[\\s\\-]','')) AS vehiculo_id,\n check_color, subcat_cgq, check_subcats, origin_invoice_date,\n ROW_NUMBER() OVER (\n PARTITION BY UPPER(REGEXP_REPLACE(TRIM(license_plate), r'[\\s\\-]',''))\n ORDER BY CASE check_color WHEN 'Rojo' THEN 1 ELSE 2 END, origin_invoice_date DESC\n ) AS rn\n FROM `autingo-159109.claude_bi.conversion_cqg_base_mat`\n WHERE check_color IN ('Rojo','Amarillo') AND converted = 0\n AND origin_invoice_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 6 MONTH)\n ) WHERE rn = 1\n ),\n car AS (\n SELECT\n v.persona_actual AS persona_id, v.vehiculo_id, v.license_plate,\n IF(c.check_color = 'Rojo', 'CAR_ROJO', 'CAR_NARANJA') AS accion,\n c.subcat_cgq AS motivo,\n ARRAY_TO_STRING(c.check_subcats, ', ') AS motivo_subcat,\n c.check_color, c.origin_invoice_date AS fecha_senal\n FROM car_dedup c\n JOIN `autingo-159109.clientes_intel.feat_cliente_vehiculo` v ON v.vehiculo_id = c.vehiculo_id\n WHERE v.persona_actual IS NOT NULL\n ),\n -- ITV: due este anio sin pre-ITV previo (o pre-ITV de hace mas de 1 anio)\n itv AS (\n SELECT persona_actual AS persona_id, vehiculo_id, license_plate,\n 'ITV' AS accion,\n CONCAT('ITV ', CAST(proxima_itv_anio AS STRING)) AS motivo,\n CAST(NULL AS STRING) AS motivo_subcat,\n CAST(NULL AS STRING) AS check_color,\n CAST(NULL AS DATE) AS fecha_senal\n FROM `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n WHERE persona_actual IS NOT NULL AND itv_due_este_anio\n AND (dias_desde_preitv IS NULL OR dias_desde_preitv > 365)\n ),\n -- Recencias de servicio (1 fila por servicio vencido)\n recencias AS (\n SELECT persona_actual AS persona_id, vehiculo_id, license_plate,\n s.accion, s.motivo, CAST(NULL AS STRING) AS motivo_subcat, CAST(NULL AS STRING) AS check_color,\n DATE_SUB(CURRENT_DATE(), INTERVAL s.dias DAY) AS fecha_senal\n FROM `autingo-159109.clientes_intel.feat_cliente_vehiculo` v,\n UNNEST([\n STRUCT('REVISION' AS accion, 'Revision general' AS motivo, v.dias_desde_revision AS dias),\n STRUCT('ACEITE', 'Cambio de aceite', v.dias_desde_aceite),\n STRUCT('FRENOS', 'Frenos', v.dias_desde_frenos),\n STRUCT('NEUMATICO', 'Neumaticos', v.dias_desde_neumatico),\n STRUCT('BATERIA', 'Bateria', v.dias_desde_bateria)\n ]) s\n WHERE v.persona_actual IS NOT NULL AND s.dias > CASE s.accion\n WHEN 'REVISION' THEN 365\n WHEN 'ACEITE' THEN 365\n WHEN 'FRENOS' THEN 730\n WHEN 'NEUMATICO' THEN 1095\n WHEN 'BATERIA' THEN 1095\n END\n ),\n -- Win-back: cliente valioso en riesgo de fuga (churn XGB) -> a nivel persona\n winback AS (\n SELECT\n m.persona_id, CAST(NULL AS STRING) AS vehiculo_id, CAST(NULL AS STRING) AS license_plate,\n 'WINBACK' AS accion,\n CONCAT('Riesgo de fuga ', c.segmento_riesgo) AS motivo,\n CAST(NULL AS STRING) AS motivo_subcat,\n CAST(NULL AS STRING) AS check_color, CAST(NULL AS DATE) AS fecha_senal\n FROM `autingo-159109.claude_bi.churn_scores_current` c\n JOIN `autingo-159109.clientes_intel.map_persona_fuente` m ON m.fuente='tpv' AND m.source_id = CAST(c.customer_id AS STRING)\n JOIN `autingo-159109.clientes_intel.score_clv` clv ON clv.persona_id = m.persona_id\n WHERE c.segmento_riesgo IN ('muy_alto', 'alto')\n AND clv.clv_norm >= 0.5\n ),\n senales AS (\n SELECT * FROM car\n UNION ALL SELECT * FROM itv\n UNION ALL SELECT * FROM recencias\n UNION ALL SELECT * FROM winback\n )\n SELECT\n s.persona_id, s.vehiculo_id, s.license_plate,\n s.accion, s.motivo, s.motivo_subcat, s.check_color, s.fecha_senal,\n COALESCE(clv.clv_norm, 0.0) AS clv_norm,\n clv.clv_score,\n CASE accion WHEN 'CAR_ROJO' THEN 100 WHEN 'WINBACK' THEN 90 WHEN 'ITV' THEN 80 WHEN 'CAR_NARANJA' THEN 60 WHEN 'REVISION' THEN 45 WHEN 'ACEITE' THEN 42 WHEN 'FRENOS' THEN 35 WHEN 'NEUMATICO' THEN 32 WHEN 'BATERIA' THEN 25 ELSE 0 END AS peso_accion,\n ROUND(CASE accion WHEN 'CAR_ROJO' THEN 100 WHEN 'WINBACK' THEN 90 WHEN 'ITV' THEN 80 WHEN 'CAR_NARANJA' THEN 60 WHEN 'REVISION' THEN 45 WHEN 'ACEITE' THEN 42 WHEN 'FRENOS' THEN 35 WHEN 'NEUMATICO' THEN 32 WHEN 'BATERIA' THEN 25 ELSE 0 END * (0.5 + 0.5 * COALESCE(clv.clv_norm, 0.0)), 4) AS prioridad_score,\n dp.canal_entrada,\n dp.contactable_email, dp.contactable_telefono,\n dp.email_optout, dp.sms_optout, dp.do_not_call\n FROM senales s\n JOIN `autingo-159109.clientes_intel.data_points_contacto` dp ON dp.persona_id = s.persona_id AND dp.es_b2c\n LEFT JOIN `autingo-159109.clientes_intel.score_clv` clv ON clv.persona_id = s.persona_id;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:36:04.548000+00:00", + "refs": [ + "claude_bi.churn_scores_current", + "claude_bi.conversion_cqg_base_mat", + "clientes_intel.data_points_contacto", + "clientes_intel.feat_cliente_vehiculo", + "clientes_intel.map_persona_fuente", + "clientes_intel.score_clv" + ] + }, + "reco_promo_cluster": { + "query": "DROP TABLE IF EXISTS clientes_intel.reco_promo_cluster", + "stmt": "DROP_TABLE", + "last_run": "2026-06-03 12:00:25.955000+00:00", + "refs": [] + }, + "reco_promo_personalizada": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.reco_promo_personalizada`\n CLUSTER BY categoria_objetivo, persona_id AS\n WITH acciones AS (\n SELECT\n r.persona_id, r.vehiculo_id, r.license_plate,\n r.accion, r.motivo, r.motivo_subcat,\n r.prioridad_score, r.clv_norm,\n r.contactable_email, r.contactable_telefono, r.do_not_call,\n \n CASE\n WHEN r.accion = 'NEUMATICO' THEN 'NEUMATICOS'\n WHEN r.accion = 'FRENOS' THEN 'FRICCION'\n WHEN r.accion = 'ACEITE' THEN 'LUBRICA'\n WHEN r.accion = 'REVISION' THEN 'REVISION'\n WHEN r.accion = 'BATERIA' THEN 'BATERIAS'\n WHEN r.accion = 'ITV' THEN 'REVISION'\n WHEN r.accion IN ('CAR_ROJO','CAR_NARANJA') THEN CASE\n WHEN r.motivo = 'Neumaticos' THEN 'NEUMATICOS'\n WHEN r.motivo = 'Lunas' THEN 'LUNAS'\n WHEN r.motivo_subcat LIKE '%Friccion%' THEN 'FRICCION'\n WHEN r.motivo_subcat LIKE '%Iluminacion%' THEN 'ILUMINACIO'\n WHEN r.motivo IN ('Mantenimiento','Tienda') THEN 'LUBRICA'\n ELSE NULL\n END\n ELSE NULL\n END\n AS categoria_objetivo\n FROM `autingo-159109.clientes_intel.reco_acciones` r\n WHERE r.contactable_email OR r.contactable_telefono\n ),\n -- mejor promo activa por (segmento RFM, categoria), no colectiva.\n -- match_fuerte = el nombre contiene la palabra canonica de su categoria (evita\n -- falsos positivos del etiquetado por keyword, ej. 'FIBRA NATURAL' -> REVISION).\n promo_cat AS (\n SELECT segmento AS rfm_segment, categoria_promo, promo_id, promo_name, label AS promo_label,\n fin_promo, afinidad, palanca,\n ROW_NUMBER() OVER (\n PARTITION BY segmento, categoria_promo\n ORDER BY match_fuerte DESC, afinidad DESC, fin_promo\n ) AS rk\n FROM (\n SELECT *,\n CASE categoria_promo\n WHEN 'NEUMATICOS' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'NEU|RUEDA|EQUILIBRAD|VALVULA'), 1, 0)\n WHEN 'REVISION' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'REVISION|MANTENIMIENTO|ITV|TODO INCLUIDO'), 1, 0)\n WHEN 'LUBRICA' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'ACEITE|LUBRICA'), 1, 0)\n WHEN 'FRICCION' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'FRENO|PASTILLA|FRICCION|DISCO'), 1, 0)\n WHEN 'BATERIAS' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'BATERIA'), 1, 0)\n WHEN 'ILUMINACIO' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'ILUMINAC|BOMBILLA|FARO|LED'), 1, 0)\n WHEN 'LUNAS' THEN IF(REGEXP_CONTAINS(UPPER(promo_name), r'LUNA|CRISTAL|PARABRISA'), 1, 0)\n ELSE 0\n END AS match_fuerte\n FROM `autingo-159109.clientes_intel.reco_promo_segmento`\n WHERE esquema = 'RFM' AND NOT es_colectivo\n )\n ),\n base AS (\n SELECT\n a.persona_id, a.vehiculo_id, a.license_plate,\n a.accion, a.motivo, a.motivo_subcat, a.categoria_objetivo,\n a.prioridad_score, a.clv_norm,\n s.rfm_segment, s.full_name, s.email, s.phone,\n v.make, v.model, v.model_family, v.fuel, v.tyre_size, v.anio_matricula,\n pc.promo_id, pc.promo_name, pc.promo_label, pc.fin_promo, pc.afinidad, pc.palanca,\n a.contactable_email, a.contactable_telefono,\n CASE\n WHEN a.contactable_email THEN 'EMAIL'\n WHEN a.contactable_telefono AND NOT a.do_not_call THEN 'SMS'\n ELSE NULL\n END AS canal_recomendado,\n ROW_NUMBER() OVER (PARTITION BY a.persona_id ORDER BY a.prioridad_score DESC) AS rk_persona\n FROM acciones a\n JOIN `autingo-159109.clientes_intel.seg_cliente_360` s ON s.persona_id = a.persona_id\n LEFT JOIN `autingo-159109.clientes_intel.feat_cliente_vehiculo` v ON v.vehiculo_id = a.vehiculo_id\n LEFT JOIN promo_cat pc ON pc.rfm_segment = s.rfm_segment\n AND pc.categoria_promo = a.categoria_objetivo AND pc.rk = 1\n WHERE a.categoria_objetivo IS NOT NULL\n )\n SELECT\n * EXCEPT(rk_persona),\n rk_persona = 1 AS top_persona,\n CONCAT(\n accion, ' / ', categoria_objetivo,\n IF(make IS NOT NULL, CONCAT(' (', make, ' ', COALESCE(model, ''), ')'), ''),\n ' -> ', COALESCE(promo_name, 'sin promo activa de esta categoria')\n ) AS mensaje,\n TRUE AS envio_habilitado,\n CAST(NULL AS STRING) AS bloqueo_motivo\n FROM base\n WHERE canal_recomendado IS NOT NULL;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 08:21:04.866000+00:00", + "refs": [ + "clientes_intel.feat_cliente_vehiculo", + "clientes_intel.reco_acciones", + "clientes_intel.reco_promo_segmento", + "clientes_intel.seg_cliente_360" + ] + }, + "reco_promo_segmento": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.reco_promo_segmento`\n CLUSTER BY esquema, segmento AS\n WITH seg_long AS (\n SELECT persona_id, 'RFM' AS esquema, rfm_segment AS segmento FROM `autingo-159109.clientes_intel.seg_cliente_360` WHERE rfm_segment IS NOT NULL\n UNION ALL\n SELECT persona_id, 'CLV', clv_nivel FROM `autingo-159109.clientes_intel.seg_cliente_360` WHERE clv_nivel IN ('Alto','Medio','Bajo')\n UNION ALL\n SELECT persona_id, 'CL', \n CASE cluster\n WHEN '0' THEN 'Generalista gasto medio'\n WHEN '1' THEN 'Fieles alto valor'\n WHEN '2' THEN 'Coche reciente bajo gasto'\n WHEN '3' THEN 'Coche antiguo bajo gasto'\n WHEN '4' THEN 'Coche premium (lujo)'\n END\n FROM `autingo-159109.clientes_intel.seg_cliente_360` WHERE cluster IS NOT NULL\n ),\n afinidad AS (\n SELECT sl.esquema, sl.segmento, t.categoria,\n SAFE_DIVIDE(SUM(t.importe), SUM(SUM(t.importe)) OVER (PARTITION BY sl.esquema, sl.segmento)) AS pct_gasto\n FROM `autingo-159109.clientes_intel.fact_transaccion` t\n JOIN seg_long sl USING (persona_id)\n WHERE t.categoria IS NOT NULL AND t.importe > 0\n GROUP BY sl.esquema, sl.segmento, t.categoria\n ),\n segmentos AS (SELECT DISTINCT esquema, segmento FROM seg_long WHERE segmento IS NOT NULL),\n promos AS (\n SELECT id AS promo_id, name AS promo_name, label, DATE(ending_date) AS fin_promo,\n \n CASE\n WHEN REGEXP_CONTAINS(UPPER(name), r'ESCOBILLA') THEN 'ESCOBILLAS'\n WHEN REGEXP_CONTAINS(UPPER(name), r'NEU|NEUMATIC|EQUILIBRAD|VALVULA|RUEDA') THEN 'NEUMATICOS'\n WHEN REGEXP_CONTAINS(UPPER(name), r'REVISION|MANTENIMIENTO|TODO INCLUIDO|INFINITY|ADVANCE|NATURA') THEN 'REVISION'\n WHEN REGEXP_CONTAINS(UPPER(name), r'FILTRO') THEN 'FILTROS'\n WHEN REGEXP_CONTAINS(UPPER(name), r'BATERIA') THEN 'BATERIAS'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ACEITE|LUBRICA') THEN 'LUBRICA'\n WHEN REGEXP_CONTAINS(UPPER(name), r'FRENO|PASTILLA|FRICCION|DISCO') THEN 'FRICCION'\n WHEN REGEXP_CONTAINS(UPPER(name), r'LUNA|LLUVIA|CRISTAL|PARABRISA') THEN 'LUNAS'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ILUMINAC|BOMBILLA|FARO|LED') THEN 'ILUMINACIO'\n ELSE 'GENERAL'\n END\n AS categoria_promo, \n CASE\n WHEN REGEXP_CONTAINS(UPPER(name), r'PINCHAZO') THEN 'Pinchazo'\n WHEN REGEXP_CONTAINS(UPPER(name), r'PASTILLA') THEN 'Pastillas'\n WHEN REGEXP_CONTAINS(UPPER(name), r'DISCO') THEN 'Discos'\n WHEN REGEXP_CONTAINS(UPPER(name), r'LIQUIDO FRENO|DOT4|DOT 4') THEN 'Liquido de frenos'\n WHEN REGEXP_CONTAINS(UPPER(name), r'EQUILIBRAD|VALVULA|MONTAJE') THEN 'Montaje/equilibrado'\n WHEN REGEXP_CONTAINS(UPPER(name), r'NEUMATIC|RUEDA') THEN 'Neumaticos'\n WHEN REGEXP_CONTAINS(UPPER(name), r'BATERIA') THEN 'Bateria'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ILUMINAC|BOMBILLA|FARO|\\bLED\\b') THEN 'Iluminacion'\n WHEN REGEXP_CONTAINS(UPPER(name), r'LAVAPARABRISAS|PARABRISA|\\bLUNA|CRISTAL') THEN 'Lunas/cristales'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ESCOBILLA') THEN 'Escobillas'\n WHEN REGEXP_CONTAINS(UPPER(name), r'FILTRO') THEN 'Filtros'\n WHEN REGEXP_CONTAINS(UPPER(name), r'CARGA AIRE|R1234|R134|AIRE ACOND|CLIMA') THEN 'Aire acondicionado'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ANTICONGELANTE') THEN 'Anticongelante'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ACEITE|LUBRICA') THEN 'Aceite'\n WHEN REGEXP_CONTAINS(UPPER(name), r'ITV') THEN 'ITV'\n WHEN REGEXP_CONTAINS(UPPER(name), r'REVISION|\\bREV\\b|REVISIONE') THEN 'Revision'\n WHEN REGEXP_CONTAINS(UPPER(name), r'CUPON|PROXIMA COMPRA') THEN 'Cupon/fidelizacion'\n WHEN REGEXP_CONTAINS(UPPER(name), r'GRATIS|REGALO|CAMISETA|AMBIENTADOR|CUBREASIENTO|OZONO') THEN 'Regalo/obsequio'\n ELSE 'Otros'\n END\n AS subcategoria_promo, REGEXP_CONTAINS(UPPER(name), r'COLECTIVO|FLOTA|EMPLEAD|GESCAB|TALRENT|INTERDATA|CARREFOUR|GRUPO COLECTIVO') AS es_colectivo\n FROM `autingo-159109.psql_dcpublic.promotions`\n WHERE status = 1 AND starting_date <= CURRENT_TIMESTAMP() AND ending_date >= CURRENT_TIMESTAMP()\n ),\n cruce AS (\n SELECT s.esquema, s.segmento, p.promo_id, p.promo_name, p.label, p.fin_promo,\n p.categoria_promo, p.subcategoria_promo, p.es_colectivo,\n CASE WHEN p.categoria_promo = 'GENERAL' THEN 0.10 ELSE COALESCE(a.pct_gasto, 0.0) END AS afinidad\n FROM segmentos s\n CROSS JOIN promos p\n LEFT JOIN afinidad a ON a.esquema = s.esquema AND a.segmento = s.segmento AND a.categoria = p.categoria_promo\n )\n SELECT *,\n -- palanca de comunicacion del arquetipo (deck Ciclo de vida); solo aplica al esquema CL\n CASE WHEN esquema = 'CL' THEN CASE segmento\n WHEN 'Generalista gasto medio' THEN 'RETENCION'\n WHEN 'Fieles alto valor' THEN 'VOLUMEN'\n WHEN 'Coche reciente bajo gasto' THEN 'FRECUENCIA'\n WHEN 'Coche antiguo bajo gasto' THEN 'PREVENCION'\n WHEN 'Coche premium (lujo)' THEN 'DIFERENCIACION'\n END END AS palanca,\n ROW_NUMBER() OVER (PARTITION BY esquema, segmento, es_colectivo ORDER BY afinidad DESC, fin_promo) AS rank_en_segmento\n FROM cruce;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 11:58:03.947000+00:00", + "refs": [ + "clientes_intel.fact_transaccion", + "clientes_intel.seg_cliente_360", + "psql_dcpublic.promotions" + ] + }, + "rpt_campana": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.rpt_campana` CLUSTER BY canal AS\n WITH \n resp AS (\n SELECT persona_id, canal, campana, ultima_fecha AS fecha_envio,\n abrio, clico, n_enviados, n_entregados, n_rebotados, n_baja\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta` WHERE ultima_fecha IS NOT NULL\n ),\n lines AS (\n SELECT r.*, t.fecha AS tx_fecha, t.importe,\n (t.fecha > r.fecha_envio AND t.fecha <= DATE_ADD(r.fecha_envio, INTERVAL 30 DAY)) AS in_win\n FROM resp r\n LEFT JOIN `autingo-159109.clientes_intel.fact_transaccion` t ON t.persona_id = r.persona_id AND t.fecha >= '2025-01-01'\n ),\n ranked AS (\n SELECT *,\n MIN(IF(in_win, tx_fecha, NULL)) OVER (PARTITION BY canal, campana, persona_id) AS first_buy\n FROM lines\n ),\n per AS (\n SELECT canal, campana, persona_id,\n ANY_VALUE(abrio) AS abrio, ANY_VALUE(clico) AS clico,\n ANY_VALUE(n_enviados) AS n_enviados, ANY_VALUE(n_entregados) AS n_entregados,\n ANY_VALUE(n_rebotados) AS n_rebotados, ANY_VALUE(n_baja) AS n_baja,\n ANY_VALUE(fecha_envio) AS fecha_envio,\n MAX(IF(in_win, 1, 0)) AS compro,\n COUNTIF(in_win) AS compras,\n SUM(IF(in_win, importe, 0)) AS eur,\n SUM(IF(in_win AND tx_fecha = first_buy, importe, 0)) AS eur_primera,\n SUM(IF(in_win AND tx_fecha > first_buy, importe, 0)) AS eur_siguientes\n FROM ranked\n GROUP BY canal, campana, persona_id\n )\n SELECT\n canal, campana,\n MAX(fecha_envio) AS fecha_campana,\n COUNT(*) AS personas_impactadas,\n SUM(n_enviados) AS enviados,\n SUM(n_entregados) AS entregados,\n SUM(n_rebotados) AS rebotados,\n SUM(n_baja) AS bajas,\n COUNTIF(abrio) AS personas_abrio,\n COUNTIF(clico) AS personas_clico,\n ROUND(AVG(CAST(abrio AS INT64)), 4) AS tasa_or,\n ROUND(AVG(CAST(clico AS INT64)), 4) AS tasa_ctr,\n ROUND(SAFE_DIVIDE(SUM(n_entregados), SUM(n_enviados)), 4) AS tasa_entregas,\n ROUND(SAFE_DIVIDE(SUM(n_rebotados), SUM(n_enviados)), 4) AS tasa_rebote,\n COUNTIF(compro = 1) AS personas_conversion,\n ROUND(AVG(compro), 4) AS tasa_conversion,\n SUM(compras) AS compras_totales,\n ROUND(SUM(eur), 0) AS ingresos,\n ROUND(SUM(eur_primera), 0) AS ingresos_primera_compra,\n ROUND(SUM(eur_siguientes), 0) AS ingresos_siguientes,\n ROUND(SAFE_DIVIDE(SUM(eur), COUNT(*)), 2) AS ingreso_por_impactado\n FROM per GROUP BY canal, campana", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 11:07:07.488000+00:00", + "refs": [ + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion" + ] + }, + "rpt_campana_lift": { + "query": "-- rpt_campana_lift: conversion incremental de campanas email (Salesforce) sobre venta TPV.\n-- Universo de medida = ABRIDORES (entraron a verlo). Ventana 30 dias antes/despues del envio.\n-- Outcome producto = compra de la categoria de la campana (clasificada por nombre).\n-- Outcome compra = compra de cualquier cosa (sirve para campanas genericas sin producto).\n-- Ingreso 1a compra = FACTURA COMPLETA del primer dia de compra post-envio.\n-- DiD = (abr_post - abr_pre) - (noabr_post - noabr_pre) en puntos porcentuales.\nCREATE OR REPLACE TABLE `autingo-159109.clientes_intel.rpt_campana_lift`\nCLUSTER BY ambito, tema AS\nWITH cls0 AS (\n SELECT DISTINCT campana,\n UPPER(REGEXP_REPLACE(NORMALIZE(campana, NFD), r'\\p{Mn}', '')) AS n\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta`\n WHERE campana IS NOT NULL\n),\ncls AS (\n SELECT campana,\n REGEXP_CONTAINS(n, r'NEUMATIC|PIRELLI|CEAT|MICHELIN|BRIDGESTONE|GOODYEAR|CONTINENTAL|HANKOOK|DUNLOP|FIRESTONE|KUMHO|NEXEN|4X3|RUEDA') AS t_neum,\n REGEXP_CONTAINS(n, r'BATERIA') AS t_bat,\n REGEXP_CONTAINS(n, r'AIRE ACOND|CLIMATIZ|A/A') AS t_aa,\n REGEXP_CONTAINS(n, r'REVISION|ESENCIA|MANTENIMIENTO|PUESTA A PUNTO|ITV') AS t_rev,\n REGEXP_CONTAINS(n, r'ACEITE|LUBRIC') AS t_ace,\n REGEXP_CONTAINS(n, r'ESCOBILLA|LIMPIAPARABRISA') AS t_esc,\n REGEXP_CONTAINS(n, r'FRENO|PASTILLA') AS t_fre,\n REGEXP_CONTAINS(n, r'LUNA|PARABRISAS|CARGLASS|CRISTAL') AS t_lun,\n REGEXP_CONTAINS(n, r'BOMBILLA|FARO|ILUMINAC') AS t_ilu,\n REGEXP_CONTAINS(n, r'(^|[^A-Z])MT([^A-Z]|$)|MOTORTOWN|MOTOR TOWN') AS es_mt,\n REGEXP_CONTAINS(n, r'REMINDER|RECORDATORIO|ULTIMA HORA|ULTIMAS HORAS') AS es_reminder\n FROM cls0\n),\nclassified AS (\n SELECT campana,\n ARRAY_CONCAT(\n IF(t_neum, ['NEUMATICOS'], CAST([] AS ARRAY)),\n IF(t_bat, ['BATERIAS'], CAST([] AS ARRAY)),\n IF(t_rev, ['REVISION','LUBRICA','FILTROS'], CAST([] AS ARRAY)),\n IF(t_ace AND NOT t_rev, ['LUBRICA'], CAST([] AS ARRAY)),\n IF(t_esc, ['ESCOBILLAS'], CAST([] AS ARRAY)),\n IF(t_fre, ['FRICCION'], CAST([] AS ARRAY)),\n IF(t_lun, ['LUNAS','SERV LUNAS'], CAST([] AS ARRAY)),\n IF(t_ilu, ['ILUMINACIO'], CAST([] AS ARRAY))\n ) AS cats,\n t_aa AS aa,\n IF(es_mt, 'MotorTown', 'Aurgi') AS ambito,\n es_reminder,\n TRIM(CONCAT(\n IF(t_neum,'neumatico ',''), IF(t_bat,'bateria ',''), IF(t_aa,'aire ',''),\n IF(t_rev,'revision ',''), IF(t_ace AND NOT t_rev,'aceite ',''),\n IF(t_esc,'escobilla ',''), IF(t_fre,'freno ',''), IF(t_lun,'luna ',''),\n IF(t_ilu,'iluminacion ',''))) AS tema_raw\n FROM cls\n),\nclassified2 AS (\n SELECT campana, cats, aa, ambito, es_reminder,\n (ARRAY_LENGTH(cats) > 0 OR aa) AS has_product,\n IF(tema_raw = '', 'generico', tema_raw) AS tema\n FROM classified\n),\nrecip AS (\n SELECT f.campana, f.persona_id,\n MAX(CAST(f.abrio AS INT64)) = 1 AS abrio,\n MAX(f.ultima_fecha) AS fe\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta` f\n WHERE f.ultima_fecha IS NOT NULL AND f.ultima_fecha >= '2025-03-01'\n AND f.persona_id != -4712881641624161506 -- excluir generico TPV \"No identificado\"\n GROUP BY f.campana, f.persona_id\n),\nr AS (\n SELECT recip.campana, recip.persona_id, recip.abrio, recip.fe,\n c.cats, c.aa\n FROM recip JOIN classified2 c USING (campana)\n),\ntx AS (\n SELECT r.campana, r.persona_id, r.abrio, r.fe,\n t.fecha, t.importe,\n (t.categoria IN UNNEST(r.cats) OR (r.aa AND t.grupo IN ('SERVICO AA','PROMO AA'))) AS is_rel\n FROM r JOIN `autingo-159109.clientes_intel.fact_transaccion` t\n ON t.persona_id = r.persona_id\n AND t.fecha BETWEEN DATE_SUB(r.fe, INTERVAL 30 DAY) AND DATE_ADD(r.fe, INTERVAL 30 DAY)\n AND t.fecha >= '2025-01-01'\n),\nper AS (\n SELECT campana, persona_id, ANY_VALUE(abrio) AS abrio, ANY_VALUE(fe) AS fe,\n MAX(IF(fecha > fe AND fecha <= DATE_ADD(fe, INTERVAL 30 DAY), 1, 0)) AS post_any,\n MAX(IF(fecha > fe AND fecha <= DATE_ADD(fe, INTERVAL 30 DAY) AND is_rel, 1, 0)) AS post_rel,\n MAX(IF(fecha <= fe AND fecha > DATE_SUB(fe, INTERVAL 30 DAY), 1, 0)) AS pre_any,\n MAX(IF(fecha <= fe AND fecha > DATE_SUB(fe, INTERVAL 30 DAY) AND is_rel, 1, 0)) AS pre_rel,\n MIN(IF(fecha > fe AND fecha <= DATE_ADD(fe, INTERVAL 30 DAY), fecha, NULL)) AS first_day\n FROM tx GROUP BY campana, persona_id\n),\nrev AS (\n SELECT tx.campana, tx.persona_id, ANY_VALUE(per.abrio) AS abrio,\n SUM(IF(tx.fecha = per.first_day, tx.importe, 0)) AS ing_1a_factura,\n SUM(IF(tx.fecha = per.first_day AND tx.is_rel, tx.importe, 0)) AS imp_prod_1a,\n SUM(IF(tx.fecha > per.first_day AND tx.fecha <= DATE_ADD(per.fe, INTERVAL 30 DAY), tx.importe, 0)) AS ing_siguientes\n FROM tx JOIN per USING (campana, persona_id)\n WHERE per.first_day IS NOT NULL\n GROUP BY tx.campana, tx.persona_id\n),\ndenom AS (\n SELECT campana, COUNT(*) AS impactados, COUNTIF(abrio) AS abridores,\n COUNTIF(NOT abrio) AS noab, MAX(fe) AS envio\n FROM recip GROUP BY campana\n),\nstats AS (\n SELECT campana,\n COUNTIF(abrio AND post_any = 1) AS compradores,\n COUNTIF(abrio AND post_rel = 1) AS abr_post_rel,\n COUNTIF(abrio AND pre_rel = 1) AS abr_pre_rel,\n COUNTIF(NOT abrio AND post_rel = 1) AS noabr_post_rel,\n COUNTIF(NOT abrio AND pre_rel = 1) AS noabr_pre_rel,\n COUNTIF(abrio AND pre_any = 1) AS abr_pre_any,\n COUNTIF(NOT abrio AND post_any = 1) AS noabr_post_any,\n COUNTIF(NOT abrio AND pre_any = 1) AS noabr_pre_any\n FROM per GROUP BY campana\n),\nrevagg AS (\n SELECT campana,\n ROUND(SUM(IF(abrio, ing_1a_factura, 0)), 2) AS ing_1a_factura,\n ROUND(SUM(IF(abrio, imp_prod_1a, 0)), 2) AS imp_prod_1a,\n ROUND(SUM(IF(abrio, ing_siguientes, 0)), 2) AS ing_siguientes\n FROM rev GROUP BY campana\n)\nSELECT\n d.campana, c.ambito, c.tema, c.es_reminder, c.has_product, d.envio,\n d.impactados, d.abridores, s.compradores,\n ROUND(SAFE_DIVIDE(d.abridores, d.impactados), 4) AS tasa_apertura,\n ROUND(SAFE_DIVIDE(s.compradores, d.abridores), 4) AS tasa_conversion,\n -- DiD (puntos porcentuales)\n ROUND(((SAFE_DIVIDE(s.compradores, d.abridores) - SAFE_DIVIDE(s.abr_pre_any, d.abridores))\n - (SAFE_DIVIDE(s.noabr_post_any, d.noab) - SAFE_DIVIDE(s.noabr_pre_any, d.noab))) * 100, 3) AS did_compra_pp,\n IF(c.has_product,\n ROUND(((SAFE_DIVIDE(s.abr_post_rel, d.abridores) - SAFE_DIVIDE(s.abr_pre_rel, d.abridores))\n - (SAFE_DIVIDE(s.noabr_post_rel, d.noab) - SAFE_DIVIDE(s.noabr_pre_rel, d.noab))) * 100, 3),\n NULL) AS did_producto_pp,\n -- compradores incrementales (usa DiD producto si hay, si no DiD compra)\n ROUND(IF(c.has_product,\n ((SAFE_DIVIDE(s.abr_post_rel, d.abridores) - SAFE_DIVIDE(s.abr_pre_rel, d.abridores))\n - (SAFE_DIVIDE(s.noabr_post_rel, d.noab) - SAFE_DIVIDE(s.noabr_pre_rel, d.noab))),\n ((SAFE_DIVIDE(s.compradores, d.abridores) - SAFE_DIVIDE(s.abr_pre_any, d.abridores))\n - (SAFE_DIVIDE(s.noabr_post_any, d.noab) - SAFE_DIVIDE(s.noabr_pre_any, d.noab)))\n ) * d.abridores, 1) AS compradores_increm,\n ra.ing_1a_factura,\n ra.imp_prod_1a AS importe_solo_producto,\n ra.ing_siguientes,\n ROUND(SAFE_DIVIDE(ra.imp_prod_1a, ra.ing_1a_factura), 4) AS pct_producto,\n ROUND(1 - SAFE_DIVIDE(ra.imp_prod_1a, ra.ing_1a_factura), 4) AS pct_upselling,\n ROUND(SAFE_DIVIDE(ra.ing_siguientes, ra.ing_1a_factura + ra.ing_siguientes), 4) AS pct_siguientes\nFROM denom d\nJOIN classified2 c USING (campana)\nJOIN stats s USING (campana)\nLEFT JOIN revagg ra USING (campana)\nWHERE d.impactados >= 1000;\n", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 16:21:06.258000+00:00", + "refs": [ + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion" + ] + }, + "rpt_campana_usuario": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.rpt_campana_usuario` CLUSTER BY persona_id AS\n WITH \n resp AS (\n SELECT persona_id, canal, campana, ultima_fecha AS fecha_envio,\n abrio, clico, n_enviados, n_entregados, n_rebotados, n_baja\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta` WHERE ultima_fecha IS NOT NULL\n ),\n lines AS (\n SELECT r.*, t.fecha AS tx_fecha, t.importe,\n (t.fecha > r.fecha_envio AND t.fecha <= DATE_ADD(r.fecha_envio, INTERVAL 30 DAY)) AS in_win\n FROM resp r\n LEFT JOIN `autingo-159109.clientes_intel.fact_transaccion` t ON t.persona_id = r.persona_id AND t.fecha >= '2025-01-01'\n ),\n ranked AS (\n SELECT *,\n MIN(IF(in_win, tx_fecha, NULL)) OVER (PARTITION BY canal, campana, persona_id) AS first_buy\n FROM lines\n ),\n per AS (\n SELECT canal, campana, persona_id,\n ANY_VALUE(abrio) AS abrio, ANY_VALUE(clico) AS clico,\n ANY_VALUE(n_enviados) AS n_enviados, ANY_VALUE(n_entregados) AS n_entregados,\n ANY_VALUE(n_rebotados) AS n_rebotados, ANY_VALUE(n_baja) AS n_baja,\n ANY_VALUE(fecha_envio) AS fecha_envio,\n MAX(IF(in_win, 1, 0)) AS compro,\n COUNTIF(in_win) AS compras,\n SUM(IF(in_win, importe, 0)) AS eur,\n SUM(IF(in_win AND tx_fecha = first_buy, importe, 0)) AS eur_primera,\n SUM(IF(in_win AND tx_fecha > first_buy, importe, 0)) AS eur_siguientes\n FROM ranked\n GROUP BY canal, campana, persona_id\n ),\n agg AS (\n SELECT persona_id,\n COUNT(*) AS n_campanas_impactadas,\n COUNTIF(compro = 1) AS n_conversiones,\n ROUND(SAFE_DIVIDE(COUNTIF(compro = 1), COUNT(*)), 4) AS tasa_conversion,\n SUM(n_enviados) AS n_enviados,\n LOGICAL_OR(abrio) AS abrio_alguna,\n LOGICAL_OR(clico) AS clico_alguna,\n SUM(n_baja) AS n_bajas,\n ROUND(SUM(eur), 2) AS importe_total,\n ROUND(SUM(eur_primera), 2) AS importe_primera,\n ROUND(SUM(eur_siguientes), 2) AS importe_siguientes\n FROM per GROUP BY persona_id\n )\n SELECT\n a.*, p.full_name AS nombre, p.salesforce_customer_id AS salesforce_id,\n p.es_empresa, p.email IS NOT NULL AS tiene_email\n FROM agg a JOIN `autingo-159109.clientes_intel.dim_persona` p USING (persona_id)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 11:07:23.245000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion" + ] + }, + "rpt_impacto_persona": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.rpt_impacto_persona`\n CLUSTER BY compro AS\n -- Atribucion UNICA a nivel persona: cada compra se cuenta UNA vez aunque caiga\n -- en la ventana de varias campanas (evita el inflado del SUM por campana).\n WITH resp AS (\n SELECT persona_id, ultima_fecha AS fecha_envio\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta`\n WHERE ultima_fecha IS NOT NULL\n ),\n tx AS (\n SELECT DISTINCT r.persona_id, t.transaccion_id, t.fecha AS tx_fecha, t.importe\n FROM resp r\n JOIN `autingo-159109.clientes_intel.fact_transaccion` t\n ON t.persona_id = r.persona_id\n AND t.fecha > r.fecha_envio\n AND t.fecha <= DATE_ADD(r.fecha_envio, INTERVAL 30 DAY)\n AND t.fecha >= '2025-01-01'\n ),\n firstbuy AS (\n SELECT persona_id, MIN(tx_fecha) AS first_buy FROM tx GROUP BY persona_id\n ),\n imp AS (\n SELECT x.persona_id,\n SUM(x.importe) AS eur_unico,\n SUM(IF(x.tx_fecha = f.first_buy, x.importe, 0)) AS eur_primera,\n SUM(IF(x.tx_fecha > f.first_buy, x.importe, 0)) AS eur_siguientes\n FROM tx x JOIN firstbuy f USING (persona_id)\n GROUP BY x.persona_id\n )\n SELECT\n r.persona_id,\n imp.persona_id IS NOT NULL AS compro,\n COALESCE(imp.eur_unico, 0) AS eur_unico,\n COALESCE(imp.eur_primera, 0) AS eur_primera,\n COALESCE(imp.eur_siguientes, 0) AS eur_siguientes\n FROM (SELECT DISTINCT persona_id FROM resp) r\n LEFT JOIN imp USING (persona_id)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 11:07:41.285000+00:00", + "refs": [ + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion" + ] + }, + "seg_audiencia": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.seg_audiencia`\n CLUSTER BY accion, persona_id AS\n WITH base AS (\n SELECT\n r.persona_id, r.vehiculo_id, r.license_plate,\n r.accion, r.motivo, r.motivo_subcat, r.prioridad_score, r.peso_accion, r.clv_norm,\n r.canal_entrada,\n r.contactable_email, r.contactable_telefono,\n r.email_optout, r.sms_optout, r.do_not_call,\n p.salesforce_customer_id AS salesforce_contact_id,\n p.full_name, p.email, p.phone,\n -- canal recomendado respetando consentimiento\n CASE\n WHEN r.contactable_email THEN 'EMAIL'\n WHEN r.contactable_telefono AND NOT r.do_not_call THEN 'SMS'\n ELSE NULL\n END AS canal_recomendado,\n ROW_NUMBER() OVER (\n PARTITION BY r.persona_id ORDER BY r.prioridad_score DESC, r.peso_accion DESC\n ) AS rk_persona\n FROM `autingo-159109.clientes_intel.reco_acciones` r\n JOIN `autingo-159109.clientes_intel.dim_persona` p USING(persona_id)\n WHERE r.contactable_email OR r.contactable_telefono\n )\n SELECT\n * EXCEPT(rk_persona),\n rk_persona = 1 AS top_persona,\n TRUE AS envio_habilitado,\n CAST(NULL AS STRING) AS bloqueo_motivo\n FROM base\n WHERE canal_recomendado IS NOT NULL;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 15:54:04.407000+00:00", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.reco_acciones" + ] + }, + "seg_cliente_360": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.seg_cliente_360`\n CLUSTER BY rfm_segment, cluster AS\n WITH veh AS (\n SELECT persona_actual AS persona_id,\n COUNT(*) AS n_vehiculos_propios,\n LOGICAL_OR(itv_due_este_anio) AS itv_due_este_anio,\n ROUND(AVG(antiguedad_anios), 1) AS antiguedad_media\n FROM `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n WHERE persona_actual IS NOT NULL GROUP BY persona_actual\n ),\n reco AS (\n SELECT persona_id,\n COUNT(*) AS n_acciones_reco,\n ARRAY_AGG(accion ORDER BY prioridad_score DESC LIMIT 1)[OFFSET(0)] AS top_accion,\n ARRAY_AGG(motivo ORDER BY prioridad_score DESC LIMIT 1)[OFFSET(0)] AS top_motivo,\n ARRAY_AGG(motivo_subcat ORDER BY prioridad_score DESC LIMIT 1)[OFFSET(0)] AS top_submotivo\n FROM `autingo-159109.clientes_intel.reco_acciones` GROUP BY persona_id\n ),\n mut AS (\n SELECT persona_id, MIN(flag) AS flag FROM (\n SELECT DISTINCT agregador_persona_id AS persona_id, 'Cuenta agregadora' AS flag\n FROM `autingo-159109.clientes_intel.map_mutualista_particular`\n UNION ALL\n SELECT particular_persona_id, 'Mutualista'\n FROM `autingo-159109.clientes_intel.map_mutualista_particular` WHERE es_mutualista\n ) GROUP BY persona_id\n ),\n geo AS (\n SELECT d.persona_id, pr.provincia, pr.ccaa\n FROM `autingo-159109.clientes_intel.dim_persona` d\n LEFT JOIN `autingo-159109.clientes_intel.dim_cp_provincia` pr\n ON pr.cp2 = SUBSTR(REGEXP_REPLACE(COALESCE(d.postal_code, ''), r'[^0-9]', ''), 1, 2)\n ),\n cit AS (\n SELECT m.persona_id, COUNT(*) AS n_citas, MAX(a.fec_fecha_cita) AS ultima_cita\n FROM `autingo-159109.ontologia.aurgiCitas_mat` a\n JOIN `autingo-159109.clientes_intel.map_persona_fuente` m\n ON m.fuente = 'tpv' AND m.source_id = a.id_cliente\n WHERE a.id_cliente IS NOT NULL AND a.id_cliente != ''\n AND NOT COALESCE(a.flag_fivetran_deleted, FALSE)\n AND a.fec_fecha_cita <= (SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`)\n GROUP BY m.persona_id\n ),\n promo_mut AS (\n SELECT DISTINCT persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n AND producto_nav_id IN ('190471','190472','190473','190474','190475','190476','190680','190988')\n )\n SELECT\n p.persona_id, p.full_name, p.es_empresa,\n NOT p.es_excluido AS es_b2c,\n -- ejes de segmentacion\n p.rfm_segment,\n CAST(c.cluster AS STRING) AS cluster,\n t.tipo_cliente, t.clv_nivel,\n p.r_score, p.f_score, p.m_score, p.rfm_score,\n clv.clv_score, clv.clv_decil, clv.clv_norm,\n -- valor / comportamiento\n p.recency_days, p.frequency, p.monetary_total, p.valor_ponderado, p.ticket_medio,\n p.mean_inter_visit_days,\n p.margen_total, p.margen_pct, clv.clv_12m_margen, clv.clv_12m_ingreso, clv.prob_activo,\n p.n_categorias, p.categoria_top, p.n_centros, p.centro_principal,\n p.empresa_principal, COALESCE(dp.canal_entrada, p.canal_preferido) AS canal_entrada,\n -- vehiculo / ITV\n COALESCE(v.n_vehiculos_propios, 0) AS n_vehiculos,\n COALESCE(v.itv_due_este_anio, FALSE) AS itv_due_este_anio,\n v.antiguedad_media,\n -- contactabilidad / RGPD\n dp.score_confianza_norm, dp.data_points,\n COALESCE(dp.contactable_email, FALSE) AS contactable_email,\n COALESCE(dp.contactable_telefono, FALSE) AS contactable_telefono,\n p.email_optout, p.sms_optout, p.do_not_call,\n -- contacto directo (para exportar audiencia)\n p.email, p.phone,\n -- enlace a activacion\n COALESCE(r.n_acciones_reco, 0) AS n_acciones_reco,\n r.top_accion AS top_accion_reco,\n r.top_motivo, r.top_submotivo,\n -- mutualistas: conductor real recuperado tras cuenta agregadora (mutua/flota)\n CASE\n WHEN COALESCE(mut.flag, 'No') = 'Cuenta agregadora' THEN 'Cuenta agregadora'\n WHEN mut.flag = 'Mutualista' OR pm.persona_id IS NOT NULL THEN 'Mutualista'\n ELSE 'No'\n END AS es_mutualista,\n -- geografia del cliente (por CP) y actividad en citas / inactividad\n g.provincia, g.ccaa,\n COALESCE(ci.n_citas, 0) AS n_citas,\n DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), ci.ultima_cita, DAY) AS dias_sin_cita,\n LEAST(p.recency_days, COALESCE(DATE_DIFF((SELECT MAX(fecha) FROM `autingo-159109.clientes_intel.fact_transaccion`), ci.ultima_cita, DAY), p.recency_days)) AS dias_inactivo\n FROM `autingo-159109.clientes_intel.feat_cliente_persona` p\n LEFT JOIN `autingo-159109.clientes_intel.score_clv` clv USING (persona_id)\n LEFT JOIN `autingo-159109.clientes_intel.seg_cluster_persona` c USING (persona_id)\n LEFT JOIN `autingo-159109.clientes_intel.tipologia_cliente` t USING (persona_id)\n LEFT JOIN `autingo-159109.clientes_intel.data_points_contacto` dp USING (persona_id)\n LEFT JOIN veh v USING (persona_id)\n LEFT JOIN reco r USING (persona_id)\n LEFT JOIN mut USING (persona_id)\n LEFT JOIN promo_mut pm USING (persona_id)\n LEFT JOIN geo g USING (persona_id)\n LEFT JOIN cit ci USING (persona_id)\n WHERE NOT p.es_excluido;", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 11:34:06.221000+00:00", + "refs": [ + "clientes_intel.data_points_contacto", + "clientes_intel.dim_cp_provincia", + "clientes_intel.dim_persona", + "clientes_intel.fact_transaccion", + "clientes_intel.feat_cliente_persona", + "clientes_intel.feat_cliente_vehiculo", + "clientes_intel.map_persona_fuente", + "clientes_intel.map_persona_vehiculo", + "clientes_intel.reco_acciones", + "clientes_intel.score_clv", + "clientes_intel.seg_cluster_persona", + "clientes_intel.tipologia_cliente", + "ontologia.aurgiCitas_mat" + ] + }, + "seg_cluster_persona": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.seg_cluster_persona` AS\nWITH pv AS (\n SELECT m.persona_id, s.cluster, v.importe_total,\n ROW_NUMBER() OVER (PARTITION BY m.persona_id ORDER BY v.importe_total DESC NULLS LAST) AS rn\n FROM `autingo-159109.clientes_intel.map_persona_vehiculo` m\n JOIN `autingo-159109.clientes_intel.seg_cluster_vehiculo` s ON s.plate_norm = m.plate_norm\n LEFT JOIN `autingo-159109.clientes_intel.feat_cliente_vehiculo` v ON v.vehiculo_id = m.plate_norm\n)\nSELECT persona_id, cluster FROM pv WHERE rn = 1", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-09 07:41:53.575000+00:00", + "refs": [ + "clientes_intel.feat_cliente_vehiculo", + "clientes_intel.map_persona_vehiculo", + "clientes_intel.seg_cluster_vehiculo" + ] + }, + "seg_vega_persona": { + "query": "-- Tabla companera del segmentador (dash 1067 / tab 450). Una fila por persona (B2C+B2B\n-- igual que seg_cliente_360) con las variables estilo \"segmentador Vega\" pedidas, todas\n-- alimentadas desde NUESTRO pipeline clientes_intel + maestro crudo aurgiClientes (por DNI).\n-- Se une con LEFT JOIN v USING(persona_id) en las cards 11196-11202.\n-- Refresco: scheduled query cada 6h.\nCREATE OR REPLACE TABLE `autingo-159109.clientes_intel.seg_vega_persona`\nCLUSTER BY persona_id AS\nWITH base AS (\n SELECT persona_id, tenure_days, ultima_compra\n FROM `autingo-159109.clientes_intel.feat_cliente_persona`\n WHERE NOT es_excluido\n),\ndp AS (\n SELECT persona_id, UPPER(TRIM(document_number)) AS dni, postal_code,\n COALESCE(whatsapp_optout, FALSE) AS whatsapp_optout\n FROM `autingo-159109.clientes_intel.dim_persona`\n),\n-- maestro crudo de clientes, por DNI (pk_dni es unico): edad, poblacion, tipo, franquiciado,\n-- flags de canal cliente y fechas de aceptacion RGPD.\nac AS (\n SELECT UPPER(TRIM(pk_dni)) AS dni,\n ANY_VALUE(num_edad) AS edad,\n ANY_VALUE(desc_poblacion) AS poblacion,\n ANY_VALUE(desc_tipo_cliente) AS tipo_cliente_real,\n LOGICAL_OR(COALESCE(flag_cliente_franquisiado_navision, FALSE)) AS franquiciado_navision,\n LOGICAL_OR(COALESCE(flag_aurgi, FALSE)) AS flag_aurgi,\n LOGICAL_OR(COALESCE(flag_autingo, FALSE)) AS flag_autingo,\n LOGICAL_OR(COALESCE(flag_motortown, FALSE)) AS flag_motortown,\n LOGICAL_OR(COALESCE(flag_cliente_online, FALSE)) AS flag_web,\n DATE(MAX(ts_fecha_aceptacion_privacidad)) AS fecha_acept_privacidad,\n DATE(MAX(ts_fecha_aceptacion_tyc)) AS fecha_acept_tyc\n FROM `autingo-159109.ontologia.aurgiClientes`\n WHERE pk_dni IS NOT NULL AND pk_dni != ''\n GROUP BY dni\n),\nveh AS (\n SELECT persona_actual AS persona_id,\n LOGICAL_OR(UPPER(make) IN (\n 'AUDI','BMW','MERCEDES','MERCEDES-BENZ','VOLVO','LEXUS','JAGUAR','LAND ROVER','PORSCHE',\n 'MASERATI','BENTLEY','FERRARI','LAMBORGHINI','ROLLS-ROYCE','ASTON MARTIN','TESLA','MINI',\n 'ALFA ROMEO','DS','INFINITI','CADILLAC','LINCOLN','JEEP')) AS tiene_vehiculo_lujo\n FROM `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n WHERE persona_actual IS NOT NULL\n GROUP BY persona_actual\n),\nlim AS (SELECT DATE_SUB(MAX(fecha), INTERVAL 365 DAY) AS d FROM `autingo-159109.clientes_intel.fact_transaccion`),\ntx AS (\n SELECT t.persona_id,\n COUNT(DISTINCT NULLIF(t.otr_nav_id, '')) AS n_documentos,\n COUNT(DISTINCT NULLIF(t.id_factura_nav, '')) AS n_facturas,\n ARRAY_AGG(DISTINCT t.categoria IGNORE NULLS) AS cat_comprado,\n ARRAY_AGG(DISTINCT t.grupo IGNORE NULLS) AS grupo_comprado,\n ARRAY_AGG(DISTINCT t.canal IGNORE NULLS) AS canal_comprado,\n ARRAY_AGG(DISTINCT IF(t.fecha >= lim.d, t.categoria, NULL) IGNORE NULLS) AS cat_ultimo_ano,\n ARRAY_AGG(DISTINCT IF(t.fecha >= lim.d, t.grupo, NULL) IGNORE NULLS) AS grupo_ultimo_ano,\n ARRAY_AGG(DISTINCT IF(t.fecha >= lim.d, t.canal, NULL) IGNORE NULLS) AS canal_ultimo_ano\n FROM `autingo-159109.clientes_intel.fact_transaccion` t CROSS JOIN lim\n WHERE t.persona_id IS NOT NULL\n GROUP BY t.persona_id\n)\nSELECT\n b.persona_id,\n ac.edad,\n ac.poblacion,\n ac.tipo_cliente_real,\n COALESCE(ac.franquiciado_navision, FALSE) AS franquiciado_navision,\n ac.fecha_acept_privacidad,\n ac.fecha_acept_tyc,\n dp.postal_code AS codigo_postal,\n dp.whatsapp_optout,\n b.tenure_days AS antiguedad_cliente_dias,\n b.ultima_compra AS fecha_ultimo_mov,\n COALESCE(v.tiene_vehiculo_lujo, FALSE) AS tiene_vehiculo_lujo,\n COALESCE(tx.n_documentos, 0) AS n_documentos,\n COALESCE(tx.n_facturas, 0) AS n_facturas,\n IFNULL(tx.cat_comprado, []) AS cat_comprado,\n IFNULL(tx.grupo_comprado, []) AS grupo_comprado,\n IFNULL(tx.canal_comprado, []) AS canal_comprado,\n IFNULL(tx.cat_ultimo_ano, []) AS cat_ultimo_ano,\n IFNULL(tx.grupo_ultimo_ano, []) AS grupo_ultimo_ano,\n IFNULL(tx.canal_ultimo_ano, []) AS canal_ultimo_ano,\n ARRAY(SELECT x FROM UNNEST([\n IF(COALESCE(ac.flag_aurgi, FALSE), 'Aurgi', NULL),\n IF(COALESCE(ac.flag_web, FALSE), 'Web', NULL),\n IF(COALESCE(ac.flag_autingo, FALSE), 'Autingo', NULL),\n IF(COALESCE(ac.flag_motortown, FALSE), 'MotorTown', NULL)\n ]) x WHERE x IS NOT NULL) AS canales_cliente\nFROM base b\nLEFT JOIN dp USING (persona_id)\nLEFT JOIN ac ON ac.dni = dp.dni\nLEFT JOIN veh v ON v.persona_id = b.persona_id\nLEFT JOIN tx ON tx.persona_id = b.persona_id;\n", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:39:10.071000+00:00", + "refs": [ + "citaprevia_aurphcp.clientes", + "clientes_intel.dim_persona", + "clientes_intel.fact_transaccion", + "clientes_intel.feat_cliente_persona", + "clientes_intel.feat_cliente_vehiculo", + "mssql2022_dbo.anjana_customer", + "postal_codes.postal_codes", + "psql_dcpublic.addresses", + "psql_dcpublic.channels", + "psql_dcpublic.companies", + "psql_dcpublic.customers", + "psql_dcpublic.otr_customers", + "psql_dcpublic.tpv_customers", + "psql_dcpublic.tpv_customers_companies", + "psql_dcpublic.tpv_persons" + ] + }, + "tipologia_cliente": { + "query": "\n CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.tipologia_cliente` AS\n SELECT\n f.persona_id,\n f.rfm_segment,\n COALESCE(CONCAT('C', CAST(s.cluster AS STRING)), 'sin cluster') AS cluster,\n c.clv_norm,\n CASE\n WHEN c.clv_norm IS NULL THEN 'n/d'\n WHEN c.clv_norm >= 0.66 THEN 'Alto'\n WHEN c.clv_norm >= 0.33 THEN 'Medio'\n ELSE 'Bajo'\n END AS clv_nivel,\n f.monetary_total,\n -- tipo de cliente = RFM segment x Cluster (el CLV se reporta como atributo)\n CONCAT(\n f.rfm_segment, ' | ',\n COALESCE(CONCAT('C', CAST(s.cluster AS STRING)), 'sin cluster')\n ) AS tipo_cliente\n FROM `autingo-159109.clientes_intel.feat_cliente_persona` f\n LEFT JOIN `autingo-159109.clientes_intel.seg_cluster_persona` s USING (persona_id)\n LEFT JOIN `autingo-159109.clientes_intel.score_clv` c USING (persona_id)\n WHERE NOT f.es_excluido AND f.rfm_segment IS NOT NULL\n ", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-09 11:11:43.092000+00:00", + "refs": [ + "clientes_intel.feat_cliente_persona", + "clientes_intel.score_clv", + "clientes_intel.seg_cluster_persona" + ] + }, + "tipologia_cliente_resumen": { + "query": "\n CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.tipologia_cliente_resumen` AS\n SELECT\n tipo_cliente, rfm_segment, cluster,\n COUNT(*) AS n_personas,\n ROUND(COUNT(*) / SUM(COUNT(*)) OVER (), 4) AS pct,\n ROUND(AVG(monetary_total), 0) AS gasto_medio,\n ROUND(SUM(monetary_total), 0) AS gasto_total,\n ROUND(AVG(clv_norm), 3) AS clv_norm_medio,\n COUNTIF(clv_nivel = 'Alto') AS n_clv_alto,\n COUNTIF(clv_nivel = 'Medio') AS n_clv_medio,\n COUNTIF(clv_nivel = 'Bajo') AS n_clv_bajo\n FROM `autingo-159109.clientes_intel.tipologia_cliente`\n GROUP BY tipo_cliente, rfm_segment, cluster\n ORDER BY n_personas DESC\n ", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-06-09 11:11:48.450000+00:00", + "refs": [ + "clientes_intel.tipologia_cliente" + ] + }, + "veh_cluster": { + "query": "CREATE OR REPLACE TABLE `autingo-159109.clientes_intel.veh_cluster` AS\nSELECT\n m.vehiculo_id,\n m.CENTROID_ID AS cluster,\n CASE m.CENTROID_ID\n WHEN 1 THEN 'Servicio puntual gasto bajo'\n WHEN 2 THEN 'Flota / alto valor fiel'\n WHEN 3 THEN 'Coche viejo dormido'\n WHEN 4 THEN 'Coche nuevo activo'\n WHEN 5 THEN 'Coche nuevo inactivo'\n WHEN 6 THEN 'Recurrentes valor medio'\n WHEN 7 THEN 'Coche viejo activo'\n END AS cluster_nombre,\n t.cluster_tecnico,\n t.cluster_tecnico_nombre\nFROM ML.PREDICT(MODEL `autingo-159109.clientes_intel._veh_km_k7`,\n (SELECT * FROM `autingo-159109.clientes_intel._veh_cluster_feat`)) m\nLEFT JOIN (\n SELECT\n vehiculo_id,\n CENTROID_ID AS cluster_tecnico,\n CASE CENTROID_ID\n WHEN 1 THEN 'Monovolumen diésel 7 plazas'\n WHEN 2 THEN 'Utilitario moderno pequeño'\n WHEN 3 THEN 'Compacto familiar diésel antiguo'\n WHEN 4 THEN 'SUV 4x4 potente'\n END AS cluster_tecnico_nombre\n FROM ML.PREDICT(MODEL `autingo-159109.clientes_intel._veh_km_tec_k4`,\n (SELECT * FROM `autingo-159109.clientes_intel._veh_tec_feat`))\n) t USING (vehiculo_id)", + "stmt": "CREATE_TABLE_AS_SELECT", + "last_run": "2026-07-01 14:46:10.517000+00:00", + "refs": [ + "clientes_intel._veh_cluster_feat", + "clientes_intel._veh_km_k7", + "clientes_intel._veh_km_tec_k4", + "clientes_intel._veh_tec_feat" + ] + } +} \ No newline at end of file diff --git a/scratchpad/intel_lineage.json b/scratchpad/intel_lineage.json new file mode 100644 index 00000000..a446baad --- /dev/null +++ b/scratchpad/intel_lineage.json @@ -0,0 +1,53 @@ +{ + "intel_involved": [ + "_margen_rate_producto", + "_persona_records", + "_plate_year_calib", + "data_points_contacto", + "dim_cp_provincia", + "dim_persona", + "dim_vehiculo", + "fact_campana_respuesta", + "fact_transaccion", + "fact_visita", + "feat_cliente_persona", + "feat_cliente_vehiculo", + "map_persona_canal8", + "map_persona_fuente", + "map_persona_vehiculo", + "reco_acciones", + "score_clv", + "seg_cliente_360", + "seg_cluster_persona", + "seg_cluster_vehiculo", + "tipologia_cliente" + ], + "external_sources": [ + "anjana_bi_amg.margenes_mat", + "citaprevia_aurphcp.clientes", + "citaprevia_aurphcp.clientes_matriculas", + "claude_bi.churn_scores_current", + "claude_bi.conversion_cqg_base_mat", + "claude_bi.todos_datos_lineas_mat", + "mssql2022_dbo.anjana_customer", + "ontologia.aurgiCitas_mat", + "psql_dcpublic.call_transactions", + "psql_dcpublic.car_makes", + "psql_dcpublic.car_model_families", + "psql_dcpublic.car_models", + "psql_dcpublic.car_versions", + "psql_dcpublic.customers", + "psql_dcpublic.otr_customers", + "psql_dcpublic.otr_vehicles", + "psql_dcpublic.tecrmi_license_plates", + "psql_dcpublic.tpv_customers", + "psql_dcpublic.tpv_vehicles_vehicle", + "psql_dcpublic.tpv_vehicles_vehicleowner", + "psql_dcpublic.users", + "salesforce_ew1.contacts_latest", + "salesforce_ew1.email_clicked", + "salesforce_ew1.email_opened", + "salesforce_ew1.email_sent", + "salesforce_ew1.sms" + ] +} \ No newline at end of file diff --git a/scratchpad/lineage_graph.json b/scratchpad/lineage_graph.json new file mode 100644 index 00000000..773b0b3b --- /dev/null +++ b/scratchpad/lineage_graph.json @@ -0,0 +1,332 @@ +{ + "project": "autingo-159109", + "seeds": [ + "customer_marts.customer_brand_affinity", + "customer_marts.customer_category_spend", + "customer_marts.customer_channel", + "customer_marts.customer_contactability", + "customer_marts.customer_monetary", + "customer_marts.customer_payment_method", + "customer_marts.customer_predictive", + "customer_marts.customer_product", + "customer_marts.customer_profile", + "customer_marts.customer_promo_tolerance", + "customer_marts.customer_promo_usage", + "customer_marts.customer_store_spend", + "customer_marts.customer_temporal", + "customer_marts.customer_vehicles" + ], + "graph": { + "customer_marts.customer_brand_affinity": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_brand_affinity`\nAS WITH lineas AS (\n SELECT\n f.persona_id,\n UPPER(TRIM(p.Producto___Nav__idAtributo3)) AS marca,\n f.importe,\n f.cantidad\n FROM `autingo-159109.clientes_intel.fact_transaccion` f\n JOIN `autingo-159109.anjana_bi_datamart.Objeto_productos` p\n ON f.producto_nav_id = p.nav_id\n WHERE f.persona_id IS NOT NULL\n AND p.Producto___Nav__idAtributo3 IS NOT NULL\n AND TRIM(p.Producto___Nav__idAtributo3) NOT IN ('', 'SIN', 'ENCARGO')\n),\nagg AS (\n SELECT\n persona_id,\n marca,\n SUM(importe) AS gasto,\n COUNT(*) AS n_lineas,\n SUM(cantidad) AS uds\n FROM lineas\n GROUP BY persona_id, marca\n)\nSELECT\n persona_id,\n marca,\n gasto,\n n_lineas,\n uds,\n SAFE_DIVIDE(gasto, SUM(gasto) OVER (PARTITION BY persona_id)) AS share_gasto_marca\nFROM agg;", + "refs": [ + "anjana_bi_datamart.Objeto_productos", + "clientes_intel.fact_transaccion" + ], + "depth": 0 + }, + "anjana_bi_datamart.Objeto_productos": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.anjana_bi_datamart.Objeto_productos`\nAS SELECT base.*, img.imagen_principal, img.imagen_secundaria\nFROM (\nSELECT * FROM (\nSELECT\n\n `source`.`id` AS `id`,\n\n `source`.`nav_id` AS `nav_id`,\n\n `source`.`description` AS `description`,\n\n `source`.`is_tecdoc` AS `is_tecdoc`,\n\n `source`.`Status_de_producto` AS `Status_de_producto`,\n\n `source`.`Is_Encargo` AS `Is_Encargo`,\n\n `source`.`is_service` AS `is_service`,\n\n `source`.`is_tpv` AS `is_tpv`,\n\n `source`.`Tipo___Navision` AS `Tipo___Navision`,\n\n `source`.`Indicadores_tag` AS `Indicadores_tag`,\n\n `source`.`Product_Groups__description` AS `Product_Groups__description`,\n\n `source`.`Product_Categories__description` AS `Product_Categories__description`,\n\n `source`.`Producto___Nav__idAtributo1` AS `Producto___Nav__idAtributo1`,\n\n `source`.`Producto___Nav__idAtributo2` AS `Producto___Nav__idAtributo2`,\n\n `source`.`Producto___Nav__idAtributo3` AS `Producto___Nav__idAtributo3`,\n\n `source`.`Producto___Nav__idAtributo5` AS `Producto___Nav__idAtributo5`,\n\n `source`.`Producto___Nav__idAtributo4` AS `Producto___Nav__idAtributo4`,\n\n `source`.`Producto___Nav__idAtributo6` AS `Producto___Nav__idAtributo6`,\n\n `source`.`Producto___Nav__idAtributo7` AS `Producto___Nav__idAtributo7`,\n\n `source`.`Producto___Nav__idAtributo8` AS `Producto___Nav__idAtributo8`,\n\n `source`.`Producto___Nav__idAtributo9` AS `Producto___Nav__idAtributo9`,\n\n `source`.`Producto___Nav__idAtributo10` AS `Producto___Nav__idAtributo10`,\n\n `source`.`Producto___Nav__idCategoria` AS `Producto___Nav__idCategoria`,\n\n `source`.`Producto___Nav__idGrupo` AS `Producto___Nav__idGrupo`,\n\n `source`.`Categoria_16_07_cgq___tipo_old__cat_cgq` AS `Categoria_16_07_cgq___tipo_old__cat_cgq`,\n\n `source`.`Categoria_16_07_cgq___tipo_old__tipo_cgq` AS `Categoria_16_07_cgq___tipo_old__tipo_cgq`,\n\n `source`.`Categoria_16_07_cgq___tipo_old__subcate_cgq` AS `Categoria_16_07_cgq___tipo_old__subcate_cgq`,\n\n `source`.`Tipologia_de_producto__tipo` AS `Tipologia_de_producto__tipo`\n\nFROM\n\n (\n\n SELECT\n\n `psql_dcpublic.products`.`id` AS `id`,\n\n `psql_dcpublic.products`.`nav_id` AS `nav_id`,\n\n `psql_dcpublic.products`.`description` AS `description`,\n\n `psql_dcpublic.products`.`is_tecdoc` AS `is_tecdoc`,\n\n CASE\n\n WHEN `psql_dcpublic.products`.`status` = 0 THEN 'Activo'\n\n WHEN `psql_dcpublic.products`.`status` = 1 THEN 'Activo para salida'\n\n WHEN `psql_dcpublic.products`.`status` = 2 THEN 'Activo para entrada'\n\n WHEN `psql_dcpublic.products`.`status` = 3 THEN 'Desactivado'\n\n END AS `Status_de_producto`,\n\n CASE\n\n WHEN `psql_dcpublic.products`.`is_encargo` IS NULL THEN FALSE\n\n END AS `Is_Encargo`,\n\n `psql_dcpublic.products`.`is_service` AS `is_service`,\n\n CASE\n\n WHEN `Producto___Nav`.`idProducto` IS NULL THEN 'Producto de Tpv'\n\n WHEN `Producto___Nav`.`idProducto` IS NOT NULL THEN 'Producto de Navision'\n\n END AS `is_tpv`,\n\n CASE\n\n WHEN `Producto___Nav`.`idTipo` = 0 THEN 'Tienda'\n\n WHEN `Producto___Nav`.`idTipo` = 1 THEN 'Taller'\n\n END AS `Tipo___Navision`,\n\n CASE\n\n WHEN (`Product_Groups`.`nav_id` = '4X4')\n\n \n\n OR (`Product_Groups`.`nav_id` = 'TRAN LIGER')\n\n OR (`Product_Groups`.`nav_id` = 'TURISMO') THEN 'neumaticos'\n\n WHEN `Product_Groups`.`nav_id` = 'BATERIAS' THEN 'baterias'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9101018')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101037')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101047')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101019')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101046')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101020')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101021')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9101022')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108047') THEN 'paralelos'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9103040')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103011')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103007')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103008')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103014')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103039')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103013')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103009')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103019')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103017')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103022')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103021') THEN 'frenado_mano_de_obra'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9103009')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103040') THEN 'discos_mano_de_obra'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9104019')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104021')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104003')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104004')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104022')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104020')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104001')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104015')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104002')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104016')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104014')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104017')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104018')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104026')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104024')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104006')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104028')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104027') THEN 'amortiguadores'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9301003')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301005')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301012')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301006')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301001')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301007')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301008')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301010')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301009')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9301011')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104010')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104025')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104011')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9104008') THEN 'fuelles'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9108053')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108054')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108052') THEN 'revision_liquidos'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9108065')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108064')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102029')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102041')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102040')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102028')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102001')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9102003') THEN 'revisiones'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9108044')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108048')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108040')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108059')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108060')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108058')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108042')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108062')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108063')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108038')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108061')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108070')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108071')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108056')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108057')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108073')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108055')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108068')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108043')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108050')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108049')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108039')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108041')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108037')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108053')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108054')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108052')\n\n OR (`psql_dcpublic.products`.`nav_id` = '64594')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108069')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108087')\n\n OR (`psql_dcpublic.products`.`nav_id` = '23071')\n\n OR (`psql_dcpublic.products`.`nav_id` = '64191')\n\n OR (`psql_dcpublic.products`.`nav_id` = '68547')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108046')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108051')\n\n OR (`psql_dcpublic.products`.`nav_id` = '54850') THEN 'revisiones_de_calidad'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9108053')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108054')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108052')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103004')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103003')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103037')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103038')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103035')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103036')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103029')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9103030')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108004')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108005') THEN 'liquidos'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9108032')\n\n OR (`psql_dcpublic.products`.`nav_id` = '65631')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108066')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9108067') THEN 'diagnosis'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9201026')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9201027')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9201028') THEN 'diagnosis_montajes_bateria'\n\n WHEN (`psql_dcpublic.products`.`nav_id` = '9201024')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9201025')\n\n OR (`psql_dcpublic.products`.`nav_id` = '9201003') THEN 'montajes_bateria'\n\n END AS `Indicadores_tag`,\n\n `Product_Groups`.`description` AS `Product_Groups__description`,\n\n `Product_Categories`.`description` AS `Product_Categories__description`,\n\n `Producto___Nav`.`idAtributo1` AS `Producto___Nav__idAtributo1`,\n\n `Producto___Nav`.`idAtributo2` AS `Producto___Nav__idAtributo2`,\n\n `Producto___Nav`.`idAtributo3` AS `Producto___Nav__idAtributo3`,\n\n `Producto___Nav`.`idAtributo5` AS `Producto___Nav__idAtributo5`,\n\n `Producto___Nav`.`idAtributo4` AS `Producto___Nav__idAtributo4`,\n\n `Producto___Nav`.`idAtributo6` AS `Producto___Nav__idAtributo6`,\n\n `Producto___Nav`.`idAtributo7` AS `Producto___Nav__idAtributo7`,\n\n `Producto___Nav`.`idAtributo8` AS `Producto___Nav__idAtributo8`,\n\n `Producto___Nav`.`idAtributo9` AS `Producto___Nav__idAtributo9`,\n\n `Producto___Nav`.`idAtributo10` AS `Producto___Nav__idAtributo10`,\n\n `Producto___Nav`.`idCategoria` AS `Producto___Nav__idCategoria`,\n\n `Producto___Nav`.`idGrupo` AS `Producto___Nav__idGrupo`,\n\n `Categoria_16_07_cgq___tipo_old`.`cat_cgq` AS `Categoria_16_07_cgq___tipo_old__cat_cgq`,\n\n `Categoria_16_07_cgq___tipo_old`.`tipo_cgq` AS `Categoria_16_07_cgq___tipo_old__tipo_cgq`,\n\n `Categoria_16_07_cgq___tipo_old`.`subcate_cgq` AS `Categoria_16_07_cgq___tipo_old__subcate_cgq`,\n\n `Tipologia_de_producto`.`tipo` AS `Tipologia_de_producto__tipo`\n\n FROM\n\n `psql_dcpublic.products`\n\n \n\nLEFT JOIN `psql_dcpublic.product_groups` AS `Product_Groups` ON `psql_dcpublic.products`.`product_group_id` = `Product_Groups`.`id`\n\n LEFT JOIN `psql_dcpublic.product_categories` AS `Product_Categories` ON `Product_Groups`.`product_category_id` = `Product_Categories`.`id`\n\n LEFT JOIN `stg_anjana_bi.producto` AS `Producto___Nav` ON `psql_dcpublic.products`.`nav_id` = `Producto___Nav`.`idProducto`\n\n LEFT JOIN (\n\n SELECT\n\n string_field_0 as tipo_old,\n\n string_field_1 as cat_old,\n\n string_field_2 as sub_cat_old,\n\n string_field_3 as union_str,\n\n string_field_4 as cat_cgq,\n\n string_field_5 as tipo_cgq,\n\n string_field_6 as subcate_cgq\n\n FROM\n\n `autingo-159109.anjana_bi_datamart.Cruce_16_07_cgq`\n\n ) AS `Categoria_16_07_cgq___tipo_old` ON (\n\n CASE\n\n WHEN `Producto___Nav`.`idTipo` = 0 THEN 'Tienda'\n\n WHEN `Producto___Nav`.`idTipo` = 1 THEN 'Taller'\n\n END = `Categoria_16_07_cgq___tipo_old`.`tipo_old`\n\n )\n\n \n\n AND (\n\n `Producto___Nav`.`idCategoria` = `Categoria_16_07_cgq___tipo_old`.`cat_old`\n\n )\n\n AND (\n\n `Producto___Nav`.`idGrupo` = `Categoria_16_07_cgq___tipo_old`.`sub_cat_old`\n\n )\n\n LEFT JOIN (\n\n SELECT\n\n `psql_dcpublic.products`.`id` AS `id`,\n\n -- `psql_dcpublic.products`.`product_group_id` AS `product_group_id`,\n\n -- `psql_dcpublic.products`.`nav_id` AS `nav_id`,\n\n -- `psql_dcpublic.products`.`normalized_code` AS `normalized_code`,\n\n -- `psql_dcpublic.products`.`old_aurgi_family` AS `old_aurgi_family`,\n\n -- `psql_dcpublic.products`.`description` AS `description`,\n\n -- `psql_dcpublic.products`.`web_description` AS `web_description`,\n\n -- `psql_dcpublic.products`.`search_description` AS `search_description`,\n\n -- `psql_dcpublic.products`.`status` AS `status`,\n\n -- `psql_dcpublic.products`.`is_blocked` AS `is_blocked`,\n\n -- `psql_dcpublic.products`.`created_at` AS `created_at`,\n\n -- `psql_dcpublic.products`.`updated_at` AS `updated_at`,\n\n -- `psql_dcpublic.products`.`current_stock` AS `current_stock`,\n\n -- `psql_dcpublic.products`.`description_2` AS `description_2`,\n\n -- `psql_dcpublic.products`.`replenishment` AS `replenishment`,\n\n -- `psql_dcpublic.products`.`full_description` AS `full_description`,\n\n -- `psql_dcpublic.products`.`main_image` AS `main_image`,\n\n -- `psql_dcpublic.products`.`secondary_image` AS `secondary_image`,\n\n -- `psql_dcpublic.products`.`validation_pending` AS `validation_pending`,\n\n -- `psql_dcpublic.products`.`vendor_source_id` AS `vendor_source_id`,\n\n -- `psql_dcpublic.products`.`data` AS `data`,\n\n -- `psql_dcpublic.products`.`main_attributes` AS `main_attributes`,\n\n -- `psql_dcpublic.products`.`slug` AS `slug`,\n\n -- `psql_dcpublic.products`.`slugs` AS `slugs`,\n\n -- `psql_dcpublic.products`.`price_data` AS `price_data`,\n\n -- `psql_dcpublic.products`.`stock_data` AS `stock_data`,\n\n -- `psql_dcpublic.products`.`promo_data` AS `promo_data`,\n\n -- `psql_dcpublic.products`.`can_be_delivered` AS `can_be_delivered`,\n\n -- `psql_dcpublic.products`.`show_on_ecommerce` AS `show_on_ecommerce`,\n\n -- `psql_dcpublic.products`.`description_text` AS `description_text`,\n\n -- `psql_dcpublic.products`.`is_service` AS `is_service`,\n\n -- `psql_dcpublic.products`.`execution_time` AS `execution_time`,\n\n -- `psql_dcpublic.products`.`blocked_for_automatic_updates` AS `blocked_for_automatic_updates`,\n\n -- `psql_dcpublic.products`.`ecommerce_category_id` AS `ecommerce_category_id`,\n\n -- `psql_dcpublic.products`.`is_marketplace` AS `is_marketplace`,\n\n -- `psql_dcpublic.products`.`relevancy` AS `relevancy`,\n\n -- `psql_dcpublic.products`.`data_to_index` AS `data_to_index`,\n\n -- `psql_dcpublic.products`.`product_type_id` AS `product_type_id`,\n\n -- `psql_dcpublic.products`.`is_encargo` AS `is_encargo`,\n\n -- `psql_dcpublic.products`.`custom_price` AS `custom_price`,\n\n -- `psql_dcpublic.products`.`min_platform_stock` AS `min_platform_stock`,\n\n -- `psql_dcpublic.products`.`mechanic_commission` AS `mechanic_commission`,\n\n -- `psql_dcpublic.products`.`salesman_commission` AS `salesman_commission`,\n\n -- `psql_dcpublic.products`.`check_type_id` AS `check_type_id`,\n\n -- `psql_dcpublic.products`.`reviewer_commission` AS `reviewer_commission`,\n\n -- `psql_dcpublic.products`.`eci_allowed` AS `eci_allowed`,\n\n -- `psql_dcpublic.products`.`eci_file_path` AS `eci_file_path`,\n\n -- `psql_dcpublic.products`.`eci_files` AS `eci_files`,\n\n -- `psql_dcpublic.products`.`current_average_price` AS `current_average_price`,\n\n -- `psql_dcpublic.products`.`current_aurgi_all_included_price` AS `current_aurgi_all_included_price`,\n\n -- `psql_dcpublic.products`.`current_mt_all_included_price` AS `current_mt_all_included_price`,\n\n -- `psql_dcpublic.products`.`current_aurgi_price` AS `current_aurgi_price`,\n\n -- `psql_dcpublic.products`.`current_mt_price` AS `current_mt_price`,\n\n -- `psql_dcpublic.products`.`aurgi_published` AS `aurgi_published`,\n\n -- `psql_dcpublic.products`.`mt_published` AS `mt_published`,\n\n -- `psql_dcpublic.products`.`current_prices_updated_at` AS `current_prices_updated_at`,\n\n -- `psql_dcpublic.products`.`is_tecdoc` AS `is_tecdoc`,\n\n -- `psql_dcpublic.products`.`cross_docking` AS `cross_docking`,\n\n -- `psql_dcpublic.products`.`shipping_price` AS `shipping_price`,\n\n -- `psql_dcpublic.products`.`leadtime_to_ship` AS `leadtime_to_ship`,\n\n -- `psql_dcpublic.products`.`extra_item_set_id` AS `extra_item_set_id`,\n\n -- `psql_dcpublic.products`.`reference_supplier` AS `reference_supplier`,\n\n -- `psql_dcpublic.products`.`available_in_garage` AS `available_in_garage`,\n\n -- `psql_dcpublic.products`.`equivalents` AS `equivalents`,\n\n -- `psql_dcpublic.products`.`allow_automatic_provider_order` AS `allow_automatic_provider_order`,\n\n -- `psql_dcpublic.products`.`combo_products` AS `combo_products`,\n\n -- `psql_dcpublic.products`.`energy_tag` AS `energy_tag`,\n\n -- `psql_dcpublic.products`.`tyre_class` AS `tyre_class`,\n\n -- `psql_dcpublic.products`.`is_nav` AS `is_nav`,\n\n -- `psql_dcpublic.products`.`hierarchical` AS `hierarchical`,\n\n -- `psql_dcpublic.products`.`normalized_ref` AS `normalized_ref`,\n\n -- `psql_dcpublic.products`.`datastream_metadata` AS `datastream_metadata`,\n\n -- `psql_dcpublic.products`.`trans_descriptions` AS `trans_descriptions`,\n\n -- `psql_dcpublic.products`.`proportional_execution_time` AS `proportional_execution_time`,\n\n -- `psql_dcpublic.products`.`is_merchant` AS `is_merchant`,\n\n -- `psql_dcpublic.products`.`shop_pick_up` AS `shop_pick_up`,\n\n -- `psql_dcpublic.products`.`tecdoc_brand` AS `tecdoc_brand`,\n\n -- `psql_dcpublic.products`.`quantity_unit` AS `quantity_unit`,\n\n -- `psql_dcpublic.products`.`quantity_per_unit` AS `quantity_per_unit`,\n\n -- `psql_dcpublic.products`.`last_indexed_at` AS `last_indexed_at`,\n\n -- `psql_dcpublic.products`.`default_ean` AS `default_ean`,\n\n -- `psql_dcpublic.products`.`shopping_feed_last_updated` AS `shopping_feed_last_updated`,\n\n -- `psql_dcpublic.products`.`variant_code_id` AS `variant_code_id`,\n\n -- `psql_dcpublic.products`.`redirection_redirect_type` AS `redirection_redirect_type`,\n\n -- `psql_dcpublic.products`.`redirection_redirect_to` AS `redirection_redirect_to`,\n\n -- `psql_dcpublic.products`.`resource_type_id` AS `resource_type_id`,\n\n -- `psql_dcpublic.products`.`encargo_type` AS `encargo_type`,\n\n -- `psql_dcpublic.products`.`reception_time` AS `reception_time`,\n\n -- `psql_dcpublic.products`.`logistic_class` AS `logistic_class`,\n\n -- `psql_dcpublic.products`.`in_glass_idx` AS `in_glass_idx`,\n\n -- `psql_dcpublic.products`.`default_provider_id` AS `default_provider_id`,\n\n -- `psql_dcpublic.products`.`is_glass_commission` AS `is_glass_commission`,\n\n -- `psql_dcpublic.products`.`adas_required` AS `adas_required`,\n\n CASE\n\n WHEN `psql_dcpublic.products`.`is_service` = TRUE THEN 'Servicio'\n\n WHEN `psql_dcpublic.products`.`is_encargo` = TRUE THEN 'Encargo'\n\n WHEN `psql_dcpublic.products`.`description` IS NOT NULL\n\n AND `psql_dcpublic.products`.`description` LIKE '%SIGNFU%' THEN 'Ecotasa'\n\n WHEN `psql_dcpublic.products`.`description` IS NOT NULL\n\n AND `psql_dcpublic.products`.`description` LIKE '%APORTACION SIGAUS%' THEN 'Aportación SIGAUS'\n\n WHEN `Product_Categories`.`description` = 'TECDOC' THEN 'TecDoc'\n\n WHEN `Product_Categories`.`description` = 'Devolucion' THEN 'Devolución'\n\n WHEN `Product_Categories`.`description` = 'Lavado' THEN 'Lavado'\n\n WHEN `Product_Categories`.`description` = 'KITS' THEN 'Kits'\n\n WHEN `psql_dcpublic.products`.`description` IS NOT NULL\n\n AND `psql_dcpublic.products`.`description` LIKE '%ENTREGA%' THEN 'Entrega'\n\n WHEN `Product_Categories`.`description` = 'Autingo' THEN 'Autingo'\n\n WHEN `psql_dcpublic.products`.`nav_id` = 'COMISION_MIRAKL' THEN 'Comisión Mirakl'\n\n WHEN `Product_Categories`.`description` = 'Servicio Rapido' THEN 'Servicio Rápido'\n\n WHEN `Product_Categories`.`description` = 'Consumible taller' THEN 'Consumible taller'\n\n WHEN `Product_Categories`.`description` = 'Anticipos Aurgi' THEN 'Anticipos Aurgi'\n\n WHEN `Product_Categories`.`description` = 'Montaje Alcampo' THEN 'Montaje Alcampo'\n\n WHEN `Product_Categories`.`description` = 'Varios' THEN 'Varios'\n\n ELSE 'Productos'\n\n END AS `tipo`,\n\n -- `Product_Groups`.`description` AS `Product_Groups__description`,\n\n -- `Product_Groups`.`nav_id` AS `Product_Groups__nav_id`,\n\n -- `Product_Categories`.`description` AS `Product_Categories__description`,\n\n -- `Product_Categories`.`nav_id` AS `Product_Categories__nav_id`\n\n FROM\n\n `psql_dcpublic.products`\n\n LEFT JOIN `psql_dcpublic.product_groups` AS `Product_Groups` ON `psql_dcpublic.products`.`product_group_id` = `Product_Groups`.`id`\n\n LEFT JOIN `psql_dcpublic.product_categories` AS `Product_Categories` ON `Product_Groups`.`product_category_id` = `Product_Categories`.`id`\n\n ) AS `Tipologia_de_producto` ON `psql_dcpublic.products`.`id` = `Tipologia_de_producto`.`id`\n\n \n\nWHERE\n\n (`psql_dcpublic.products`.`nav_id` IS NOT NULL)\n\n AND (\n\n (`psql_dcpublic.products`.`nav_id` <> '')\n\n OR (`psql_dcpublic.products`.`nav_id` IS NULL)\n\n )\n\n AND (`psql_dcpublic.products`.`id` IS NOT NULL)\n\n \n\nORDER BY\n\n `psql_dcpublic.products`.`nav_id` DESC\n\n ) AS `source`\n)\nUNION ALL\nSELECT\n CAST(NULL AS INT64) AS `id`,\n pt.nav_id AS `nav_id`,\n pt.description AS `description`,\n CAST(NULL AS BOOLEAN) AS `is_tecdoc`,\n 'Activo' AS `Status_de_producto`,\n FALSE AS `Is_Encargo`,\n FALSE AS `is_service`,\n 'Producto de Navision' AS `is_tpv`,\n CAST(NULL AS STRING) AS `Tipo___Navision`,\n CAST(NULL AS STRING) AS `Indicadores_tag`,\n CAST(NULL AS STRING) AS `Product_Groups__description`,\n CAST(NULL AS STRING) AS `Product_Categories__description`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo1`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo2`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo3`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo5`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo4`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo6`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo7`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo8`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo9`,\n CAST(NULL AS STRING) AS `Producto___Nav__idAtributo10`,\n CAST(NULL AS STRING) AS `Producto___Nav__idCategoria`,\n CAST(NULL AS STRING) AS `Producto___Nav__idGrupo`,\n pt.cat_cgq AS `Categoria_16_07_cgq___tipo_old__cat_cgq`,\n pt.tipo_cgq AS `Categoria_16_07_cgq___tipo_old__tipo_cgq`,\n pt.subcat_cgq AS `Categoria_16_07_cgq___tipo_old__subcate_cgq`,\n CAST(NULL AS STRING) AS `Tipologia_de_producto__tipo`\n FROM `autingo-159109.claude_bi.productos_tasa_mat` pt\n) base\nLEFT JOIN `autingo-159109.external_datasets.product_object_images` img USING (nav_id);", + "refs": [ + "anjana_bi_datamart.Cruce_16_07_cgq", + "claude_bi.productos_tasa_mat", + "external_datasets.product_object_images", + "psql_dcpublic.product_categories", + "psql_dcpublic.product_groups", + "psql_dcpublic.products", + "stg_anjana_bi.producto" + ], + "depth": 1 + }, + "anjana_bi_datamart.Cruce_16_07_cgq": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.anjana_bi_datamart.Cruce_16_07_cgq`\n(\n string_field_0 STRING,\n string_field_1 STRING,\n string_field_2 STRING,\n string_field_3 STRING,\n string_field_4 STRING,\n string_field_5 STRING,\n string_field_6 STRING\n);", + "refs": [], + "depth": 2 + }, + "claude_bi.productos_tasa_mat": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.claude_bi.productos_tasa_mat`\n(\n nav_id STRING,\n description STRING,\n cat_cgq STRING,\n subcat_cgq STRING,\n tipo_cgq STRING,\n origen STRING\n)\nCLUSTER BY nav_id;", + "refs": [], + "depth": 2 + }, + "external_datasets.product_object_images": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.external_datasets.product_object_images`\n(\n nav_id STRING,\n imagen_principal STRING,\n imagen_secundaria STRING,\n n_imagenes INT64,\n origen STRING\n);", + "refs": [], + "depth": 2 + }, + "psql_dcpublic.product_categories": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.product_categories`\n(\n id INT64,\n description STRING,\n slug STRING,\n nav_id STRING,\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n purchase_strategy INT64,\n min_platform_stock FLOAT64,\n shipping_price FLOAT64,\n h1 STRING,\n meta_title STRING,\n meta_description STRING,\n mechanic_commission FLOAT64,\n salesman_commission FLOAT64,\n reviewer_commission FLOAT64,\n datastream_metadata STRUCT,\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 2 + }, + "psql_dcpublic.product_groups": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.product_groups`\n(\n id INT64,\n product_category_id INT64,\n description STRING,\n slug STRING,\n nav_id STRING,\n navision_groups_config JSON,\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n h1 STRING,\n meta_title STRING,\n meta_description STRING,\n min_platform_stock FLOAT64,\n mechanic_commission FLOAT64,\n salesman_commission FLOAT64,\n reviewer_commission FLOAT64,\n datastream_metadata STRUCT,\n cross_selling_nav_ids JSON,\n encargo_group_id INT64,\n ecommerce_category_id INT64,\n logistic_class STRING,\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 2 + }, + "psql_dcpublic.products": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.products`\n(\n id INT64,\n product_group_id INT64,\n nav_id STRING,\n normalized_code STRING,\n old_aurgi_family STRING,\n description STRING,\n web_description STRING,\n search_description STRING,\n status INT64,\n is_blocked BOOL,\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n current_stock STRING,\n description_2 STRING,\n replenishment BOOL,\n full_description STRING,\n main_image STRING,\n secondary_image STRING,\n validation_pending BOOL,\n vendor_source_id INT64,\n data JSON,\n main_attributes JSON,\n slug STRING,\n slugs JSON,\n price_data JSON,\n stock_data JSON,\n promo_data JSON,\n can_be_delivered BOOL,\n show_on_ecommerce BOOL,\n description_text STRING,\n is_service BOOL,\n execution_time INT64,\n blocked_for_automatic_updates BOOL,\n ecommerce_category_id INT64,\n is_marketplace BOOL,\n relevancy INT64,\n data_to_index JSON,\n product_type_id INT64,\n is_encargo BOOL,\n custom_price BOOL,\n min_platform_stock FLOAT64,\n mechanic_commission FLOAT64,\n salesman_commission FLOAT64,\n check_type_id INT64,\n reviewer_commission FLOAT64,\n eci_allowed BOOL,\n eci_file_path STRING,\n eci_files JSON,\n current_average_price FLOAT64,\n current_aurgi_all_included_price FLOAT64,\n current_mt_all_included_price FLOAT64,\n current_aurgi_price FLOAT64,\n current_mt_price FLOAT64,\n aurgi_published BOOL,\n mt_published BOOL,\n current_prices_updated_at TIMESTAMP,\n is_tecdoc BOOL,\n cross_docking BOOL,\n shipping_price FLOAT64,\n leadtime_to_ship INT64,\n extra_item_set_id INT64,\n reference_supplier STRING,\n available_in_garage BOOL,\n equivalents JSON,\n allow_automatic_provider_order BOOL,\n combo_products JSON,\n energy_tag STRING,\n tyre_class STRING,\n is_nav BOOL,\n hierarchical JSON,\n normalized_ref STRING,\n datastream_metadata STRUCT,\n trans_descriptions JSON,\n proportional_execution_time BOOL,\n is_merchant BOOL,\n shop_pick_up BOOL,\n tecdoc_brand STRING,\n quantity_unit INT64,\n quantity_per_unit INT64,\n last_indexed_at TIMESTAMP,\n default_ean STRING,\n shopping_feed_last_updated TIMESTAMP,\n variant_code_id INT64,\n redirection_redirect_type STRING,\n redirection_redirect_to STRING,\n resource_type_id INT64,\n encargo_type INT64,\n reception_time INT64,\n logistic_class STRING,\n in_glass_idx BOOL,\n default_provider_id INT64,\n is_glass_commission BOOL,\n adas_required BOOL,\n is_minderest BOOL,\n provided_signal_word STRING,\n has_hazards BOOL,\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 2 + }, + "stg_anjana_bi.producto": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.stg_anjana_bi.producto`\nAS select\r\np.No_ AS idProducto,\r\ncase\r\n\twhen p.Description is null or p.Description = '' then 'Sin especificar'\r\n\telse p.Description\r\nend AS nombreProducto,\r\np.Activo_Externo AS idActivoExterno,\r\np.Liquidacion AS idLiquidacion,\r\np.Estado_AURGI AS idEstado,\r\ncase\r\n\twhen p.Tipo_Producto_AURGI is null then -1\r\n\telse p.Tipo_Producto_AURGI\r\nend AS idTipo,\r\ncase\r\n\twhen p.Item_Category_Code is null or p.Item_Category_Code = '' then 'SE'\r\n\telse p.Item_Category_Code\r\nend AS idCategoria,\r\ncase\r\n\twhen p.Product_Group_Code is null or p.Product_Group_Code = '' then 'SE'\r\n\telse p.Product_Group_Code\r\nend AS idGrupo,\r\ncase\r\n\twhen p.Valor_Atributo_1 is null or p.Valor_Atributo_1 = '' then 'SE'\r\n\telse p.Valor_Atributo_1\r\nend AS idAtributo1,\r\ncase\r\n\twhen p.Valor_Atributo_2 is null or p.Valor_Atributo_2 = '' then 'SE'\r\n\telse p.Valor_Atributo_2\r\nend AS idAtributo2,\r\ncase\r\n\twhen p.Valor_Atributo_3 is null or p.Valor_Atributo_3 = '' then 'SE'\r\n\telse p.Valor_Atributo_3\r\nend AS idAtributo3,\r\ncase\r\n\twhen p.Valor_Atributo_4 is null or p.Valor_Atributo_4 = '' then 'SE'\r\n\telse p.Valor_Atributo_4\r\nend AS idAtributo4,\r\ncase\r\n\twhen p.Valor_Atributo_5 is null or p.Valor_Atributo_5 = '' then 'SE'\r\n\telse p.Valor_Atributo_5\r\nend AS idAtributo5,\r\ncase\r\n\twhen p.Valor_Atributo_6 is null or p.Valor_Atributo_6 = '' then 'SE'\r\n\telse p.Valor_Atributo_6\r\nend AS idAtributo6,\r\ncase\r\n\twhen p.Valor_Atributo_7 is null or p.Valor_Atributo_7 = '' then 'SE'\r\n\telse p.Valor_Atributo_7\r\nend AS idAtributo7,\r\ncase\r\n\twhen p.Valor_Atributo_8 is null or p.Valor_Atributo_8 = '' then 'SE'\r\n\telse p.Valor_Atributo_8\r\nend AS idAtributo8,\r\ncase\r\n\twhen p.Valor_Atributo_9 is null or p.Valor_Atributo_9 = '' then 'SE'\r\n\telse p.Valor_Atributo_9\r\nend AS idAtributo9,\r\ncase\r\n\twhen p.Valor_Atributo_10 is null or p.Valor_Atributo_10 = '' then 'SE'\r\n\telse p.Valor_Atributo_10\r\nend AS idAtributo10,\r\n0 AS Periodo_de_Cobertura,\r\ncase \r\n\twhen b.cod__matricula is null or b.cod__matricula = '' then 'Sin especificar'\r\n\telse b.cod__matricula\r\nend AS Matricula\r\nfrom `autingo-159109.mssql2022_dbo.item` p \r\nleft join `autingo-159109.mssql2022_dbo.equivalencias_matriculas_saf` b\r\n on p.No_ = b.num__producto\r\nunion all\r\nselect\r\n'SE' AS idProducto,\r\n'SE' AS nombreProducto,\r\n0 AS idActivoExterno,\r\n0 AS idLiquidacion,\r\n0 AS idEstado,\r\n-1 AS idTipo,\r\n'SE' AS idCategoria,\r\n'SE' AS idGrupo,\r\n'SE' AS idAtributo1,\r\n'SE' AS idAtributo2,\r\n'SE' AS idAtributo3,\r\n'SE' AS idAtributo4,\r\n'SE' AS idAtributo5,\r\n'SE' AS idAtributo6,\r\n'SE' AS idAtributo7,\r\n'SE' AS idAtributo8,\r\n'SE' AS idAtributo9,\r\n'SE' AS idAtributo10,\r\n0 AS Periodo_de_Cobertura,\r\n'Sin especificar' AS Matricula;", + "refs": [ + "mssql2022_dbo.equivalencias_matriculas_saf", + "mssql2022_dbo.item" + ], + "depth": 2 + }, + "mssql2022_dbo.equivalencias_matriculas_saf": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.mssql2022_dbo.equivalencias_matriculas_saf`\n(\n num__producto STRING,\n stock_web_plataforma BIGNUMERIC,\n fecha_modificacion DATETIME,\n matricula_antigua STRING,\n error_matricula INT64,\n cod__matricula STRING,\n usuario_modificacion STRING,\n fecha_alta DATETIME,\n usuario_alta STRING,\n timestamp BYTES,\n _fivetran_deleted BOOL,\n _fivetran_synced TIMESTAMP,\n PRIMARY KEY (num__producto) NOT ENFORCED\n);", + "refs": [], + "depth": 3 + }, + "mssql2022_dbo.item": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.mssql2022_dbo.item`\n(\n no_ STRING,\n reserve INT64,\n price_profit_calculation INT64,\n estado_aurgi INT64,\n alternative_item_no_ STRING,\n standard_cost BIGNUMERIC,\n product_group_code STRING,\n serial_nos_ STRING,\n imagen_pag_ppal STRING,\n manufacturing_policy INT64,\n periodo_de_vigencia___hasta__ DATETIME,\n perfil_web STRING,\n deshabilitar_prestashop INT64,\n item_category_code STRING,\n etiqueta STRING,\n duty_code STRING,\n categoria1_web STRING,\n blocked INT64,\n producto_aliexpress INT64,\n rolled_up_capacity_cost BIGNUMERIC,\n special_equipment_code STRING,\n freight_type STRING,\n desactivado_mt INT64,\n use_cross_docking INT64,\n llanta_web STRING,\n fecha_desactivado_mt DATETIME,\n pertenecegrupo01 INT64,\n pertenecegrupo05 INT64,\n pertenecegrupo04 INT64,\n descripcion_prestashop STRING,\n pertenecegrupo03 INT64,\n pertenecegrupo02 INT64,\n minimum_order_quantity BIGNUMERIC,\n unit_volume BIGNUMERIC,\n maximum_order_quantity BIGNUMERIC,\n periodo_de_vigencia___desde___m DATETIME,\n producto_aurgi INT64,\n timestamp BYTES,\n producto_mt_canarias INT64,\n created_from_nonstock_item INT64,\n routing_no_ STRING,\n overhead_rate BIGNUMERIC,\n order_tracking_policy INT64,\n destacado_prestashop INT64,\n single_level_material_cost BIGNUMERIC,\n liquidacion INT64,\n cod__velocidad_web STRING,\n pvp___anterior___mt BIGNUMERIC,\n vat_bus__posting_gr___price_ STRING,\n imagen_principal STRING,\n meta_stock BIGNUMERIC,\n last_direct_cost BIGNUMERIC,\n valor_atributo_1 STRING,\n producto_lh INT64,\n valor_atributo_3 STRING,\n valor_atributo_2 STRING,\n no__series STRING,\n tiempo_ejecucion_teorico BIGNUMERIC,\n valor_atributo_9 STRING,\n fecha_modificacion DATETIME,\n valor_atributo_8 STRING,\n search_description STRING,\n valor_atributo_5 STRING,\n valor_atributo_4 STRING,\n actualizar_imagen_web INT64,\n valor_atributo_7 STRING,\n valor_atributo_6 STRING,\n rolled_up_material_cost BIGNUMERIC,\n maximum_inventory BIGNUMERIC,\n proveedor_alta STRING,\n stock_minimo_web BIGNUMERIC,\n base_unit_of_measure STRING,\n desactivado_aurgi INT64,\n categoria2_web STRING,\n item_disc__group STRING,\n vat_prod__posting_group STRING,\n producto_autingo INT64,\n last_unit_cost_calc__date DATETIME,\n referencia_para_catalogo STRING,\n unit_list_price BIGNUMERIC,\n asin STRING,\n oferta INT64,\n producto_mt INT64,\n reorder_point BIGNUMERIC,\n budget_profit BIGNUMERIC,\n reordering_policy INT64,\n flushing_method INT64,\n service_item_group STRING,\n precio_web BIGNUMERIC,\n producto_ebay INT64,\n automatic_ext__texts INT64,\n activo_externo INT64,\n shelf_bin_no_ STRING,\n imagen_pag_sec STRING,\n budget_quantity BIGNUMERIC,\n marca_web STRING,\n producto_historico INT64,\n sales_unit_of_measure STRING,\n usuario_modificacion STRING,\n referencia_antigua STRING,\n replenishment_system INT64,\n net_weight BIGNUMERIC,\n busqueda_web STRING,\n pvp___anterior__ BIGNUMERIC,\n global_dimension_1_code STRING,\n statistics_group INT64,\n indirect_cost__ BIGNUMERIC,\n oferta_mt INT64,\n ean_code STRING,\n duty_unit_conversion BIGNUMERIC,\n duty_due__ BIGNUMERIC,\n prestashop INT64,\n unit_price BIGNUMERIC,\n tax_group_code STRING,\n ancho_web STRING,\n producto_glass INT64,\n webmt INT64,\n ae_id STRING,\n imgm STRING,\n categoria3_web STRING,\n allow_invoice_disc_ INT64,\n imgl STRING,\n destacado_web INT64,\n alta_pendiente INT64,\n descripcion_editable INT64,\n aviso_en_tpv STRING,\n imgs STRING,\n lot_size BIGNUMERIC,\n price_unit_conversion INT64,\n producto_amazon INT64,\n volumenequivalente BIGNUMERIC,\n deshabilitar_web_mt INT64,\n description STRING,\n country_region_of_origin_code STRING,\n allow_online_adjustment INT64,\n no__2 STRING,\n units_per_parcel BIGNUMERIC,\n global_dimension_2_code STRING,\n rounding_precision BIGNUMERIC,\n safety_lead_time STRING,\n unit_cost BIGNUMERIC,\n tipo_producto_aurgi INT64,\n periodo_de_vigencia___desde__ DATETIME,\n atencion_comercial INT64,\n descripcion_web STRING,\n description_2 STRING,\n commission_group INT64,\n profit__ BIGNUMERIC,\n safety_stock_quantity BIGNUMERIC,\n inventory_value_zero INT64,\n item_tracking_code STRING,\n mostrar_web INT64,\n medidas_web INT64,\n price_includes_vat INT64,\n gen__prod__posting_group STRING,\n inventory_posting_group STRING,\n low_level_code INT64,\n cod__subfamilia STRING,\n country_region_purchased_code STRING,\n cod__variedad STRING,\n consumible INT64,\n familia_aurgi__antigua_ STRING,\n cost_is_adjusted INT64,\n listing_id STRING,\n categoria_marketplace STRING,\n common_item_no_ STRING,\n order_multiple BIGNUMERIC,\n include_inventory INT64,\n budgeted_amount BIGNUMERIC,\n reorder_quantity BIGNUMERIC,\n periodo_de_vigencia___hasta___m DATETIME,\n fecha_creacion DATETIME,\n producto_exclusivo_web INT64,\n durability STRING,\n unidades_por_caja_web BIGNUMERIC,\n vendor_item_no_ STRING,\n tariff_no_ STRING,\n production_bom_no_ STRING,\n pvp_manual INT64,\n ecotasasigaus STRING,\n cod__familia STRING,\n doble_boleta INT64,\n actualizar_producto_web INT64,\n valor_atributo_10 STRING,\n gross_weight BIGNUMERIC,\n especificaciones_tecnicas STRING,\n valor_atributo_14 STRING,\n valor_atributo_13 STRING,\n categorybc STRING,\n valor_atributo_12 STRING,\n valor_atributo_11 STRING,\n reorder_cycle STRING,\n purch__unit_of_measure STRING,\n desactivar_control_uds_caja INT64,\n manufacturer_code STRING,\n last_date_modified DATETIME,\n valor_atributo_16 STRING,\n usuario_creacion STRING,\n valor_atributo_15 STRING,\n expiration_calculation STRING,\n discrete_order_quantity INT64,\n producto_saf STRING,\n costing_method INT64,\n lead_time_calculation STRING,\n generar_autofactura INT64,\n vendor_no_ STRING,\n lot_nos_ STRING,\n aire_acondicionado INT64,\n kiloequivalente BIGNUMERIC,\n _fivetran_deleted BOOL,\n _fivetran_synced TIMESTAMP,\n exclusivoscompra INT64,\n PRIMARY KEY (no_) NOT ENFORCED\n);", + "refs": [], + "depth": 3 + }, + "clientes_intel.fact_transaccion": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.fact_transaccion`\n(\n transaccion_id INT64,\n vehiculo_id STRING,\n customer_id_tpv INT64,\n fecha DATE,\n centro_nav_id STRING,\n centro_nombre STRING,\n empresa STRING,\n zona STRING,\n canal STRING,\n categoria STRING,\n grupo STRING,\n producto_nav_id STRING,\n producto_desc STRING,\n cantidad INT64,\n importe FLOAT64,\n importe_con_iva FLOAT64,\n coste_unitario FLOAT64,\n coste_linea_aprox FLOAT64,\n id_otr INT64,\n otr_status STRING,\n id_factura_nav STRING,\n otr_nav_id STRING,\n origen STRING,\n persona_id INT64\n)\nPARTITION BY fecha\nCLUSTER BY persona_id, centro_nav_id;", + "refs": [], + "depth": 1 + }, + "customer_marts.customer_category_spend": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_category_spend`\nAS WITH agg AS (\n SELECT\n persona_id,\n categoria,\n grupo,\n SUM(importe) AS gasto,\n SUM(importe - coste_linea_aprox) AS margen,\n COUNT(DISTINCT id_factura_nav) AS n_tickets,\n COUNT(*) AS n_lineas,\n SUM(cantidad) AS uds,\n MIN(fecha) AS primera_fecha,\n MAX(fecha) AS ultima_fecha\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id, categoria, grupo\n)\nSELECT\n persona_id,\n categoria,\n grupo,\n gasto,\n margen,\n n_tickets,\n n_lineas,\n uds,\n primera_fecha,\n ultima_fecha,\n SAFE_DIVIDE(gasto, SUM(gasto) OVER (PARTITION BY persona_id)) AS share_gasto_categoria\nFROM agg;", + "refs": [ + "clientes_intel.fact_transaccion" + ], + "depth": 0 + }, + "customer_marts.customer_channel": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_channel`\nAS WITH fuentes AS (\n SELECT\n persona_id,\n ARRAY_AGG(DISTINCT fuente IGNORE NULLS ORDER BY fuente) AS fuentes\n FROM `autingo-159109.clientes_intel.map_persona_fuente`\n GROUP BY persona_id\n)\nSELECT\n dp.persona_id,\n -- canal preferido transaccional\n fcp.canal_preferido,\n -- mix de canal (pct de fact_transaccion)\n fcp.pct_aurgi,\n fcp.pct_motortown,\n fcp.pct_web,\n fcp.pct_servicio,\n -- canal de entrada / canal8\n c8.canal8,\n seg.canal_entrada,\n -- fuentes (ARRAY)\n f.fuentes,\n ARRAY_LENGTH(f.fuentes) AS n_fuentes\nFROM `autingo-159109.clientes_intel.dim_persona` dp\nLEFT JOIN `autingo-159109.clientes_intel.feat_cliente_persona` fcp USING (persona_id)\nLEFT JOIN `autingo-159109.clientes_intel.map_persona_canal8` c8 USING (persona_id)\nLEFT JOIN `autingo-159109.clientes_intel.seg_cliente_360` seg USING (persona_id)\nLEFT JOIN fuentes f USING (persona_id);", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.feat_cliente_persona", + "clientes_intel.map_persona_canal8", + "clientes_intel.map_persona_fuente", + "clientes_intel.seg_cliente_360" + ], + "depth": 0 + }, + "clientes_intel.dim_persona": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.dim_persona`\n(\n persona_id INT64,\n confianza STRING,\n metodo_match STRING,\n document_number STRING,\n document_kind STRING,\n full_name STRING,\n email STRING,\n phone STRING,\n postal_code STRING,\n es_empresa BOOL,\n salesforce_customer_id STRING,\n tpv_nav_id STRING,\n email_optout BOOL,\n sms_optout BOOL,\n whatsapp_optout BOOL,\n do_not_call BOOL,\n num_fuentes INT64,\n in_tpv BOOL,\n in_web BOOL,\n in_otr BOOL,\n in_navision BOOL,\n in_citaprevia BOOL,\n in_salesforce BOOL,\n in_users BOOL\n);", + "refs": [], + "depth": 1 + }, + "clientes_intel.feat_cliente_persona": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.feat_cliente_persona`\n(\n persona_id INT64,\n recency_days INT64,\n frequency INT64,\n monetary_total FLOAT64,\n ticket_medio FLOAT64,\n ticket_std FLOAT64,\n tenure_days INT64,\n primera_compra DATE,\n ultima_compra DATE,\n valor_ponderado FLOAT64,\n mean_inter_visit_days FLOAT64,\n std_inter_visit_days FLOAT64,\n n_categorias INT64,\n n_centros INT64,\n pct_aurgi FLOAT64,\n pct_motortown FLOAT64,\n pct_web FLOAT64,\n pct_servicio FLOAT64,\n categoria_top STRING,\n pct_top_cat FLOAT64,\n centro_principal STRING,\n empresa_principal STRING,\n mes_top_compra INT64,\n margen_total FLOAT64,\n n_vehiculos INT64,\n es_excluido BOOL,\n r_score INT64,\n f_score INT64,\n m_score INT64,\n rfm_score INT64,\n rfm_sum INT64,\n rfm_segment STRING,\n margen_pct FLOAT64,\n canal_preferido STRING,\n full_name STRING,\n email STRING,\n phone STRING,\n es_empresa BOOL,\n confianza_identidad STRING,\n num_fuentes INT64,\n tiene_email BOOL,\n tiene_telefono BOOL,\n email_optout BOOL,\n sms_optout BOOL,\n whatsapp_optout BOOL,\n do_not_call BOOL,\n contactable BOOL\n)\nCLUSTER BY rfm_segment, persona_id;", + "refs": [], + "depth": 1 + }, + "clientes_intel.map_persona_canal8": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.map_persona_canal8`\n(\n persona_id INT64,\n canal8 STRING\n);", + "refs": [], + "depth": 1 + }, + "clientes_intel.map_persona_fuente": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.map_persona_fuente`\n(\n persona_id INT64,\n fuente STRING,\n source_id STRING\n);", + "refs": [], + "depth": 1 + }, + "clientes_intel.seg_cliente_360": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.seg_cliente_360`\n(\n persona_id INT64,\n full_name STRING,\n es_empresa BOOL,\n es_b2c BOOL,\n rfm_segment STRING,\n cluster STRING,\n tipo_cliente STRING,\n clv_nivel STRING,\n r_score INT64,\n f_score INT64,\n m_score INT64,\n rfm_score INT64,\n clv_score INT64,\n clv_decil INT64,\n clv_norm FLOAT64,\n recency_days INT64,\n frequency INT64,\n monetary_total FLOAT64,\n valor_ponderado FLOAT64,\n ticket_medio FLOAT64,\n mean_inter_visit_days FLOAT64,\n margen_total FLOAT64,\n margen_pct FLOAT64,\n clv_12m_margen FLOAT64,\n clv_12m_ingreso FLOAT64,\n prob_activo FLOAT64,\n n_categorias INT64,\n categoria_top STRING,\n n_centros INT64,\n centro_principal STRING,\n empresa_principal STRING,\n canal_entrada STRING,\n n_vehiculos INT64,\n itv_due_este_anio BOOL,\n antiguedad_media FLOAT64,\n score_confianza_norm FLOAT64,\n data_points INT64,\n contactable_email BOOL,\n contactable_telefono BOOL,\n email_optout BOOL,\n sms_optout BOOL,\n do_not_call BOOL,\n email STRING,\n phone STRING,\n n_acciones_reco INT64,\n top_accion_reco STRING,\n top_motivo STRING,\n top_submotivo STRING,\n es_mutualista STRING,\n provincia STRING,\n ccaa STRING,\n n_citas INT64,\n dias_sin_cita INT64,\n dias_inactivo INT64\n)\nCLUSTER BY rfm_segment, cluster;", + "refs": [], + "depth": 1 + }, + "customer_marts.customer_contactability": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_contactability`\nAS SELECT\n dp.persona_id,\n fcp.tiene_email,\n fcp.tiene_telefono,\n fcp.contactable,\n dp.email_optout,\n dp.sms_optout,\n dp.whatsapp_optout,\n dp.do_not_call,\n dp.num_fuentes,\n dp.in_tpv,\n dp.in_web,\n dp.in_otr,\n dp.in_navision,\n dp.in_citaprevia,\n dp.in_salesforce,\n seg.data_points\nFROM `autingo-159109.clientes_intel.dim_persona` dp\nLEFT JOIN `autingo-159109.clientes_intel.feat_cliente_persona` fcp USING (persona_id)\nLEFT JOIN `autingo-159109.clientes_intel.seg_cliente_360` seg USING (persona_id);", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.feat_cliente_persona", + "clientes_intel.seg_cliente_360" + ], + "depth": 0 + }, + "customer_marts.customer_monetary": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_monetary`\nAS WITH ticket_stats AS (\n SELECT\n persona_id,\n COUNT(DISTINCT id_factura_nav) AS n_tickets,\n COUNT(*) AS n_lineas,\n SUM(cantidad) AS uds\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id\n)\nSELECT\n fcp.persona_id,\n ts.n_tickets,\n fcp.monetary_total AS gasto_total,\n fcp.ticket_medio,\n fcp.ticket_std,\n SAFE_DIVIDE(ts.n_lineas, ts.n_tickets) AS lineas_por_ticket,\n SAFE_DIVIDE(ts.uds, ts.n_tickets) AS uds_por_ticket,\n fcp.margen_total,\n fcp.margen_pct,\n fcp.valor_ponderado\nFROM `autingo-159109.clientes_intel.feat_cliente_persona` fcp\nLEFT JOIN ticket_stats ts USING (persona_id);", + "refs": [ + "clientes_intel.fact_transaccion", + "clientes_intel.feat_cliente_persona" + ], + "depth": 0 + }, + "customer_marts.customer_payment_method": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_payment_method`\nAS WITH cust2persona AS (\n SELECT customer_id_tpv, ANY_VALUE(persona_id) AS persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE customer_id_tpv IS NOT NULL\n GROUP BY customer_id_tpv\n),\npagos AS (\n SELECT\n o.customer_id,\n COALESCE(pt.description, '(sin tipo)') AS metodo_pago,\n p.amount,\n i.order_id\n FROM `autingo-159109.psql_dcpublic.tpv_orders_payment` p\n JOIN `autingo-159109.psql_dcpublic.tpv_orders_invoice` i ON p.invoice_id = i.id\n JOIN `autingo-159109.psql_dcpublic.tpv_orders_order` o ON o.id = i.order_id\n LEFT JOIN `autingo-159109.psql_dcpublic.tpv_payment_types` pt ON p.payment_type_id = pt.id\n WHERE i.order_id IS NOT NULL\n)\nSELECT\n c.persona_id,\n pg.metodo_pago,\n COUNT(*) AS n_pagos,\n COUNT(DISTINCT pg.order_id) AS n_ordenes,\n ROUND(SUM(pg.amount), 2) AS importe_pagado,\n ROUND(SAFE_DIVIDE(SUM(pg.amount),\n SUM(SUM(pg.amount)) OVER (PARTITION BY c.persona_id)), 4) AS share_importe\nFROM pagos pg\nJOIN cust2persona c ON c.customer_id_tpv = pg.customer_id\nWHERE c.persona_id IS NOT NULL\nGROUP BY c.persona_id, pg.metodo_pago;", + "refs": [ + "clientes_intel.fact_transaccion", + "psql_dcpublic.tpv_orders_invoice", + "psql_dcpublic.tpv_orders_order", + "psql_dcpublic.tpv_orders_payment", + "psql_dcpublic.tpv_payment_types" + ], + "depth": 0 + }, + "psql_dcpublic.tpv_orders_invoice": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.tpv_orders_invoice`\n(\n id INT64,\n nav_id STRING(128),\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n order_id INT64,\n created_by_id INT64,\n terminal_id INT64,\n invoice_path STRING(256),\n ticket_path STRING(256),\n status INT64,\n clear_one_path STRING,\n customer_copy JSON,\n vehicle_copy JSON,\n show_vehicle BOOL,\n profit NUMERIC(10, 2),\n profit_currency STRING(3),\n delivery_note STRING(56),\n datastream_metadata STRUCT,\n adjusted_profit NUMERIC(10, 2),\n adjusted_profit_currency STRING(3),\n registered_in_sol BOOL,\n from_standard_billing_flow BOOL,\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 1 + }, + "psql_dcpublic.tpv_orders_order": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.tpv_orders_order`\n(\n id INT64,\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n customer_id INT64,\n total_cost NUMERIC(10, 2),\n total_cost_currency STRING(3),\n calculate_payload JSON,\n terminal_id INT64,\n vehicle_id INT64,\n shift INT64,\n has_exchange BOOL,\n parent_id INT64,\n is_precaweb BOOL,\n calculate_uuid STRING,\n has_service_request BOOL,\n collective_id INT64,\n execution_time INT64,\n shift_model_id INT64,\n allow_all_returns BOOL,\n return_custom_times JSON,\n payment_methods_override JSON,\n referenced_user_id INT64,\n allow_return_with_open_otr BOOL,\n driver_id INT64,\n owner_id INT64,\n allow_returns_in_cash BOOL,\n gt_request_id INT64,\n requires_payment_authorization BOOL,\n can_be_returned_special_customer BOOL,\n tax_applied FLOAT64,\n datastream_metadata STRUCT,\n last_promo_line_number INT64,\n last_item_line_number INT64,\n last_update_in_nav TIMESTAMP,\n voucher_code STRING(50),\n kilometers INT64,\n autogenerated_from_otr_id INT64,\n reason STRING,\n appointment_id INT64,\n stop_ralarsa_sync BOOL,\n skip_provider_requires_delivery_confirmation BOOL,\n insurance_claim_id INT64,\n need_insurance_validation BOOL,\n validate_sol_pieces_when_closing_otr BOOL,\n accident_type_description STRING(255),\n operation_type STRING(50),\n insurance_policy_id INT64,\n damage_type_description STRING(255),\n recorder_car_renounce_id INT64,\n allowed_car_revision BOOL,\n date_car_renounce TIMESTAMP,\n last_incident_id INT64,\n show_budget_vs_appraisal_comparison BOOL,\n can_link_appraissal BOOL,\n source STRING(255),\n external_id STRING(500),\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 1 + }, + "psql_dcpublic.tpv_orders_payment": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.tpv_orders_payment`\n(\n id INT64,\n amount_currency STRING(3),\n amount NUMERIC(10, 2),\n created_at TIMESTAMP,\n updated_at TIMESTAMP,\n invoice_id INT64,\n payment_type_id INT64,\n ticket_path STRING(256),\n status_change INT64,\n payment_token STRING(256),\n nav_id STRING(128),\n created_by_id INT64,\n advance_payment_id INT64,\n change_amount NUMERIC(10, 2),\n change_amount_currency STRING(3),\n return_amount NUMERIC(10, 2),\n return_amount_currency STRING(3),\n related_return_id INT64,\n terminal_id INT64,\n commerce_id STRING(64),\n pin_pad_terminal_id STRING(64),\n card_number STRING(19),\n shift INT64,\n ticket_path_co STRING(256),\n shift_model_id INT64,\n service_request_id STRING(20),\n account_holder STRING(128),\n aid STRING(128),\n arc STRING(2),\n bank_authorization_code STRING(64),\n bank_name STRING(128),\n bank_transaction_number STRING(64),\n bin STRING(64),\n card_brand STRING(128),\n card_network STRING(2),\n card_type STRING(8),\n clearone_amount STRING(10),\n country STRING(5),\n currency STRING(3),\n cvm STRING(2),\n date STRING(10),\n issuing_bank_id STRING(64),\n lbl STRING(128),\n num_op_bco STRING(64),\n operation_type STRING(128),\n pinpad_read_mode STRING(64),\n reference_num_op STRING(64),\n response_code STRING(1),\n session_id STRING(64),\n tax_free STRING(2),\n text_message STRING(128),\n ticket STRING(10),\n center_eci STRING(64),\n is_manual BOOL,\n payment_name_eci STRING(64),\n terminal_eci STRING(64),\n transaction_eci STRING(64),\n date_eci STRING(10),\n company_eci STRING(64),\n datastream_metadata STRUCT,\n identificador_rts STRING(128),\n moneda STRING(128),\n marca_tarjeta STRING(128),\n firma STRING(256),\n estado STRING(128),\n literales STRING(128),\n id_app STRING(128),\n tipo_pago STRING(128),\n tarjeta_comercio_recibo STRING(128),\n operacion_emv BOOL,\n tarjeta_cliente_recibo STRING(128),\n cod_resp_auto STRING(128),\n factura STRING(128),\n res_verificacion STRING(128),\n codigo_respuesta STRING(128),\n caducidad STRING(128),\n fecha_operacion TIMESTAMP,\n comercio STRING(128),\n conttrans STRING(128),\n tipo_tasa_aplicada STRING(128),\n redsys_terminal STRING(128),\n resultado STRING(128),\n pedido STRING(128),\n sec_tarjeta STRING(128),\n importe STRING(128),\n oper_contact_less STRING(128),\n recibo_solo_cliente STRING(128),\n etiqueta_app STRING(128),\n autenticado_por_pin STRING(128),\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 1 + }, + "psql_dcpublic.tpv_payment_types": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.tpv_payment_types`\n(\n id INT64,\n name STRING(50),\n description STRING(100),\n closure_required BOOL,\n aurgi_enabled BOOL,\n mt_enabled BOOL,\n datastream_metadata STRUCT,\n returns_by_dc BOOL,\n sort_order INT64,\n is_pin_pad_category BOOL,\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 1 + }, + "customer_marts.customer_predictive": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_predictive`\nAS WITH nba AS (\n SELECT\n persona_id,\n accion AS next_best_action,\n motivo AS nba_motivo,\n prioridad_score AS nba_prioridad\n FROM (\n SELECT\n persona_id, accion, motivo, prioridad_score,\n ROW_NUMBER() OVER (\n PARTITION BY persona_id\n ORDER BY prioridad_score DESC, fecha_senal DESC\n ) AS rn\n FROM `autingo-159109.clientes_intel.reco_acciones`\n WHERE persona_id IS NOT NULL\n )\n WHERE rn = 1\n)\nSELECT\n scl.persona_id,\n scl.clv_12m_ingreso,\n scl.clv_12m_margen,\n scl.prob_activo,\n scl.pred_compras_12m,\n scl.clv_decil,\n scl.clv_score,\n seg.cluster,\n ROUND(1 - scl.prob_activo, 4) AS churn_risk,\n nba.next_best_action,\n nba.nba_motivo,\n nba.nba_prioridad\nFROM `autingo-159109.clientes_intel.score_clv` scl\nLEFT JOIN `autingo-159109.clientes_intel.seg_cliente_360` seg USING (persona_id)\nLEFT JOIN nba USING (persona_id);", + "refs": [ + "clientes_intel.reco_acciones", + "clientes_intel.score_clv", + "clientes_intel.seg_cliente_360" + ], + "depth": 0 + }, + "clientes_intel.reco_acciones": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.reco_acciones`\n(\n persona_id INT64,\n vehiculo_id STRING,\n license_plate STRING,\n accion STRING,\n motivo STRING,\n motivo_subcat STRING,\n check_color STRING,\n fecha_senal DATE,\n clv_norm FLOAT64,\n clv_score INT64,\n peso_accion INT64,\n prioridad_score FLOAT64,\n canal_entrada STRING,\n contactable_email BOOL,\n contactable_telefono BOOL,\n email_optout BOOL,\n sms_optout BOOL,\n do_not_call BOOL\n)\nCLUSTER BY accion, persona_id;", + "refs": [], + "depth": 1 + }, + "clientes_intel.score_clv": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.score_clv`\n(\n persona_id INT64,\n frequency FLOAT64,\n recency_weeks FLOAT64,\n T_weeks FLOAT64,\n monetary_value FLOAT64,\n pred_compras_12m FLOAT64,\n prob_activo FLOAT64,\n valor_esperado_transaccion FLOAT64,\n clv_12m_ingreso FLOAT64,\n margen_pct FLOAT64,\n clv_12m_margen FLOAT64,\n metodo STRING,\n clv_norm FLOAT64,\n clv_score INT64,\n clv_decil INT64\n);", + "refs": [], + "depth": 1 + }, + "customer_marts.customer_product": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_product`\nAS SELECT\n persona_id,\n producto_nav_id,\n ANY_VALUE(producto_desc) AS producto_desc,\n ANY_VALUE(categoria) AS categoria,\n ANY_VALUE(grupo) AS subcategoria,\n SUM(cantidad) AS cantidad,\n ROUND(SUM(importe), 2) AS venta_total,\n ROUND(SUM(importe - COALESCE(coste_linea_aprox, 0)), 2) AS margen_total,\n COUNT(DISTINCT id_factura_nav) AS n_tickets,\n COUNT(*) AS n_lineas,\n MIN(fecha) AS primera_compra,\n MAX(fecha) AS ultima_compra,\n ROUND(SAFE_DIVIDE(SUM(importe),\n SUM(SUM(importe)) OVER (PARTITION BY persona_id)), 4) AS share_venta\nFROM `autingo-159109.clientes_intel.fact_transaccion`\nWHERE persona_id IS NOT NULL AND producto_nav_id IS NOT NULL\nGROUP BY persona_id, producto_nav_id;", + "refs": [ + "clientes_intel.fact_transaccion" + ], + "depth": 0 + }, + "customer_marts.customer_profile": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_profile`\nAS SELECT\n _b.*,\n NOT COALESCE(_b.es_empresa, FALSE) AS es_particular,\n (_b.document_kind = 'DNI') AS es_dni,\n (NOT COALESCE(_b.es_empresa, FALSE) AND _b.document_kind = 'DNI') AS es_particular_dni\nFROM (\nSELECT\n dp.persona_id,\n -- Identidad\n dp.document_number,\n dp.document_kind,\n COALESCE(dp.full_name, fcp.full_name) AS full_name,\n COALESCE(dp.email, fcp.email) AS email,\n COALESCE(dp.phone, fcp.phone) AS phone,\n seg.provincia,\n seg.ccaa,\n dp.postal_code,\n dp.es_empresa,\n dp.salesforce_customer_id,\n -- RFM (fuente: feat_cliente_persona)\n fcp.r_score,\n fcp.f_score,\n fcp.m_score,\n fcp.rfm_score,\n fcp.rfm_segment,\n -- Monetario (fuente: feat_cliente_persona)\n fcp.monetary_total,\n fcp.ticket_medio,\n fcp.ticket_std,\n fcp.margen_total,\n fcp.margen_pct,\n fcp.valor_ponderado,\n -- Temporal (fuente: feat_cliente_persona)\n fcp.recency_days,\n fcp.frequency,\n fcp.tenure_days,\n fcp.primera_compra,\n fcp.ultima_compra,\n fcp.mean_inter_visit_days,\n fcp.std_inter_visit_days,\n fcp.mes_top_compra,\n -- Top-N / mix (fuente: feat_cliente_persona)\n fcp.categoria_top,\n fcp.pct_top_cat,\n fcp.centro_principal,\n fcp.empresa_principal,\n fcp.canal_preferido,\n fcp.n_categorias,\n fcp.n_centros,\n fcp.n_vehiculos,\n fcp.pct_aurgi,\n fcp.pct_motortown,\n fcp.pct_web,\n fcp.pct_servicio,\n -- Predictivo (fuente: score_clv para CLV; seg para cluster/tipo/nivel)\n scl.clv_12m_ingreso,\n scl.clv_12m_margen,\n scl.prob_activo,\n scl.pred_compras_12m,\n scl.clv_score,\n scl.clv_decil,\n scl.clv_norm,\n seg.cluster,\n seg.tipo_cliente,\n seg.clv_nivel,\n -- Mutualista\n seg.es_mutualista,\n -- Contacto\n fcp.contactable,\n fcp.tiene_email,\n fcp.tiene_telefono,\n dp.email_optout,\n dp.sms_optout,\n dp.whatsapp_optout,\n dp.do_not_call,\n dp.num_fuentes\nFROM `autingo-159109.clientes_intel.dim_persona` dp\nLEFT JOIN `autingo-159109.clientes_intel.feat_cliente_persona` fcp USING (persona_id)\nLEFT JOIN `autingo-159109.clientes_intel.score_clv` scl USING (persona_id)\nLEFT JOIN `autingo-159109.clientes_intel.seg_cliente_360` seg USING (persona_id)\n) AS _b;", + "refs": [ + "clientes_intel.dim_persona", + "clientes_intel.feat_cliente_persona", + "clientes_intel.score_clv", + "clientes_intel.seg_cliente_360" + ], + "depth": 0 + }, + "customer_marts.customer_promo_tolerance": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_promo_tolerance`\nAS WITH cust2pers AS (\n SELECT DISTINCT customer_id_tpv, persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE customer_id_tpv IS NOT NULL\n),\norder_promo AS (\n -- por order: total lineas, lineas promo, flag de promo en el ticket\n SELECT\n o.id AS order_id,\n o.customer_id,\n COUNT(*) AS n_lineas,\n COUNTIF(oi.is_promo) AS n_lineas_promo,\n MAX(CAST(oi.is_promo AS INT64)) AS ticket_tiene_promo\n FROM `autingo-159109.psql_dcpublic.tpv_orders_order` o\n JOIN `autingo-159109.psql_dcpublic.tpv_orders_orderitem` oi\n ON oi.order_id = o.id\n GROUP BY o.id, o.customer_id\n),\npersona_promo AS (\n SELECT\n c.persona_id,\n SUM(op.n_lineas) AS n_lineas_total,\n SUM(op.n_lineas_promo) AS n_lineas_promo,\n COUNT(*) AS n_tickets,\n SUM(op.ticket_tiene_promo) AS n_tickets_con_promo\n FROM order_promo op\n JOIN cust2pers c ON op.customer_id = c.customer_id_tpv\n GROUP BY c.persona_id\n),\ncampana AS (\n SELECT\n persona_id,\n COUNTIF(abrio OR clico) AS n_campanas_respondidas,\n AVG(tasa_apertura) AS tasa_apertura_media,\n AVG(tasa_click) AS tasa_click_media\n FROM `autingo-159109.clientes_intel.fact_campana_respuesta`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id\n)\nSELECT\n persona_id,\n pp.n_lineas_total,\n pp.n_lineas_promo,\n SAFE_DIVIDE(pp.n_lineas_promo, pp.n_lineas_total) AS promo_share,\n pp.n_tickets,\n pp.n_tickets_con_promo,\n SAFE_DIVIDE(pp.n_tickets_con_promo, pp.n_tickets) AS discount_dependence,\n cp.n_campanas_respondidas,\n cp.tasa_apertura_media,\n cp.tasa_click_media\nFROM persona_promo pp\nFULL OUTER JOIN campana cp USING (persona_id);", + "refs": [ + "clientes_intel.fact_campana_respuesta", + "clientes_intel.fact_transaccion", + "psql_dcpublic.tpv_orders_order", + "psql_dcpublic.tpv_orders_orderitem" + ], + "depth": 0 + }, + "clientes_intel.fact_campana_respuesta": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.fact_campana_respuesta`\n(\n persona_id INT64,\n canal STRING,\n campana STRING,\n n_enviados INT64,\n n_entregados INT64,\n n_rebotados INT64,\n n_baja INT64,\n n_abiertos INT64,\n n_clicados INT64,\n tasa_apertura FLOAT64,\n tasa_click FLOAT64,\n abrio BOOL,\n clico BOOL,\n baja BOOL,\n primera_fecha DATE,\n ultima_fecha DATE\n)\nCLUSTER BY canal, persona_id;", + "refs": [], + "depth": 1 + }, + "psql_dcpublic.tpv_orders_orderitem": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.psql_dcpublic.tpv_orders_orderitem`\n(\n id INT64,\n quantity INT64,\n order_id INT64,\n product_id INT64,\n created_by_id INT64,\n total_cost NUMERIC(10, 2),\n total_cost_currency STRING(3),\n all_included_price NUMERIC(10, 2),\n all_included_price_currency STRING(3),\n item_type STRING(50),\n price NUMERIC(10, 2),\n price_currency STRING(3),\n override_cost BOOL,\n parent_item_id INT64,\n unit_price NUMERIC(10, 2),\n unit_price_currency STRING(3),\n is_promo BOOL,\n name STRING(256),\n encargo_description STRING(100),\n encargo_price NUMERIC(10, 2),\n is_commission BOOL,\n reference STRING(100),\n return_count INT64,\n promo_id INT64,\n invoice_nav_line STRING(10),\n quote_nav_line STRING(10),\n order_promo BOOL,\n season STRING(256),\n has_request_service BOOL,\n carga_aa INT64,\n custom_description STRING(100),\n custom_product_id INT64,\n return_reason STRING(256),\n original_item_id INT64,\n allow_return_in_other_center BOOL,\n tax_applied STRING(20),\n tax_percentage NUMERIC(5, 2),\n is_gt BOOL,\n delivery_note_ref STRING(256),\n purchase_price NUMERIC(10, 2),\n purchase_price_currency STRING(3),\n datastream_metadata STRUCT,\n last_extra_item_line_number INT64,\n line_number INT64,\n encargo_provider_id INT64,\n generate_question BOOL,\n encargo_data JSON,\n reason STRING(50),\n search_reference STRING(100),\n plate STRING(128),\n part_id INT64,\n piece_data JSON,\n tpv_assistant_screen_question JSON,\n encargo_status STRING(50),\n need_email_for_voucher BOOL,\n adjusted_purchase_price NUMERIC(10, 2),\n anjana_maintenance_id INT64,\n adjusted_purchase_price_currency STRING(3),\n warehouse_location STRING(256),\n delivery_note_date TIMESTAMP,\n taxes JSON,\n price_set_by_user BOOL,\n source STRING(256),\n PRIMARY KEY (id) NOT ENFORCED\n)\nCLUSTER BY id\nOPTIONS(\n labels=[(\"dataplex-dp-published-project\", \"autingo-159109\"), (\"dataplex-dp-published-location\", \"europe-west1\"), (\"dataplex-dp-published-scan\", \"a615d6de4-2947-4545-8363-b4c7c141da07\")],\n max_staleness=INTERVAL '0-0 0 1:0:0' YEAR TO SECOND\n);", + "refs": [], + "depth": 1 + }, + "customer_marts.customer_promo_usage": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_promo_usage`\nAS WITH cust2pers AS (\n SELECT DISTINCT customer_id_tpv, persona_id\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE customer_id_tpv IS NOT NULL\n),\npromo_lines AS (\n SELECT\n c.persona_id,\n oi.order_id,\n oi.promo_id\n FROM `autingo-159109.psql_dcpublic.tpv_orders_orderitem` oi\n JOIN `autingo-159109.psql_dcpublic.tpv_orders_order` o\n ON oi.order_id = o.id\n JOIN cust2pers c\n ON o.customer_id = c.customer_id_tpv\n WHERE oi.is_promo = TRUE\n),\nagg AS (\n SELECT\n persona_id,\n COUNT(*) AS n_lineas_promo,\n COUNT(DISTINCT order_id) AS n_tickets_promo,\n COUNT(DISTINCT promo_id) AS n_promos_distintas\n FROM promo_lines\n GROUP BY persona_id\n)\nSELECT\n a.persona_id,\n a.n_lineas_promo,\n a.n_tickets_promo,\n a.n_promos_distintas,\n seg.es_mutualista\nFROM agg a\nLEFT JOIN `autingo-159109.clientes_intel.seg_cliente_360` seg\n USING (persona_id);", + "refs": [ + "clientes_intel.fact_transaccion", + "clientes_intel.seg_cliente_360", + "psql_dcpublic.tpv_orders_order", + "psql_dcpublic.tpv_orders_orderitem" + ], + "depth": 0 + }, + "customer_marts.customer_store_spend": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_store_spend`\nAS WITH agg AS (\n SELECT\n persona_id,\n centro_nav_id,\n ANY_VALUE(centro_nombre) AS centro_nombre,\n ANY_VALUE(empresa) AS empresa,\n ANY_VALUE(zona) AS zona,\n SUM(importe) AS gasto,\n SUM(importe - coste_linea_aprox) AS margen,\n COUNT(DISTINCT id_factura_nav) AS n_tickets,\n COUNT(*) AS n_lineas,\n MIN(fecha) AS primera_fecha,\n MAX(fecha) AS ultima_fecha\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id, centro_nav_id\n)\nSELECT\n persona_id,\n centro_nav_id,\n centro_nombre,\n empresa,\n zona,\n gasto,\n margen,\n n_tickets,\n n_lineas,\n primera_fecha,\n ultima_fecha,\n SAFE_DIVIDE(gasto, SUM(gasto) OVER (PARTITION BY persona_id)) AS share_gasto_centro\nFROM agg;", + "refs": [ + "clientes_intel.fact_transaccion" + ], + "depth": 0 + }, + "customer_marts.customer_temporal": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_temporal`\nAS WITH trend AS (\n SELECT\n persona_id,\n SUM(IF(fecha >= DATE_SUB(CURRENT_DATE(), INTERVAL 12 MONTH), importe, 0)) AS gasto_ult_12m,\n SUM(IF(fecha < DATE_SUB(CURRENT_DATE(), INTERVAL 12 MONTH)\n AND fecha >= DATE_SUB(CURRENT_DATE(), INTERVAL 24 MONTH), importe, 0)) AS gasto_12m_previos\n FROM `autingo-159109.clientes_intel.fact_transaccion`\n WHERE persona_id IS NOT NULL\n GROUP BY persona_id\n)\nSELECT\n fcp.persona_id,\n fcp.recency_days,\n fcp.frequency,\n fcp.mean_inter_visit_days,\n fcp.std_inter_visit_days,\n fcp.tenure_days,\n fcp.primera_compra,\n fcp.ultima_compra,\n fcp.mes_top_compra,\n t.gasto_ult_12m,\n t.gasto_12m_previos,\n SAFE_DIVIDE(t.gasto_ult_12m, t.gasto_12m_previos) AS ratio_tendencia\nFROM `autingo-159109.clientes_intel.feat_cliente_persona` fcp\nLEFT JOIN trend t USING (persona_id);", + "refs": [ + "clientes_intel.fact_transaccion", + "clientes_intel.feat_cliente_persona" + ], + "depth": 0 + }, + "customer_marts.customer_vehicles": { + "type": "VIEW", + "ddl": "CREATE VIEW `autingo-159109.customer_marts.customer_vehicles`\nAS SELECT\n mpv.persona_id,\n mpv.plate_norm,\n COALESCE(dv.license_plate, fcv.license_plate) AS license_plate,\n COALESCE(dv.make, fcv.make) AS make,\n COALESCE(dv.model, fcv.model) AS model,\n COALESCE(dv.model_family, fcv.model_family) AS model_family,\n COALESCE(dv.fuel, fcv.fuel) AS fuel,\n dv.version,\n dv.year,\n dv.km,\n dv.tyre_size,\n fcv.antiguedad_anios,\n fcv.anio_matricula,\n -- Actividad del vehiculo en relacion a esta persona\n mpv.n_visitas,\n mpv.n_eventos,\n mpv.primera_fecha,\n mpv.ultima_fecha,\n -- Metricas del vehiculo (feat)\n fcv.importe_total,\n fcv.recency_days,\n fcv.n_lineas,\n fcv.n_categorias,\n fcv.pct_servicio,\n fcv.dias_desde_neumatico,\n fcv.dias_desde_revision,\n fcv.dias_desde_bateria,\n fcv.dias_desde_aceite,\n fcv.dias_desde_frenos,\n fcv.dias_desde_preitv,\n fcv.itv_periodicidad,\n fcv.proxima_itv_anio,\n fcv.itv_due_este_anio,\n fcv.persona_actual,\n -- Vias de relacion persona-vehiculo\n mpv.via_visita,\n mpv.via_owner_tpv,\n mpv.via_otr,\n mpv.via_citaprevia\nFROM `autingo-159109.clientes_intel.map_persona_vehiculo` mpv\nLEFT JOIN `autingo-159109.clientes_intel.dim_vehiculo` dv\n ON mpv.plate_norm = dv.plate_norm\nLEFT JOIN `autingo-159109.clientes_intel.feat_cliente_vehiculo` fcv\n ON mpv.plate_norm = fcv.vehiculo_id;", + "refs": [ + "clientes_intel.dim_vehiculo", + "clientes_intel.feat_cliente_vehiculo", + "clientes_intel.map_persona_vehiculo" + ], + "depth": 0 + }, + "clientes_intel.dim_vehiculo": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.dim_vehiculo`\n(\n plate_norm STRING,\n license_plate STRING,\n make STRING,\n model STRING,\n model_family STRING,\n version STRING,\n fuel STRING,\n year INT64,\n car_frame STRING,\n km INT64,\n tyre_size STRING,\n in_tpv BOOL,\n in_otr BOOL,\n in_lineas BOOL,\n in_citaprevia BOOL,\n vin STRING,\n vin_source STRING,\n tec_marca STRING,\n tec_modelo STRING,\n tec_modelo_generico STRING,\n tec_combustible STRING,\n tec_cilindrada INT64,\n tec_potencia_kw FLOAT64,\n tec_potencia_fiscal FLOAT64,\n tec_co2 FLOAT64,\n tec_plazas INT64,\n tec_peso_maximo INT64,\n tec_tara INT64,\n tec_tipo STRING,\n tec_segmento STRING,\n tec_mercado STRING,\n tec_procedencia STRING,\n tec_provincia_matricula STRING,\n tec_fecha_1a_matricula DATE,\n tec_fecha_matricula DATE,\n ktypes ARRAY,\n car_versions ARRAY\n);", + "refs": [], + "depth": 1 + }, + "clientes_intel.feat_cliente_vehiculo": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.feat_cliente_vehiculo`\n(\n vehiculo_id STRING,\n license_plate STRING,\n make STRING,\n model STRING,\n model_family STRING,\n fuel STRING,\n tyre_size STRING,\n year_declarado INT64,\n anio_estimado_matricula INT64,\n anio_real_matricula INT64,\n anio_matricula INT64,\n fuente_anio STRING,\n antiguedad_anios INT64,\n n_visitas INT64,\n n_lineas INT64,\n importe_total FLOAT64,\n primera_fecha DATE,\n ultima_fecha DATE,\n recency_days INT64,\n n_categorias INT64,\n pct_servicio FLOAT64,\n n_lineas_neumatico INT64,\n dias_desde_neumatico INT64,\n dias_desde_revision INT64,\n dias_desde_bateria INT64,\n dias_desde_aceite INT64,\n dias_desde_frenos INT64,\n dias_desde_preitv INT64,\n n_personas INT64,\n persona_actual INT64,\n vin STRING,\n tec_combustible STRING,\n tec_modelo_generico STRING,\n tec_cilindrada INT64,\n tec_potencia_kw FLOAT64,\n tec_potencia_fiscal FLOAT64,\n tec_co2 FLOAT64,\n tec_segmento STRING,\n tec_tipo STRING,\n tec_plazas INT64,\n tec_fecha_matricula DATE,\n tec_fecha_1a_matricula DATE,\n combustible STRING,\n itv_periodicidad STRING,\n proxima_itv_anio INT64,\n itv_due_este_anio BOOL\n)\nCLUSTER BY make, vehiculo_id;", + "refs": [], + "depth": 1 + }, + "clientes_intel.map_persona_vehiculo": { + "type": "BASE TABLE", + "ddl": "CREATE TABLE `autingo-159109.clientes_intel.map_persona_vehiculo`\n(\n persona_id INT64,\n plate_norm STRING,\n primera_fecha DATE,\n ultima_fecha DATE,\n n_visitas INT64,\n n_eventos INT64,\n via_visita BOOL,\n via_owner_tpv BOOL,\n via_otr BOOL,\n via_citaprevia BOOL\n);", + "refs": [], + "depth": 1 + } + } +} \ No newline at end of file diff --git a/scratchpad/trace_intel.py b/scratchpad/trace_intel.py new file mode 100644 index 00000000..0eba0325 --- /dev/null +++ b/scratchpad/trace_intel.py @@ -0,0 +1,106 @@ +"""Traza la construccion de clientes_intel: para cada tabla, recupera el SQL del ultimo +job que la escribio (INFORMATION_SCHEMA.JOBS) + sus referenced_tables, y recorre hacia +atras hasta las tablas fuente (TPV, customers, users, Navision, Salesforce). + +Vuelca todo a scratchpad/intel_build.json. +""" +import json +import warnings + +warnings.filterwarnings("ignore") +import google.auth +from google.cloud import bigquery + +PROJECT = "autingo-159109" +REGION = "region-europe-west1" + +creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/bigquery"]) +creds = creds.with_quota_project(None) +c = bigquery.Client(project=PROJECT, credentials=creds) + +# Ultimo job por tabla destino en clientes_intel: query + referenced_tables + stmt. +sql = f""" +WITH j AS ( + SELECT + dest.table_id AS tbl, + query, + statement_type AS stmt, + creation_time, + ARRAY( + SELECT AS STRUCT rt.project_id, rt.dataset_id, rt.table_id + FROM UNNEST(referenced_tables) rt + ) AS refs, + ROW_NUMBER() OVER (PARTITION BY dest.table_id ORDER BY creation_time DESC) AS rn + FROM `{PROJECT}`.`{REGION}`.INFORMATION_SCHEMA.JOBS_BY_PROJECT, + UNNEST([destination_table]) dest + WHERE dest.dataset_id = 'clientes_intel' + AND state = 'DONE' AND error_result IS NULL + AND statement_type IS NOT NULL + AND creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 120 DAY) +) +SELECT tbl, query, stmt, creation_time, refs FROM j WHERE rn = 1 +ORDER BY tbl +""" + +builds = {} +for r in c.query(sql).result(): + refs = [] + for rt in r.refs: + refs.append(f"{rt['dataset_id']}.{rt['table_id']}") + builds[r.tbl] = { + "query": r.query or "", + "stmt": r.stmt, + "last_run": str(r.creation_time), + "refs": sorted(set(x for x in refs if not x.endswith(f".{r.tbl}"))), + } + +json.dump(builds, open("scratchpad/intel_build.json", "w"), indent=2, ensure_ascii=False) +print(f"tablas clientes_intel con SQL de construccion capturado: {len(builds)}\n") + +# Recursion desde las 12 tablas usadas por customer_marts. +SEED = [ + "dim_persona", "dim_vehiculo", "fact_transaccion", "fact_campana_respuesta", + "feat_cliente_persona", "feat_cliente_vehiculo", "seg_cliente_360", "score_clv", + "reco_acciones", "map_persona_canal8", "map_persona_fuente", "map_persona_vehiculo", +] +intel_involved = set() +external_sources = set() +stack = list(SEED) +while stack: + t = stack.pop() + if t in intel_involved: + continue + intel_involved.add(t) + b = builds.get(t) + if not b: + continue + for ref in b["refs"]: + ds, tbl = ref.split(".", 1) + if ds == "clientes_intel": + if tbl not in intel_involved: + stack.append(tbl) + else: + external_sources.add(ref) + +print("== tablas clientes_intel implicadas en el linaje de customer_marts ==") +for t in sorted(intel_involved): + b = builds.get(t, {}) + print(f" {t:26s} {b.get('stmt','(sin job)')}") + +print("\n== FUENTES EXTERNAS (fuera de clientes_intel) usadas por el pipeline ==") +for s in sorted(external_sources): + print(f" {s}") + +# Marcar las fuentes de CLIENTE que pide el usuario. +KEYS = ["customer", "customers", "cliente", "user", "usuario", "tpv", "salesforce", + "sf_", "contact", "mkt_cloud", "persona"] +print("\n== fuentes que parecen de CLIENTE/usuario ==") +for s in sorted(external_sources): + low = s.lower() + if any(k in low for k in KEYS): + print(f" {s}") + +json.dump({ + "intel_involved": sorted(intel_involved), + "external_sources": sorted(external_sources), +}, open("scratchpad/intel_lineage.json", "w"), indent=2, ensure_ascii=False) diff --git a/scratchpad/trace_lineage.py b/scratchpad/trace_lineage.py new file mode 100644 index 00000000..7220c740 --- /dev/null +++ b/scratchpad/trace_lineage.py @@ -0,0 +1,158 @@ +"""Traza el linaje recursivo de las vistas de customer_marts hasta las tablas fuente. + +Para cada objeto: obtiene su tipo (VIEW/BASE TABLE/EXTERNAL/MATERIALIZED VIEW) y su DDL +via INFORMATION_SCHEMA.TABLES, extrae las referencias a otras tablas del DDL y recurre +sobre las que son vistas. Vuelca el grafo completo a un JSON en scratchpad. +""" +import json +import re +import sys +import warnings + +warnings.filterwarnings("ignore") + +import google.auth +from google.cloud import bigquery + +PROJECT = "autingo-159109" + +creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/bigquery"]) +creds = creds.with_quota_project(None) +client = bigquery.Client(project=PROJECT, credentials=creds) + +# Cache de metadata por dataset: {dataset: {table_name: {"type":..., "ddl":...}}} +dataset_cache: dict[str, dict] = {} + + +def load_dataset(dataset: str) -> dict: + """Carga todas las tablas/vistas de un dataset (una query por dataset).""" + if dataset in dataset_cache: + return dataset_cache[dataset] + result: dict[str, dict] = {} + try: + sql = f""" + SELECT table_name, table_type, ddl + FROM `{PROJECT}`.`{dataset}`.INFORMATION_SCHEMA.TABLES + """ + for r in client.query(sql).result(): + result[r.table_name] = {"type": r.table_type, "ddl": r.ddl or ""} + except Exception as e: # noqa: BLE001 + print(f" [warn] no se pudo leer dataset {dataset}: {e}", file=sys.stderr) + dataset_cache[dataset] = result + return result + + +# En el DDL que emite INFORMATION_SCHEMA, las referencias a otras tablas SIEMPRE van +# entre backticks y totalmente cualificadas: `proyecto.dataset.tabla`. Los alias de +# CTE/JOIN (dp, fcp, f...) nunca llevan backticks, asi que restringiendo a lo que hay +# entre backticks eliminamos todo el ruido. +BACKTICK_RE = re.compile(r"`([^`]+)`") +# Variante con cada parte en su propio backtick: `proj`.`dataset`.`tabla` +MULTIPART_RE = re.compile( + r"`([A-Za-z0-9_-]+)`\.`([A-Za-z0-9_-]+)`(?:\.`([A-Za-z0-9_-]+)`)?" +) + + +def _norm(proj: str, ds: str, tbl: str) -> tuple[str, str] | None: + if ds.upper() == "INFORMATION_SCHEMA" or tbl.upper() == "INFORMATION_SCHEMA": + return None + return (ds, tbl) + + +def extract_refs(ddl: str) -> set[tuple[str, str]]: + """Devuelve el conjunto de (dataset, table) referenciados en el cuerpo del DDL. + + Se queda con el SELECT (tras el primer 'AS') para no capturar el nombre del propio objeto. + """ + body = ddl + m = re.search(r"\bAS\b", ddl, flags=re.IGNORECASE) + if m: + body = ddl[m.end():] + + refs: set[tuple[str, str]] = set() + + # Estilo `proyecto.dataset.tabla` (todo en un backtick). + for tok in BACKTICK_RE.findall(body): + parts = [p for p in tok.split(".") if p] + if len(parts) == 3: + r = _norm(parts[0], parts[1], parts[2]) + elif len(parts) == 2: + r = _norm(PROJECT, parts[0], parts[1]) + else: + r = None + if r: + refs.add(r) + + # Estilo `proj`.`dataset`.`tabla` (parte por backtick, 3 partes cualificadas). + # OJO: `alias`.`columna` (2 partes con cada parte en su propio backtick) es una + # referencia a columna, NO a tabla — se descarta exigiendo las 3 partes. + for mt in MULTIPART_RE.finditer(body): + g1, g2, g3 = mt.group(1), mt.group(2), mt.group(3) + if g3: + r = _norm(g1, g2, g3) + if r: + refs.add(r) + + return refs + + +graph: dict[str, dict] = {} # key "dataset.table" -> {type, ddl, refs:[...]} +visited: set[str] = set() + + +def visit(dataset: str, table: str, depth: int = 0): + key = f"{dataset}.{table}" + if key in visited: + return + visited.add(key) + meta = load_dataset(dataset).get(table) + if meta is None: + graph[key] = {"type": "UNKNOWN", "ddl": "", "refs": [], "depth": depth} + return + ddl = meta["ddl"] + ttype = meta["type"] + refs: list[str] = [] + if ttype in ("VIEW", "MATERIALIZED VIEW"): + for ds, tbl in sorted(extract_refs(ddl)): + # Evitar auto-referencia + if ds == dataset and tbl == table: + continue + refs.append(f"{ds}.{tbl}") + graph[key] = {"type": ttype, "ddl": ddl, "refs": refs, "depth": depth} + for ref in refs: + rds, rtbl = ref.split(".", 1) + visit(rds, rtbl, depth + 1) + + +# Semillas: las 14 vistas de customer_marts. +SEEDS = [ + "customer_brand_affinity", "customer_category_spend", "customer_channel", + "customer_contactability", "customer_monetary", "customer_payment_method", + "customer_predictive", "customer_product", "customer_profile", + "customer_promo_tolerance", "customer_promo_usage", "customer_store_spend", + "customer_temporal", "customer_vehicles", +] +for s in SEEDS: + visit("customer_marts", s, 0) + +out = { + "project": PROJECT, + "seeds": [f"customer_marts.{s}" for s in SEEDS], + "graph": graph, +} +with open("scratchpad/lineage_graph.json", "w") as f: + json.dump(out, f, indent=2, ensure_ascii=False) + +# Resumen +n_view = sum(1 for v in graph.values() if v["type"] in ("VIEW", "MATERIALIZED VIEW")) +n_base = sum(1 for v in graph.values() if v["type"] == "BASE TABLE") +n_ext = sum(1 for v in graph.values() if v["type"] == "EXTERNAL") +n_unk = sum(1 for v in graph.values() if v["type"] == "UNKNOWN") +print(f"objetos totales: {len(graph)} vistas: {n_view} base: {n_base} external: {n_ext} desconocidos: {n_unk}") +print("\n== objetos por dataset ==") +by_ds: dict[str, int] = {} +for k in graph: + ds = k.split(".", 1)[0] + by_ds[ds] = by_ds.get(ds, 0) + 1 +for ds, n in sorted(by_ds.items(), key=lambda x: -x[1]): + print(f" {n:3d} {ds}")