Skip to main content

generate-invoices-orders-co

1. Descripción general

El pom.xml describe el proyecto como "Generate invoices on orders from Mexico", pero esta descripción es incorrecta: tanto el artifactId (generate-invoices-orders-co), el CLAUDE.md del repositorio y, sobre todo, el código (moneda COP, IVA del 19%, lógica de NIT/CC colombiana, códigos municipales DANE, vendedor PLAY HAWKERS COLOMBIA SAS, zona horaria America/Bogota en el CronJob) confirman que el proyecto genera facturas para pedidos de Colombia, no de México. Se documenta aquí el comportamiento real verificado en el código, y se señala la descripción incorrecta del pom.xml como hallazgo (sección 13).

Es un microservicio batch (runner) que toma hasta 10 pedidos pendientes de facturar (is_generate_invoice = false), calcula sus totales fiscales (IVA, descuentos y envío prorrateados), genera un Excel con el formato exigido por el proveedor de facturación electrónica Fymtech y lo sube por SFTP.

2. Información técnica

CampoValor
artifactIdgenerate-invoices-orders-co
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI)
MódulosNo aplica (proyecto de módulo único)

3. Arquitectura y diseño

No es una API REST: es una aplicación Spring Boot CLI con un único CommandLineRunner (GenerateInvoicesOrdersCoRunner) que ejecuta todo el flujo y cierra la JVM al finalizar.

Paquetes principales:

  • com.hawkersco.generateinvoicesordersco — clase principal y el runner.
  • .configLogisticsDbConfig (datasource primario logistics), DynamicsDbConfig (segundo datasource dynamics-pro), FtpProperties (record @ConfigurationProperties, prefijo fymtech.ftp), OrderFraudulentProcessConfig (declaración manual de servicios de logistics-commons/dynamics-commons; el nombre de esta clase no guarda relación con su contenido, ver sección 13).
  • .utilGenerateInvoicesOrdersCoUtil, con la resolución de códigos DANE.
flowchart TD
A[GenerateInvoicesOrdersCoRunner] -->|findAll barcodes| B[(dynamics-pro<br/>HWKInventItemBarcodesService)]
A -->|findOrdersToGenerateInvoiceFalse max 10| C[(logistics · Order)]
A -->|findFirst1 contador| D[(logistics · OrderInvoice)]
A -->|parseo XML SFCC crudo| E[SfccUtils]
A -->|resolución ciudad/depto| F[dane-code-alt.json]
A -->|genera XLSX| G[invoice/*.xlsx]
G -->|SFTP| H[Servidor Fymtech]
H -->|éxito| I[updateIsGenerateInvoice + nuevo contador]

Dos datasources PostgreSQL independientes, ambos con EntityManagerFactory/TransactionManager propios: LogisticsDbConfig (@Primary, prefijo spring.datasource, paquete com.hawkersco.logisticscommons.dao) y DynamicsDbConfig (prefijo dynamics.datasource, paquete com.hawkersco.dynamicscommons.dao).

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
org.apache.poi:poi-ooxml:5.4.0Generación del fichero Excel de factura
com.googlecode.json-simple:json-simple:1.1.1Parseo de dane-code-alt.json
org.apache.commons:commons-lang3:3.17.0StringUtils.stripAccents para normalizar nombres de ciudad
com.hawkersco:logistics-commons:1.0.25-SNAPSHOTEntidades/servicios de la BD logistics (Order, OrderInvoice, OrderLine)
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOTHWKInventItemBarcodesService (mapeo SKU→EAN desde Dynamics)
com.hawkersco:sfcc-commons:1.0.25-SNAPSHOTSfccUtils, parseo del XML de pedido de Salesforce Commerce Cloud almacenado en Order.rawData
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTDateUtils, DirectoryUtils, SftpUtils
spring-boot-starter-test (test)JUnit 5 + Spring Test

5. API / Endpoints

No aplica a este proyecto. Es un batch/runner sin capa REST.

6. Integraciones externas

SistemaProtocoloDirecciónDetalle
PostgreSQL (logistics)JDBCEntrante/SalienteLectura de pedidos pendientes de facturar y actualización del flag/contador de factura
PostgreSQL (dynamics-pro)JDBCEntranteLectura del mapeo SKU→EAN (HWKInventItemBarcodesService)
Servidor SFTP Fymtech (sftp.hawkersco.com)SFTP (SftpUtils)SalienteSubida del Excel de factura generado

7. Configuración

En producción (application-pro.properties) las credenciales llegan por variables de entorno inyectadas como Secret de Kubernetes; en local (application.properties) el repositorio contiene actualmente valores reales hardcodeados (ver alerta de seguridad).

ClaveDescripciónEjemplo (producción)
spring.datasource.jdbc-urlURL JDBC de la BD logistics${dbLogisitcsUrl}
spring.datasource.username / .passwordCredenciales de BD logistics${dbLogisitcsUsername} / ${dbLogisitcsPassword}
dynamics.datasource.jdbc-urlURL JDBC de la BD dynamics-pro${dbDynamicsProUrl}
dynamics.datasource.username / .passwordCredenciales de BD dynamics-pro${dbDynamicsProUsername} / ${dbDynamicsProPassword}
fymtech.ftp.server / .port / .user / .passConexión SFTP al servidor Fymtech${ftpServer} / ${ftpPort} / ${ftpUser} / ${ftpPass}
fymtech.ftp.dirDirectorio remoto de subida/src/pending/

⚠️ Alerta de seguridad

El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: contraseñas de ambas bases de datos PostgreSQL (logistics y dynamics-pro, ambas con el mismo usuario admin y la misma contraseña) y contraseña del servidor SFTP de Fymtech. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:

  1. Rotar las contraseñas de ambas bases de datos y la contraseña SFTP.
  2. Sustituir los valores hardcodeados de application.properties por credenciales de un entorno de desarrollo aislado.
  3. Revisar el historial de control de versiones, ya que estas credenciales pueden seguir expuestas en commits anteriores.

8. Persistencia

Dos bases de datos PostgreSQL independientes: logistics (entidades Order, OrderLine, OrderInvoice, vía logistics-commons) y dynamics-pro (entidad HWKInventItemBarcodes, vía dynamics-commons). Ambas con spring.jpa.hibernate.ddl-auto=none. OrderInvoice actúa como contador incremental de numeración de factura: el runner borra el registro de contador existente y guarda uno nuevo con el valor final tras cada ejecución exitosa. No hay Flyway/Liquibase en este repositorio.

9. Procesos programados y mensajería

No hay @Scheduled ni listeners de colas: la periodicidad la impone el CronJob de Kubernetes (k8s/cronjob.yaml), que ejecuta el contenedor una vez al día a medianoche (schedule: "0 0 * * *", zona horaria America/Bogota, concurrencyPolicy: Forbid). Flujo único de GenerateInvoicesOrdersCoRunner:

  1. Construye el mapa SKU→EAN desde HWKInventItemBarcodesService.findAll().
  2. Obtiene hasta 10 pedidos con is_generate_invoice = false (orderService.findOrdersToGenerateInvoiceFalse(10)) y el contador actual de factura (orderInvoiceService.findFirst1()).
  3. Por cada pedido: parsea el XML crudo de SFCC (Order.rawData) con SfccUtils, calcula descuento y envío totales del pedido y los prorratea entre las líneas de producto según su peso de precio neto, calcula el IVA (19% fijo) y determina si el cliente es persona (CC) o empresa (NIT, 10 dígitos con prefijo 860/830/900-909) a partir del atributo personalizado SFCC HW_customer_identification; si no hay identificación, usa el cliente por defecto 222222222 / "Cuantías Menores".
  4. Resuelve el código municipal DANE de la ciudad de envío contra dane-code-alt.json (normalizando tildes salvo la ñ).
  5. Escribe una fila por línea de producto en un Excel de 57 columnas (formato específico exigido por Fymtech).
  6. Sube el fichero por SFTP; si la subida tiene éxito, marca los pedidos como facturados (updateIsGenerateInvoice(true, ...)) y actualiza el contador de factura (OrderInvoice).

10. Ejecución en local

Requisitos previos: JDK 25, Maven, acceso a las BD logistics y dynamics-pro, y credenciales SFTP válidas de Fymtech en un application.properties local.

# Compilar sin tests (igual que en CI)
./mvnw -B -DskipTests clean install

# Compilar con tests
./mvnw clean install

# Ejecutar tests
./mvnw test

# Ejecutar un test concreto
./mvnw test -Dtest=GenerateInvoicesOrdersCoApplicationTests

# Ejecutar la aplicación localmente
./mvnw spring-boot:run

Al ser un CommandLineRunner, no expone Actuator/health: la forma de verificar la ejecución es revisar el log de consola, el contenido de invoice/*.xlsx generado localmente, o el estado is_generate_invoice de los pedidos tras la ejecución.

11. Despliegue

  • Imagen: construida con jib-maven-plugin (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/generate-invoices-orders-co:<tag>.
  • Orquestación: Kubernetes CronJob (k8s/cronjob.yaml) en el clúster GKE pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), namespace pi, ejecutándose diariamente a medianoche hora de Bogotá.
  • CI/CD (Jenkins): pipeline con 3 etapas — CheckoutBuild & Push (sustituye application-pro.properties por application.properties antes de mvn clean package jib:build) → Deploy to GKE (borra el CronJob existente con --ignore-not-found y aplica el manifiesto templado vía sed). El CLAUDE.md menciona una imagen base eclipse-temurin:25-jdk-alpine y etapas Build → Test (vacío) → Push → Deployment → Clean, que no coinciden exactamente con el Jenkinsfile/pom.xml actuales (ver sección 13).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (generate-invoices-orders-co).

Job de Jenkins: https://jenkins-pi.hawkersco.net/job/generate-invoices-orders-co/

12. Manejo de errores y logging

No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). generateAndUploadInvoice envuelve toda la generación y subida del Excel en un único try/catch que registra el mensaje de error con Logger.severe y no relanza la excepción — el runner termina "con éxito" desde el punto de vista del proceso (sin marcar los pedidos como facturados) aunque haya fallado a mitad de camino. No hay notificación a Slack ni otro canal de alerta. Logging mediante java.util.logging.Logger estándar (consola).

13. Notas y consideraciones

  • Descripción del pom.xml incorrecta: indica "Generate invoices on orders from Mexico", pero el proyecto genera facturas para Colombia (moneda COP, IVA 19%, NIT/CC, códigos DANE, vendedor PLAY HAWKERS COLOMBIA SAS, zona horaria America/Bogota). Es probable que la descripción se copiara de un proyecto homólogo para México sin actualizarla. Se recomienda corregirla en el pom.xml.
  • Bug real de NullPointerException en extractCustomerInfo: la condición if (attr.getContent().getFirst() != null || !attr.getContent().getFirst().toString().isEmpty()) usa || en lugar de &&. Si attr.getContent().getFirst() es null, el primer operando es false, por lo que Java evalúa el segundo operando (!attr.getContent().getFirst().toString().isEmpty()), que vuelve a invocar .getFirst() (de nuevo null) y llama .toString() sobre él, lanzando una NullPointerException. Cualquier pedido SFCC cuyo atributo HW_customer_identification tenga un primer valor null haría fallar el procesamiento de ese pedido y, dado que el try/catch envolvente no distingue el pedido que falló, aborta la generación de toda la factura del lote (los demás pedidos del lote tampoco se marcan como facturados en esa ejecución). Se recomienda corregir el operador a && y añadir una comprobación de nulidad explícita antes de invocar .toString().
  • Fallo parcial silencioso: como el único try/catch de generateAndUploadInvoice cubre generación de Excel, subida SFTP y actualización de estado, un error en cualquier punto (incluida la NPE anterior) hace que la ejecución completa termine sin marcar ningún pedido como facturado, sin notificar el motivo por ningún canal salvo el log local del pod (que se recicla en cada ejecución del CronJob). No hay reintento parcial ni aislamiento por pedido.
  • Nombre de clase de configuración no relacionado con su contenido: OrderFraudulentProcessConfig solo declara beans de OrderService, OrderLineService, OrderInvoiceService y HWKInventItemBarcodesService — nada relacionado con "proceso fraudulento". El nombre es probablemente un vestigio de copiar la clase desde otro proyecto (posiblemente uno de detección de fraude) sin renombrarla.
  • CLAUDE.md con detalles de despliegue desactualizados: menciona imagen base eclipse-temurin:25-jdk-alpine y manifiesto en jenkins/deployment/app-deployment.yml, mientras que el proyecto real usa jib-maven-plugin (imagen eclipse-temurin:25-jre) y k8s/cronjob.yaml. El resto de la descripción arquitectónica (flujo, doble datasource, reglas fiscales colombianas) coincide con el código actual y es más precisa que la propia descripción del pom.xml.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.