From 9eef9410538ca3106d3617872c5e4e82c5a3b2a9 Mon Sep 17 00:00:00 2001 From: JohannVelazquez Date: Thu, 3 Sep 2026 07:33:25 -0600 Subject: [PATCH 1/5] cambios --- Plan-actividades-avance-2026-07-16.xlsx | Bin 0 -> 11216 bytes .../Correo-respuesta-Erika-2026-07-29.md | 42 ++++++++++++++++++ propuesta/Correo-propuesta-v1.3.md | 29 ++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 Plan-actividades-avance-2026-07-16.xlsx create mode 100644 planeacion/Correo-respuesta-Erika-2026-07-29.md create mode 100644 propuesta/Correo-propuesta-v1.3.md diff --git a/Plan-actividades-avance-2026-07-16.xlsx b/Plan-actividades-avance-2026-07-16.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..764d86dcad1a9df0e7c72905a81a710467629cbc GIT binary patch literal 11216 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⋙)wFwUN!n)4Gnmb2l>zx%dfYx#PjO^^u#n-e`C%@qb@I+|u zWk*`h{g`;w*@<#L_CWR3{6_|=Vzn%Z)KOE_eIg{{Vb`bm$!t_^RSx7J^`UInxvdQaaKCci-T54pUisju*$t2W* zH&i1A%CnnN1g-ipF2c(N8&4OzW#j=tZ4&Rd74-Er9U0*73CJ8-!CQ|Lt`RMM$Ku12 zR9FcY6?mHLI?z(J@v_`e2Hjafr_+AH1wDFUL=$q%X_!QYO93zKvDXw7mpA+lIiQqUAoD@k7~v|q&}%5h;U?jyK{XV6S4A` zu~9e4clx4~Bi7@?bd{Q_dCVAXG!-liYo+pb{mbE-=ng23h7X5cty#`*WhMZwgds)R zDV$^aF71oi(=4E)pVEt23hnA1=XCcDD4cvt$6oDvgXSNJL zsX{$e-=dW7`jMv|Q!v5soE5eav$PD7q!PH^u7a4j4)8vq>(eByxWnOeV<CZ5CwbDE{tI|442>KNEN?ytb7?vaYd(K7)~82gn9r0ZXitVIknn)+Lrb9`=te-41j^f0hrYtQ!H$s`DN z8I9{@pj1IQ+ZxlMfCu!BY}op(mPPvCi>#7m3RE7)dX#*g$?&=zM3Xf~(_ZR z;M9-t%jb^qCYl*dpnfBltMKx*O#L}rw-Ftkj%&g7PMPSd9kh+rH?N)JG_9|iietNF z+B|vb>U$-;8bkq2CB!V_6-Y*iA**l|%fVPiBj|NV*0j@JjUJb?te>yLTjc2Zsl(&y zSEEGAh#7zuR>U&o6#D|FxD?4vJUk1H>t6T_$qRES)fs-LPHo&|&U?nwzW&r?dfLj$ zDY^ODlND7zU~o6v@KDVv!mI4ijZ<6Bds!k{b5)T@Zi?yakbXAvWjt3`^Z>&f9zuGQ z$p*tUmvrfQxkt;XVRfw|TxnkH$wvLukmu#z!7%;!K}Gn?%hVNH=`iFfC!yu#W%qqq zjZjQY;Z7IJEnZ62wyWQqBJ|;HNr(tmva668BtIQo&h6xMIm|jPJ)O*6jXGXwk;2f# z*eEDnobI?mA7{gv#=!g)3&Znr*f~k4^A-ILyIit6;md^P)E<%{L51`p4At^BaI5L)0j1V)f5-&_GB47@kz^YR{bD142x z`*Uel9B}vY8$ydvHEYVsyME&W?N|4O`8oS<9+Xsgm=UT3<|lk-7b6>T&h4K@Wu4Gd zMdV&UmvNEt3lt1jpvAohRRgTYAErypuxf!ryyS8Oat7oGll%+PBubx?oOB~kZhDp& zn?A7#t`M0FPV?=1PS_pkdFCV@@ZJxPKBTl%nj6Arp6&xfSNHb{| z78O444s^ZZH9U_?Pq?vvfigVrPBME(8@$~(&SSv$0!euMdu-bxDGnzLDYX$1003xz zH%lI#zK&KNe+0Bgx<+w}{J4Q8b}y3KU?mYsCQ1n{LTar8ZRW9x`YL+2t}+YR*z}0X zn@;eeyqwHsa;|5J5vv@g(9QLE^T5xJ?9M88>lG|L^=_JNqvkj_N##=WU4E-t8UeN{DRKU`p|H`Kki2C< zD=H*q#T6!-*wdEWM2lfHC$kg=$>?p}palXi>I-P*mfYyF(3GrnTS$PVmHbO>Gej@^-$4;5s$N~TzLNmk0pMvnUvmVjZK^E?zr zPrrh}N>2e?`JV_{56x_i)8-wNj^BOAM^Ltt<9bJT3*!8tA#GHX)_b8QbwRkQKX!SX z3jllQ?l+KZ%td`|#HJ7yC0xzLjV{uUSW3n+q6xchecOZ{lTOabT13T|%|YBdz?0`O zZaW*d;U846VSinq)(d^>PGgBY)nc?Ia;B@px*KhY>`A-tRs537xb97p6!PI)S~0E* z+<+4M#5dVa1}R^}Us&owh=WeTwuW`l*FQ~XLb)wakIc>p`C)_rXZYYQ@@ra(FBV*S zIc!wXE5GEQ)5-2;HmZkNIwrOZ1C?lCAHJX)AJfQH`BpTSp?vP`Xjt@GJG!E6)lMcu z@Ig*S0FY>k)Cy{2!KN=nwa>>< zz=wS7S1^GbN#Bww<`4aFpz|@xKs~*in@j74yZFsj{>v1JDB5Pn&OOw_i(n50oba*s zifMSj>K+NTg-6@b{^N3)D8l8|NMeh$?_vd5NA6b)!^PUhGhW%d>@7u|H%IcvH?Ri# z>&N@WPI9tYKld8;Zi*Vif@*8ogV$2)g$Xw?S-;RmCoY6@$(OILhgjT{*FW~-2)wUl z4}D1WL9TQJgJTWFz}0C_6Rf(4&f+0+Bbz5Xt^`4r_Nz)CCRCNd@d&oKp_Utr3C--nV+k z-N7`b?JnX?bGryL6D_|Twys6H&8TFPXqP}+GlT^kiQJS@v`|1#G;Do3T-d@?Ig;l` zKH@)nJala{L~xtzCC~I(+84~$SsFe|a<>bZ=yFYJr9WNn zj%DzXwm;>oR#P1oOcAtS`Aou0_gJd$5wx2G1%VGL5XSSzRf&KE?FjDejax3AL;`yt zu)X(`fp4dX3O0kbW{wcA!*p9M_cyVLcTU+&4RoW+k%I)Uh|uASz=fp6|Ux==6i=)bwqLO#%l>aUG<|>)?BuZ;}w=W!|&=Kz^fr#NQ+pE=_vA zjas|1N1q^ZLT(mMA9|X&Vb^9ygA~!`^`q8n$1R5AL!)OH5<_9=4Wsuq^c)phJw6Ws z>OEL}Ib-J{m}ycBc~?+0!lBMT8m36g_N3s z*W_9t?RD{h$?E(NTM($hyft@}xBmVRg)j)rSdUA1#uzp*Lrup;i&h4~oGngM?FBg%NPBGgf=b!XqHv>aCt@L- zqTr>*uQyEX>ZK}m&_ks)eHtUR;Jw%`#a0OPIqliB zuj+!6R@nM)ipUJCl*!1rYrZJ#m%1Y{D(vhqM+7fQb_PAubJ5@3^cv-xG)GY#GcIYK z6P89{HyEuYY~!Ug35CRQeY>Wibcorp|Hf;2Ut~lyvp%s*w;h(;y&aP5XB}OPbOqPA z@vZ25D|GOONYB%V2v0a~=XhJ+nWoXUM^TqnLW|D>(wbBom1eDag|3 z6wQBMxU5Js4vdZxK&hl-Hq&{f4c$QL!`|#}m}iq?rU@S!3x2WWR8M9Xu^Hf!K7oY7 z^zY-T%F9~V<`eK^ZqmjR?Fgj=a2~loks&Fzm5*#uH_F{2^F2eb|L+cnDh;}f2BgMf z2XRlZAoUna7YkK)7grB<3m12*e{}o&uM7jB%;*FY#a<5F?`v?Kl0*I(+`T}FCD1T= zA87qC)P<^fDbk5JakTQ`s_0l-=&E~Q+2bV6gH3+-p%$^DYrajLDUv1xS~V}Bd9mP^ zZv$eLnkku10zq;JE|2MyaBG`fm~|fKne8jI{vA^o74;OPth}!tpUoV<<10ARoT?fd z>J)8{i;lrNu!-ElbI@5vD!LlF54fSAUW-Y|7U06DpDIm36?CbJAr0%1%D#R+7vv~> z^2pVY8Rztlxyg!k8*pUub8|g)5+OWni9L`f=-rDuU%J>REhpYBYflCw(W!Ho1-A}c zI=x#tih_-cIEF%TUYTC=%L;9?t9`H^Nk%r#S${u4MvDEowAE}BLypdQ)~#!@RnVsF ze5#F-q$W{Jl^*_DZz>H~NmzL56jt}dYb(kaC;lv%`4!R3v;UW;lrU5+8~AQ-u;V{g z29GeUK1D*Vz(DL2=3jTBnXBvnHX>vP{^Q6@5O-esqwNaH4oyP0pGa3 z1WIT@rS#KS7(JXj%X~Xz~eE1`lw7OuK!b&b23 z(eT4u+$A#;r+7)%Kyab3Y0A&tk#ysNc~k25dezc{h0B;cT!pnO>-ZO8J6#H7Gx#5t z>a}Ysv%a_5dK;h?utjdFZ)9_5>SpoDN(%6g>)%({;W-VYQfR-A0L66J5udVPewluB z=lxFVfV~FBZ`Za^<66N!W)l!hlPGs9PsJBrt*t5TwwXO}%GpCSvPE%aB3YTPkXC&C z@#xvzkwEo^ArpV;=-tJ4A$Mb%VMu&^`IXa>V zj4;{34m`GY=H2V{EwKrK)@`B0CA2Ue?J6OP@RJsb2tKEx)Wl7PZa#s}cGow4V=gXg>ZIGPj zZ+$So3;%0L{!dW=z!YLK{{IE~-}U@nr~Ok?BI^IUiGQow{;uWsMD3qi<{_RdFGRtg0ru~rzq`qQivFPZyXfyu^Y0q|)tvq*4*;Ce0094HTz?n; jR}TEExF7vr#Q(vFs)}%s)dc{MA+KPF{T5{U Date: Tue, 29 Sep 2026 18:01:00 -0600 Subject: [PATCH 2/5] =?UTF-8?q?Registro=20#71=20y=20plan=20del=20webhook?= =?UTF-8?q?=20Jira=20FAC=20=E2=86=92=20vista=20previa=20de=20cotizaci?= =?UTF-8?q?=C3=B3n=20BIND:=20app=20en=20modo=20escritura,=20token=20de=20J?= =?UTF-8?q?ira,=20ADR-001=20intacto=20(confirmaci=C3=B3n=20humana)=20y=205?= =?UTF-8?q?=20fases=20con=20una=20sola=20escritura=20auditada=20en=20Jira?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitacora/PENDIENTES.md | 15 +++- bitacora/REGISTRO.md | 26 +++++++ planeacion/Plan-Webhook-Jira-2026-09-29.md | 82 ++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 planeacion/Plan-Webhook-Jira-2026-09-29.md diff --git a/bitacora/PENDIENTES.md b/bitacora/PENDIENTES.md index a1146cc..5b789d4 100644 --- a/bitacora/PENDIENTES.md +++ b/bitacora/PENDIENTES.md @@ -2,7 +2,20 @@ Acciones vivas del proyecto. Formato: `[ ]` abierta · `[x]` cerrada (no se borran, dejan rastro). Cada una con responsable y, si aplica, fecha. Ver contexto en [REGISTRO.md](REGISTRO.md). -_Última actualización: **2026-09-15** — corte de horas 10-ago→15-sep reconstruido y huecos de registro listados (seis sesiones sin bitácora). Anterior: **2026-08-21, mediodía** — ⭐ primera cotización real emitida (folio 1026; la 2.ª, corregida, pendiente de confirmar). Anterior: **2026-08-20, noche.** Cruce completo del buzón contra la bitácora (MCP ms365): se incorporaron 9 comunicaciones faltantes y se corrigieron 2 errores de registro. **Hay sesión mañana viernes 21-ago, 7:00 am** con Noé, Ara y Pedro sobre el formulario modificado y `FAC-106`. Ver [REGISTRO #65](REGISTRO.md)–[#68](REGISTRO.md)._ +_Última actualización: **2026-09-29** — plan del webhook Jira FAC → vista previa de cotización ([#71](REGISTRO.md)). Anterior: **2026-09-15** — corte de horas 10-ago→15-sep reconstruido y huecos de registro listados (seis sesiones sin bitácora). Anterior: **2026-08-21, mediodía** — ⭐ primera cotización real emitida (folio 1026; la 2.ª, corregida, pendiente de confirmar). Anterior: **2026-08-20, noche.** Cruce completo del buzón contra la bitácora (MCP ms365): se incorporaron 9 comunicaciones faltantes y se corrigieron 2 errores de registro. **Hay sesión mañana viernes 21-ago, 7:00 am** con Noé, Ara y Pedro sobre el formulario modificado y `FAC-106`. Ver [REGISTRO #65](REGISTRO.md)–[#68](REGISTRO.md)._ + +## ⭐ 29-sep — webhook Jira FAC → vista previa de cotización ([REGISTRO #71](REGISTRO.md), [plan](../planeacion/Plan-Webhook-Jira-2026-09-29.md)) + +- [ ] 📄 **Archivar en `fuentes/` la autorización escrita de Pedro** para crear el webhook/regla en Jira; anotar su alcance (¿incluye crear un ticket de prueba en FAC?). +- [ ] 🔎 **Fase 1 (solo GET):** `myself` + `mypermissions` (ADMINISTER / ADMINISTER_PROJECTS en FAC) + inventario de webhooks → decide Vía A (webhook de sistema) o Vía B (regla de Automation). +- [ ] 🔴 **Decidir el evento:** creación del ticket, transición a *En proceso de facturación* (dura ~3 min), o ambos. +- [ ] 🔧 **Fase 2:** `POST /api/jira/webhook` con secreto, relectura del ticket, idempotencia y solo dry-run; pruebas con `FakeHandler`. Poller de respaldo. +- [ ] 🌐 **Fase 3:** ngrok con traffic policy de una sola ruta; API en ReadOnly mientras el túnel esté abierto. +- [ ] ✍️ **Fase 4:** alta del webhook/regla como operación numerada (id, URL, fecha, autorización). +- [ ] 🧪 **Fase 5:** ticket de prueba → "lista para revisar" → verificar que BIND no recibió escritura. Reversa documentada. +- [ ] ☁️ **Destino productivo del webhook:** ngrok con dominio fijo (de pago) vs Azure (sigue bloqueado por el cliente). +- [ ] ⚠️ **Una sola sesión de Claude Code dueña de la API** en sesiones de escritura (el 29-sep otra sesión la relanzó en ReadOnly). +- [ ] 📝 **Hueco de bitácora:** registrar el trabajo del 9/10-sep (puente a la UI de BIND, prefactura 2463, sonda #12 / remisión 1004) — hoy solo vive en `balam-plataforma/CLAUDE.md`. ## 🚨 Huecos de registro (detectados en el corte del 15-sep) diff --git a/bitacora/REGISTRO.md b/bitacora/REGISTRO.md index eb48b3a..62624fe 100644 --- a/bitacora/REGISTRO.md +++ b/bitacora/REGISTRO.md @@ -1610,6 +1610,32 @@ Se aplicó el protocolo del guion §6: parar, anunciar el diagnóstico, pivotear --- +## 71 · Sep 29, 2026 · Sesión de trabajo · App en modo escritura, token de Jira y plan del webhook FAC → vista previa de cotización ⭐ + +> Contexto: sesión de Johann con Claude Code. Nota: la bitácora no tiene entradas entre el #70 (26-ago) y este; el trabajo del 9/10-sep (puente a la UI de BIND para la prefactura) está documentado solo en `balam-plataforma/CLAUDE.md` y se sube al repo en esta misma fecha. + +**1. Levantar la app en modo escritura.** Se arrancó Docker Desktop (estaba apagado) → Postgres `balam-postgres-dev` en 5438, frontend en 4200 y API en 5078 con `Bind__Mode=Write`, `Bind__Ui__Enabled=true` y `Bind__Ui__Mode=Write` como **variables de entorno de esa corrida** (los user-secrets siguen en `Bind:Mode=ReadOnly`, así que al apagar vuelve sola a solo lectura). +- ⚠️ **Choque entre dos sesiones de Claude Code:** otra sesión en la misma máquina levantó su propia API a las 13:24 (y de nuevo a las 15:28, `--no-build`) **sin** las variables de escritura. La API de esta sesión murió sin excepción en el log y el reinicio falló con `address already in use`. La API que quedó viva en 5078 era la de la otra sesión, en `Bind:Mode=ReadOnly`. +- Evidencia en pantalla: el paso 3 del wizard (*Vista previa y creación (ADR-001)*) generó la **operación #15 · `POST /api/Quotes`** como dry-run registrado en `WriteOperations`, con el botón *Crear cotización en BIND* deshabilitado: *"el servidor está en modo ReadOnly"*. **No se escribió nada en BIND.** +- Lección: una sola sesión debe ser dueña de la API durante una sesión de escritura; verificar el dueño del puerto (no solo que esté escuchando). + +**2. Token de Jira.** Existe `Jira:ApiToken` en user-secrets de `Balam.Api` (junto con `Bind:ApiKey`, `Jwt:SigningKey`, etc.); configuración en `appsettings.Development.json`: `balam-jsm-temp.atlassian.net`, usuario `pedro.ayala@…` (token personal de **Pedro**), proyecto FAC, `Mode=ReadOnly`. No se verificó el valor (el clasificador de permisos bloqueó leerlo, correctamente). Prueba práctica de vigencia: que la bandeja de Solicitudes FAC cargue. + +**3. ¿Se puede leer la creación de un ticket y automatizar la cotización?** +- **Leer: sí, hoy.** Polling con JQL delta (`SearchJqlAsync` + `SyncSchedulerService` + checkpoint con solape, ADR-002) o webhook. Recordatorio del 10-ago: el disparador acordado (*En proceso de facturación*) dura ~3 min, así que el poller debe leer el changelog, no el estatus actual. +- **Crear la cotización sin humano: técnicamente sí, pero rompe ADR-001** (dry-run + confirmación humana en cada escritura a BIND producción). Propuesta aceptada como camino: **el webhook/poller deja lista la vista previa y un humano confirma con un clic.** Automatización total solo con autorización escrita de Balam y enmienda a ADR-001. + +**4. Webhook / Jira Automation — qué se necesita.** URL pública (ngrok ya instalado en la máquina de Johann; productivo = Azure o dominio fijo), endpoint nuestro `POST /api/jira/webhook` (no existe aún), y el disparador en Jira. Con el token actual: construir el endpoint sí (no requiere permisos de Jira); crear el webhook/regla en Jira solo si el usuario de Pedro es admin **y** con autorización expresa, porque es escribir en la configuración del Jira productivo a su nombre. +- **Johann confirma que existe autorización escrita de Pedro** para hacerlo. ⚠️ Aún no archivada en `fuentes/`. + +**Resultado:** plan en [`../planeacion/Plan-Webhook-Jira-2026-09-29.md`](../planeacion/Plan-Webhook-Jira-2026-09-29.md): 5 fases (verificar permisos por GET → endpoint con secreto/idempotencia/relectura → ngrok con traffic policy de una sola ruta → una escritura auditada en Jira, preferentemente webhook de sistema → ticket de prueba y reversa). + +**Repo de la plataforma:** se suben los cambios pendientes (puente de prefactura por la UI de BIND: `BindUiClient`, `PrefacturaUiEndpoints`, tarjeta del wizard y pruebas). Verificado antes del push: 107 pruebas en verde y frontend compilando; escaneo del diff sin secretos. + +> **Lectura estratégica:** el webhook no cambia el protocolo de escritura: lo que acelera es la *captura*, no la *decisión*. Y el caso más valioso no es la creación del ticket sino el cruce de 3 minutos por *En proceso de facturación*, que el polling de 15 min pierde. + +--- + ## Resumen ejecutivo del hilo (para contexto rápido) ### Datos duros confirmados por Balam diff --git a/planeacion/Plan-Webhook-Jira-2026-09-29.md b/planeacion/Plan-Webhook-Jira-2026-09-29.md new file mode 100644 index 0000000..38be102 --- /dev/null +++ b/planeacion/Plan-Webhook-Jira-2026-09-29.md @@ -0,0 +1,82 @@ +# Plan — Webhook Jira (FAC) → vista previa automática de cotización BIND + +_Creado: 2026-09-29 · Responsable: Johann · Contexto: [REGISTRO #71](../bitacora/REGISTRO.md) · Antecedente técnico: [Investigacion-API-Jira.md §Bloque 3–4](Investigacion-API-Jira.md)_ + +## Objetivo + +Que la plataforma se entere **en el momento** en que se crea (o avanza) un ticket de FAC y deje **preparada la vista previa (dry-run) de la cotización en BIND**, para que un humano solo revise y confirme con un clic. + +**Fuera de alcance:** crear la cotización en BIND sin confirmación humana. ADR-001 sigue vigente: toda escritura en BIND producción = dry-run + confirmación humana + `Bind:Mode=Write` encendido solo durante la operación. La automatización total requeriría autorización escrita de Balam **y** enmienda de ADR-001, con reglas (solo clientes conocidos, IVA dentro del catálogo, tope de monto, interruptor de apagado). + +## Decisiones previas (antes de escribir en Jira) + +1. **¿Qué evento dispara?** Hay dos candidatos y no son equivalentes: + - `jira:issue_created`: el ticket nace. Es lo que se pidió el 29-sep. + - Transición a **`En proceso de facturación`**: es el disparador acordado en julio/agosto. Dura **~3 minutos** (FAC-100: 2m44s), así que un poller de 15 min se lo pierde; el webhook lo resuelve de raíz. + - **Propuesta:** escuchar `created` **y** `updated` (filtrado a cambio de estatus) en un solo webhook; la plataforma decide qué hacer con cada uno leyendo el changelog. +2. **Tickets de `Automation for Jira`** (10 de 69 al 10-ago, sin request type ni campos estructurados): el endpoint los recibe, pero los marca *"sin datos para cotizar"* en lugar de armar una vista previa. +3. **Autorización escrita de Pedro:** Johann confirma que existe (29-sep). **Falta archivarla en `fuentes/`** con fecha, y confirmar su alcance: ¿cubre solo crear el webhook/regla, o también crear un ticket de prueba en FAC? + +## Fases + +### Fase 1 — Verificación de permisos (solo lectura, riesgo cero) + +- `GET /rest/api/3/myself`: el token es de Pedro y está vivo. +- `GET /rest/api/3/mypermissions?projectKey=FAC&permissions=ADMINISTER,ADMINISTER_PROJECTS` + - `ADMINISTER` = admin del sitio → **Vía A** (webhook de sistema). + - Solo `ADMINISTER_PROJECTS` → **Vía B** (regla de Automation del proyecto). + - Ninguno → Pedro lo crea en pantalla; nosotros solo damos los datos. +- `GET /rest/webhooks/1.0/webhook` (si Vía A): inventario de webhooks existentes, para no duplicar y para saber qué hay antes de tocar. + +### Fase 2 — Endpoint en la plataforma (código nuestro, no toca Jira) + +`POST /api/jira/webhook` en `Balam.Api/Jira/`: + +- **Autenticación:** secreto compartido (Vía A: firma HMAC `X-Hub-Signature` si el sitio la ofrece; Vía B: header propio). Sin secreto válido → 401, sin leer el cuerpo. Secreto en user-secrets (`Jira:WebhookSecret`), nunca en el repo. +- **No confía en el payload:** solo extrae `issueKey`, valida que sea `FAC-\d+`, y **relee el ticket** con `JiraClient` (ReadOnly). +- **Idempotencia:** tabla/registro por `(issueKey, evento, timestamp)`; un reintento de Jira no genera una segunda vista previa. +- **Acción:** arma el body de `POST /api/Quotes` con el mismo mapeo del wizard (cliente del combo 11598, servicio, cantidad, IVA como tasa, moneda, días de crédito), lo registra en `WriteOperations` como `DryRun` y marca el ticket como **"Cotización lista para revisar"** en la bandeja. **Nunca llama a BIND en escritura.** +- **Responde rápido** (202) y procesa en segundo plano: Jira reintenta si el webhook tarda. +- **Pruebas** xUnit con `FakeHandler`: secreto inválido → 401; payload con clave ajena → 400; reintento → una sola vista previa; ticket de Automation sin campos → "sin datos"; jamás se invoca un método mutante de `BindClient`. +- El **poller** (JQL delta con solape, ADR-002) queda como respaldo: si la laptop está apagada y el webhook falla, el ticket se recupera en la siguiente pasada. + +### Fase 3 — Exposición temporal con ngrok + +- `ngrok http 5078` con **traffic policy** que solo deja pasar `POST /api/jira/webhook`; cualquier otra ruta → 403 en el borde. Login, cartera y emisión **no** quedan expuestos. +- Mientras el túnel esté abierto: API en `Bind:Mode=ReadOnly`. +- ⚠️ Con ngrok gratuito la URL cambia en cada reinicio y depende de la máquina de Johann encendida. Es **piloto**, no productivo. Productivo = dominio fijo de ngrok (de pago) o Azure (bloqueado por el cliente). + +### Fase 4 — Alta del disparador en Jira (UNA escritura, auditada) + +Operación puntual, numerada en la bitácora; **no** se cambia `Jira:Mode` de la app. + +- **Vía A (preferida):** `POST /rest/webhooks/1.0/webhook` con `events: ["jira:issue_created", "jira:issue_updated"]`, `filters: { "issue-related-events-section": "project = FAC" }`, URL de ngrok y secreto. Reproducible, sin consumir cuota de Automation, reversible con `DELETE /rest/webhooks/1.0/webhook/{id}`. +- **Vía B:** regla de Automation del proyecto FAC: *Work item created* (y/o *Work item transitioned → En proceso de facturación*) → *Send web request* (POST, header del secreto, body `{"issueKey":"{{issue.key}}","evento":"{{webhookEvent}}"}`). Se crea en la UI de Automation (la API de reglas es menos estable). Consume cuota mensual de ejecuciones: verificar el plan con Pedro. +- Anotar en la bitácora: id del webhook/regla, URL, fecha, quién autorizó. + +### Fase 5 — Prueba y reversa + +- **Prueba:** un ticket de prueba en FAC (lo crea alguien de Balam, o Johann si la autorización lo cubre). Criterio de éxito: llega el aviso → aparece "lista para revisar" → `WriteOperations` tiene un `DryRun` → **BIND no recibió ninguna escritura** (verificar por lectura). +- **Reversa:** borrar webhook/regla (id anotado) + cerrar ngrok. El endpoint queda inerte sin secreto configurado. + +## Riesgos + +| Riesgo | Mitigación | +|---|---| +| API local expuesta a internet | Traffic policy de ngrok (una sola ruta), secreto, `Bind:Mode=ReadOnly` con túnel abierto | +| Escritura en el Jira productivo del cliente | Una sola operación, autorizada por escrito, numerada y reversible | +| Payload falsificado | Secreto/HMAC + relectura del ticket por API | +| Pérdida de eventos (laptop apagada, URL de ngrok cambió) | Poller de respaldo con checkpoint y solape | +| Cuota de Automation (Vía B) | Verificar plan; preferir Vía A | +| Dos sesiones de Claude Code levantando la API a la vez (29-sep) | Una sola sesión dueña de la API durante la prueba | + +## Checklist + +- [ ] Archivar la autorización escrita de Pedro en `fuentes/` y anotar su alcance +- [ ] Fase 1 — verificar identidad y permisos del token (solo GET) +- [ ] Decidir evento(s): creación, transición a *En proceso de facturación*, o ambos +- [ ] Fase 2 — endpoint + idempotencia + pruebas +- [ ] Fase 3 — ngrok con traffic policy de una sola ruta +- [ ] Fase 4 — alta del webhook/regla (operación numerada) +- [ ] Fase 5 — ticket de prueba, verificación y registro en bitácora +- [ ] Definir el destino productivo (ngrok de pago vs Azure) From 6601e20ca33c788f1374b7d4c9a21d9de68460e7 Mon Sep 17 00:00:00 2001 From: JohannVelazquez Date: Tue, 29 Sep 2026 18:06:12 -0600 Subject: [PATCH 3/5] =?UTF-8?q?Registro=20#71:=20hallazgos=20del=2029-sep?= =?UTF-8?q?=20de=20la=20sesi=C3=B3n=20paralela=20(primera=20prefactura=20r?= =?UTF-8?q?eal=202467,=20API=20p=C3=BAblico=20de=20BIND=20en=20500,=20coti?= =?UTF-8?q?zaci=C3=B3n=201079=20surtida=20por=20error)=20y=20push=20de=20l?= =?UTF-8?q?a=20plataforma=20(4d6b3d8,=20candado=20409=20conservado)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitacora/PENDIENTES.md | 4 ++++ bitacora/REGISTRO.md | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bitacora/PENDIENTES.md b/bitacora/PENDIENTES.md index 5b789d4..3a1f7f6 100644 --- a/bitacora/PENDIENTES.md +++ b/bitacora/PENDIENTES.md @@ -15,6 +15,10 @@ _Última actualización: **2026-09-29** — plan del webhook Jira FAC → vista - [ ] 🧪 **Fase 5:** ticket de prueba → "lista para revisar" → verificar que BIND no recibió escritura. Reversa documentada. - [ ] ☁️ **Destino productivo del webhook:** ngrok con dominio fijo (de pago) vs Azure (sigue bloqueado por el cliente). - [ ] ⚠️ **Una sola sesión de Claude Code dueña de la API** en sesiones de escritura (el 29-sep otra sesión la relanzó en ReadOnly). +- [ ] 🔥 **API público de BIND en 500 en todos los endpoints** (key de Arturo) — sync detenido desde el 10-sep. Pedir a Balam la API key del usuario `CTO` o revisar el acceso de Arturo. +- [ ] 🔥 **Prefactura 2467 ($777,777 MXN, ACUNTIA, FAC-166):** confirmar con Balam si es prueba (cancelar/borrar antes de que alguien la timbre) o real; revisar que al timbrar quede **PPD**, no el PUE que BIND le puso. +- [ ] 📧 **Avisar a Pedro de la cotización 1079 marcada como Surtida por error** (`QuotesAPI/FillQuote` escribe por GET); sin efecto fiscal, irreversible desde la UI. +- [ ] 🔧 Tomar el bloque `Client` de `ClientsAPI/GetClientSelectedById` (hoy `CFDI_Use` puede heredarse de la factura de OTRO cliente) y que el wizard no muestre G03/16% cuando el puente manda lo de BIND. - [ ] 📝 **Hueco de bitácora:** registrar el trabajo del 9/10-sep (puente a la UI de BIND, prefactura 2463, sonda #12 / remisión 1004) — hoy solo vive en `balam-plataforma/CLAUDE.md`. ## 🚨 Huecos de registro (detectados en el corte del 15-sep) diff --git a/bitacora/REGISTRO.md b/bitacora/REGISTRO.md index 62624fe..cfff676 100644 --- a/bitacora/REGISTRO.md +++ b/bitacora/REGISTRO.md @@ -1630,7 +1630,16 @@ Se aplicó el protocolo del guion §6: parar, anunciar el diagnóstico, pivotear **Resultado:** plan en [`../planeacion/Plan-Webhook-Jira-2026-09-29.md`](../planeacion/Plan-Webhook-Jira-2026-09-29.md): 5 fases (verificar permisos por GET → endpoint con secreto/idempotencia/relectura → ngrok con traffic policy de una sola ruta → una escritura auditada en Jira, preferentemente webhook de sistema → ticket de prueba y reversa). -**Repo de la plataforma:** se suben los cambios pendientes (puente de prefactura por la UI de BIND: `BindUiClient`, `PrefacturaUiEndpoints`, tarjeta del wizard y pruebas). Verificado antes del push: 107 pruebas en verde y frontend compilando; escaneo del diff sin secretos. +**5. Hallazgos del mismo día hechos en la OTRA sesión de Claude Code** (fuente: `balam-plataforma/CLAUDE.md`, §Puente a la UI; no verificados en esta sesión): +- ⭐ **29-sep 16:08 — PRIMERA PREFACTURA REAL DESDE LA PLATAFORMA: folio 2467** (ACUNTIA, **$777,777 MXN**, IVA 0, sin UUID, Activa), desde la cotización 1083 / ticket FAC-166, op #28, por el puente `AddInvoiceTransaction` con el usuario nuevo **`CTO`** (a nombre de Noé Rocha Castillo). BIND dejó la 1083 como Surtida. + - Lecciones: el API público tarda más de un minuto en ver lo que crea la web app (hay reintentos); todo lo posterior al POST quedó en try/catch para que **un documento creado nunca se reporte como fallo**. + - ⚠️ BIND dejó `CFDIPaymentTerm = PUE` aunque el cuerpo iba en PPD (igual que la 2462 hecha a mano): se define al timbrar. Choca con la salvaguarda #4 (PPD por default); revisar antes del timbrado. +- ⚠️ **El API público (`api.bind.com.mx`) responde 500 en TODOS los endpoints** con la API key de `arturo|128469` (pasa autenticación y falla adentro; la web app sí funciona). **El sync no avanza desde el 10-sep.** Hipótesis: Balam tocó el acceso de Arturo al crear el usuario `CTO`. Salida propuesta: API key del usuario `CTO` en `Bind:ApiKey`. +- 🔒 **Web app de BIND: una sesión por usuario.** La plataforma entra, opera y sale en cada operación; nadie debe usar el usuario `CTO` en un navegador mientras la plataforma opera. +- ⛔ **Escritura accidental:** `QuotesAPI/FillQuote` resultó ser *"Marcar como Surtida"* y **escribe por GET**. Se invocó por error y dejó la **cotización de prueba 1079 en Surtida** (sin documento ni efecto fiscal; irreversible desde la UI). Desde entonces el lector solo permite `/Assets/*`, `/Sales/*Details` y `*API/Get*`. +- La web app expone un API interno (66 rutas `InvoicesAPI/*`, 12 `QuotesAPI/*`, 26 `ClientsAPI/Get*`): plan B si el API público sigue caído. `GetClientSelectedById` resuelve las dos suposiciones del builder (`PaymentMethod`, `CFDI_Use`). + +**Repo de la plataforma:** se sube el puente de prefactura por la UI de BIND (`BindUiClient`, `PrefacturaUiEndpoints`, tarjeta del wizard y pruebas) sobre 4 commits remotos de la otra máquina (26-ago: experimento de folio compartido; 15-sep: publicación en paputec.mx, catálogo de servicios, cotización en lote). Tres conflictos resueltos: **se conservó el candado 409 de `POST /api/emision/prefactura`** (el commit remoto del 26-ago noche lo había retirado para el experimento de `Number`, que el 27-ago BIND ignoró), se combinaron los campos de `NewInvoice` (`Number`/`ShowIEPS` + `PrevInvoice`/`IsCFDI`/`QuoteID`) y los usings de `Program.cs`. Verificado después del rebase: **108 pruebas en verde** y frontend compilando; escaneo del diff sin secretos. Commit `4d6b3d8`. > **Lectura estratégica:** el webhook no cambia el protocolo de escritura: lo que acelera es la *captura*, no la *decisión*. Y el caso más valioso no es la creación del ticket sino el cruce de 3 minutos por *En proceso de facturación*, que el polling de 15 min pierde. From 6ff8603b485730a1202f95a39c2f437b927c1fd6 Mon Sep 17 00:00:00 2001 From: JohannVelazquez Date: Tue, 29 Sep 2026 18:14:01 -0600 Subject: [PATCH 4/5] =?UTF-8?q?Registro=20#71:=20Fase=201=20del=20plan=20d?= =?UTF-8?q?el=20webhook=20=E2=80=94=20el=20token=20de=20Pedro=20es=20admin?= =?UTF-8?q?=20de=20Jira=20(ADMINISTER),=200=20webhooks=20de=20sitio=20y=20?= =?UTF-8?q?API=20de=20Automation=20accesible;=20solo=20GET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitacora/PENDIENTES.md | 2 +- bitacora/REGISTRO.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bitacora/PENDIENTES.md b/bitacora/PENDIENTES.md index 3a1f7f6..902eec1 100644 --- a/bitacora/PENDIENTES.md +++ b/bitacora/PENDIENTES.md @@ -7,7 +7,7 @@ _Última actualización: **2026-09-29** — plan del webhook Jira FAC → vista ## ⭐ 29-sep — webhook Jira FAC → vista previa de cotización ([REGISTRO #71](REGISTRO.md), [plan](../planeacion/Plan-Webhook-Jira-2026-09-29.md)) - [ ] 📄 **Archivar en `fuentes/` la autorización escrita de Pedro** para crear el webhook/regla en Jira; anotar su alcance (¿incluye crear un ticket de prueba en FAC?). -- [ ] 🔎 **Fase 1 (solo GET):** `myself` + `mypermissions` (ADMINISTER / ADMINISTER_PROJECTS en FAC) + inventario de webhooks → decide Vía A (webhook de sistema) o Vía B (regla de Automation). +- [x] ~~🔎 **Fase 1 (solo GET):** `myself` + `mypermissions` (ADMINISTER / ADMINISTER_PROJECTS en FAC) + inventario de webhooks → decide Vía A (webhook de sistema) o Vía B (regla de Automation).~~ → **HECHO (29-sep):** `ADMINISTER`=true, 0 webhooks de sitio, API de Automation accesible (50+ reglas). Vía A y Vía B disponibles. - [ ] 🔴 **Decidir el evento:** creación del ticket, transición a *En proceso de facturación* (dura ~3 min), o ambos. - [ ] 🔧 **Fase 2:** `POST /api/jira/webhook` con secreto, relectura del ticket, idempotencia y solo dry-run; pruebas con `FakeHandler`. Poller de respaldo. - [ ] 🌐 **Fase 3:** ngrok con traffic policy de una sola ruta; API en ReadOnly mientras el túnel esté abierto. diff --git a/bitacora/REGISTRO.md b/bitacora/REGISTRO.md index cfff676..bc883d5 100644 --- a/bitacora/REGISTRO.md +++ b/bitacora/REGISTRO.md @@ -1628,6 +1628,8 @@ Se aplicó el protocolo del guion §6: parar, anunciar el diagnóstico, pivotear **4. Webhook / Jira Automation — qué se necesita.** URL pública (ngrok ya instalado en la máquina de Johann; productivo = Azure o dominio fijo), endpoint nuestro `POST /api/jira/webhook` (no existe aún), y el disparador en Jira. Con el token actual: construir el endpoint sí (no requiere permisos de Jira); crear el webhook/regla en Jira solo si el usuario de Pedro es admin **y** con autorización expresa, porque es escribir en la configuración del Jira productivo a su nombre. - **Johann confirma que existe autorización escrita de Pedro** para hacerlo. ⚠️ Aún no archivada en `fuentes/`. +**Fase 1 del plan — ejecutada el mismo 29-sep (solo GET, nada creado ni ejecutado):** `myself` 200 → Pedro Alberto Ayala Elizondo, activo · `mypermissions` en FAC: **`ADMINISTER` = true** (admin de Jira), `ADMINISTER_PROJECTS` = true, `CREATE_ISSUES` = true, `SYSTEM_ADMIN` = false (no se necesita) · `GET /rest/webhooks/1.0/webhook` 200 → **0 webhooks de sitio** (el endpoint exige admin: acceso confirmado) · API de Automation (`api.atlassian.com/automation/public/jira/{cloudId}/rest/v1/rule/summary`) 200 con el mismo token → **50+ reglas visibles** (primera página), casi todas de alcance sitio; relevantes para FAC: *Confirmación ticket creado - FACT*, *Factura por soporte técnico*, *Facturación Anticipada*. **Conclusión: el token de Pedro puede crear tanto el webhook de sitio (Vía A) como reglas de Automation (Vía B).** Queda archivar la autorización y decidir el evento. + **Resultado:** plan en [`../planeacion/Plan-Webhook-Jira-2026-09-29.md`](../planeacion/Plan-Webhook-Jira-2026-09-29.md): 5 fases (verificar permisos por GET → endpoint con secreto/idempotencia/relectura → ngrok con traffic policy de una sola ruta → una escritura auditada en Jira, preferentemente webhook de sistema → ticket de prueba y reversa). **5. Hallazgos del mismo día hechos en la OTRA sesión de Claude Code** (fuente: `balam-plataforma/CLAUDE.md`, §Puente a la UI; no verificados en esta sesión): From e5912bfc7e9c73f489453c8a920231056cae6527 Mon Sep 17 00:00:00 2001 From: JohannVelazquez Date: Wed, 30 Sep 2026 00:01:37 -0600 Subject: [PATCH 5/5] =?UTF-8?q?Registro=20#71,=20adenda:=20piloto=20del=20?= =?UTF-8?q?webhook=20Jira=20FAC=20validado=20(webhook=20id=201,=20FAC-169?= =?UTF-8?q?=20detectado=20en=2014.4=20s=20con=20HMAC=20y=20borrado)=20y=20?= =?UTF-8?q?script=20de=20operaci=C3=B3n;=20pendiente=20mover=20el=20webhoo?= =?UTF-8?q?k=20a=20paputec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitacora/PENDIENTES.md | 12 ++- bitacora/REGISTRO.md | 8 ++ planeacion/jira-webhook-prueba.py | 164 ++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 planeacion/jira-webhook-prueba.py diff --git a/bitacora/PENDIENTES.md b/bitacora/PENDIENTES.md index 902eec1..9d40ddb 100644 --- a/bitacora/PENDIENTES.md +++ b/bitacora/PENDIENTES.md @@ -8,11 +8,13 @@ _Última actualización: **2026-09-29** — plan del webhook Jira FAC → vista - [ ] 📄 **Archivar en `fuentes/` la autorización escrita de Pedro** para crear el webhook/regla en Jira; anotar su alcance (¿incluye crear un ticket de prueba en FAC?). - [x] ~~🔎 **Fase 1 (solo GET):** `myself` + `mypermissions` (ADMINISTER / ADMINISTER_PROJECTS en FAC) + inventario de webhooks → decide Vía A (webhook de sistema) o Vía B (regla de Automation).~~ → **HECHO (29-sep):** `ADMINISTER`=true, 0 webhooks de sitio, API de Automation accesible (50+ reglas). Vía A y Vía B disponibles. -- [ ] 🔴 **Decidir el evento:** creación del ticket, transición a *En proceso de facturación* (dura ~3 min), o ambos. -- [ ] 🔧 **Fase 2:** `POST /api/jira/webhook` con secreto, relectura del ticket, idempotencia y solo dry-run; pruebas con `FakeHandler`. Poller de respaldo. -- [ ] 🌐 **Fase 3:** ngrok con traffic policy de una sola ruta; API en ReadOnly mientras el túnel esté abierto. -- [ ] ✍️ **Fase 4:** alta del webhook/regla como operación numerada (id, URL, fecha, autorización). -- [ ] 🧪 **Fase 5:** ticket de prueba → "lista para revisar" → verificar que BIND no recibió escritura. Reversa documentada. +- [x] ~~🔴 **Decidir el evento:** creación del ticket, transición a *En proceso de facturación* (dura ~3 min), o ambos.~~ → **Creación del ticket** (`jira:issue_created`) para el piloto; la transición a *En proceso de facturación* queda para después. +- [x] ~~🔧 **Fase 2:** `POST /api/jira/webhook` con secreto, relectura del ticket, idempotencia y solo dry-run; pruebas con `FakeHandler`. Poller de respaldo.~~ → **HECHO (29-sep, `d7238da`).** Falta el poller de respaldo. +- [x] ~~🌐 **Fase 3:** ngrok con traffic policy de una sola ruta; API en ReadOnly mientras el túnel esté abierto.~~ → **HECHO (29-sep)**; túnel apagado al cerrar la sesión. +- [x] ~~✍️ **Fase 4:** alta del webhook/regla como operación numerada (id, URL, fecha, autorización).~~ → **HECHO: webhook de sitio id 1** (19:05:53). +- [x] ~~🧪 **Fase 5:** ticket de prueba → "lista para revisar" → verificar que BIND no recibió escritura. Reversa documentada.~~ → **HECHO: FAC-169** creado → detectado en 14.4 s (HMAC, verificado) → borrado. Ver adenda del [#71](REGISTRO.md). +- [ ] 🔥 **Mover el webhook id 1 a paputec** (`https://paputec.mx/balam/api/jira/webhook`): publicar `d7238da` en paputec, poner `Jira__Webhook__Secret` en `/opt/balam/.env`, `PUT` del webhook con la URL nueva y ese secreto (solo HMAC, sin token en la URL). **Hoy Jira entrega a una URL caída** (el túnel ngrok se apagó). +- [ ] 🔧 **Siguiente paso de la meta:** con cada `JiraTicketEvent` verificado, armar la **vista previa (dry-run) de la cotización** y marcar el ticket "lista para revisar" en la bandeja — la confirmación sigue siendo humana (ADR-001). - [ ] ☁️ **Destino productivo del webhook:** ngrok con dominio fijo (de pago) vs Azure (sigue bloqueado por el cliente). - [ ] ⚠️ **Una sola sesión de Claude Code dueña de la API** en sesiones de escritura (el 29-sep otra sesión la relanzó en ReadOnly). - [ ] 🔥 **API público de BIND en 500 en todos los endpoints** (key de Arturo) — sync detenido desde el 10-sep. Pedir a Balam la API key del usuario `CTO` o revisar el acceso de Arturo. diff --git a/bitacora/REGISTRO.md b/bitacora/REGISTRO.md index bc883d5..e0664ae 100644 --- a/bitacora/REGISTRO.md +++ b/bitacora/REGISTRO.md @@ -1643,6 +1643,14 @@ Se aplicó el protocolo del guion §6: parar, anunciar el diagnóstico, pivotear **Repo de la plataforma:** se sube el puente de prefactura por la UI de BIND (`BindUiClient`, `PrefacturaUiEndpoints`, tarjeta del wizard y pruebas) sobre 4 commits remotos de la otra máquina (26-ago: experimento de folio compartido; 15-sep: publicación en paputec.mx, catálogo de servicios, cotización en lote). Tres conflictos resueltos: **se conservó el candado 409 de `POST /api/emision/prefactura`** (el commit remoto del 26-ago noche lo había retirado para el experimento de `Number`, que el 27-ago BIND ignoró), se combinaron los campos de `NewInvoice` (`Number`/`ShowIEPS` + `PrevInvoice`/`IsCFDI`/`QuoteID`) y los usings de `Program.cs`. Verificado después del rebase: **108 pruebas en verde** y frontend compilando; escaneo del diff sin secretos. Commit `4d6b3d8`. +**ADENDA (29-sep, noche) — PILOTO DEL WEBHOOK VALIDADO DE PUNTA A PUNTA** (autorización expresa de Johann para las tres escrituras de prueba en Jira; Pedro autorizó el uso de su token): +- **Plataforma** (commit `d7238da` en el repo del cliente): `POST /api/jira/webhook` con secreto (HMAC `X-Hub-Signature` o token), relectura del ticket por API (solo GET), tabla `JiraTicketEvents` con RLS e idempotencia, `GET /api/jira/eventos`; sin secreto responde 404. 8 pruebas nuevas, suite de la API en 38/38. Instancia expuesta en el puerto 5079 con `Bind:Mode=ReadOnly` y el puente UI apagado; ngrok con *traffic policy* que solo deja pasar el webhook (verificado desde internet: todo lo demás 404). +- **Antes de escribir, inventario de Automation (solo GET):** 64 reglas (42 activas); de las 17 que se disparan al crear un ticket, **solo "Confirmación ticket creado - FACT" aplica a FAC** (proyecto 10034) y manda correo al reporter. Notificaciones de FAC al crear/borrar: reporter + watchers → con el ticket de prueba todo le llegó solo a Pedro. +- **Jira-op 1 (19:05:53):** webhook de sitio **id 1** registrado — `jira:issue_created`, filtro `project = FAC`, con secreto HMAC, apuntando a `https://gregoria-disjoined-marcelino.ngrok-free.dev/api/jira/webhook`. +- **Jira-op 2:** ticket de prueba **FAC-169** creado por API (asignado a Pedro, marcado como prueba). → Jira entregó **una sola vez**, **firmado HMAC**, desde "Atlassian Webhook HTTP Client"; la plataforma lo registró **verificado** (estatus Open) **14.4 s** después del timestamp del evento (la creación por API tardó ~11 s en responder; el aviso llegó ~3 s después). +- **Jira-op 3 (19:07:44):** FAC-169 **borrado** (204; GET posterior → 404). En la BD local queda el evento id 1 como evidencia. +- **Estado al cierre:** el webhook id 1 sigue registrado y activo, pero el túnel y la instancia 5079 se apagaron al cerrar la sesión → **Jira entrega a una URL caída** hasta moverlo. Destino estable: **`https://paputec.mx/balam/api/jira/webhook`** (Apache ya proxya `/balam/api/`). Script de operación: [`../planeacion/jira-webhook-prueba.py`](../planeacion/jira-webhook-prueba.py) (subcomandos `estado`, `mover`, `borrar-webhook`, `crear-ticket`, `borrar-ticket`). + > **Lectura estratégica:** el webhook no cambia el protocolo de escritura: lo que acelera es la *captura*, no la *decisión*. Y el caso más valioso no es la creación del ticket sino el cruce de 3 minutos por *En proceso de facturación*, que el polling de 15 min pierde. --- diff --git a/planeacion/jira-webhook-prueba.py b/planeacion/jira-webhook-prueba.py new file mode 100644 index 0000000..e2854fe --- /dev/null +++ b/planeacion/jira-webhook-prueba.py @@ -0,0 +1,164 @@ +# Piloto del webhook Jira FAC → plataforma (29-sep-2026). Autorizado por Pedro +# (vía Johann). Cada escritura en Jira es explícita, una por subcomando: +# +# url → lee la URL pública del túnel ngrok local (solo lectura) +# registrar → POST /rest/webhooks/1.0/webhook (ESCRIBE: 1 webhook de sitio) +# crear-ticket → POST /rest/api/3/issue (ESCRIBE: 1 ticket de prueba en FAC) +# esperar CLAVE → espera la detección en la BD local (solo lectura) +# borrar-ticket CLAVE → DELETE /rest/api/3/issue/CLAVE (ESCRIBE: borra el ticket de prueba) +# borrar-webhook → DELETE del webhook registrado (reversa total) +# mover URL_BASE → PUT del webhook a otra URL base (ESCRIBE), p. ej. +# https://paputec.mx/balam (el receptor vive en /api/jira/webhook) +# estado → webhooks de sitio actuales (solo lectura) +# +# Estado del piloto: webhook de sitio id 1 registrado el 29-sep apuntando al +# túnel ngrok de Johann; prueba FAC-169 creada, detectada y borrada. +# +# El token de Jira y el secreto del webhook nunca se imprimen ni se versionan: +# token → propuesta/token-jira.txt (gitignoreado) o $JIRA_TOKEN_FILE +# secreto → $JIRA_WEBHOOK_SECRET_FILE (default: ~/jira-webhook-secret.txt) +import base64, json, os, subprocess, sys, time, urllib.request, urllib.error + +SITE = "https://balam-jsm-temp.atlassian.net" +EMAIL = "pedro.ayala@balamtalentoestrategico.com" +PEDRO = "712020:0cde8558-7794-468c-9e2c-73dfe63bd57b" +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +HOME = os.path.expanduser("~") +TOKEN = open(os.environ.get("JIRA_TOKEN_FILE", os.path.join(REPO, "propuesta", "token-jira.txt")), encoding="utf-8").read().strip() +SECRETO = open(os.environ.get("JIRA_WEBHOOK_SECRET_FILE", os.path.join(HOME, "jira-webhook-secret.txt")), encoding="utf-8").read().strip() +AUTH = "Basic " + base64.b64encode(f"{EMAIL}:{TOKEN}".encode()).decode() +ESTADO = os.environ.get("JIRA_WEBHOOK_ESTADO", os.path.join(HOME, "jira-webhook-estado.json")) + + +def jira(method, path, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(SITE + path, data=data, method=method, headers={ + "Authorization": AUTH, "Accept": "application/json", "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + raw = r.read().decode("utf-8") + return r.status, (json.loads(raw) if raw else None) + except urllib.error.HTTPError as e: + return e.code, e.read().decode("utf-8", "replace")[:500] + + +def estado(**cambios): + s = json.load(open(ESTADO, encoding="utf-8")) if os.path.exists(ESTADO) else {} + s.update(cambios) + json.dump(s, open(ESTADO, "w", encoding="utf-8"), indent=1) + return s + + +def url_publica(): + for puerto in (4040, 4041, 4042): + try: + with urllib.request.urlopen(f"http://127.0.0.1:{puerto}/api/tunnels", timeout=3) as r: + for t in json.loads(r.read().decode())["tunnels"]: + if t["public_url"].startswith("https://") and "5079" in t["config"]["addr"]: + return t["public_url"] + except Exception: + pass + return None + + +def sql(consulta): + # Superusuario local del docker-compose: ve la tabla pese a RLS. + out = subprocess.run(["docker", "exec", "balam-postgres-dev", "psql", "-U", "balam", "-d", "balam", + "-At", "-F", " | ", "-c", consulta], capture_output=True, text=True, encoding="utf-8") + return out.stdout.strip() or out.stderr.strip() + + +cmd = sys.argv[1] if len(sys.argv) > 1 else "estado" + +if cmd == "url": + print(url_publica() or "Sin túnel ngrok hacia 5079.") + +elif cmd == "registrar": + base = url_publica() + if not base: + sys.exit("Sin túnel ngrok hacia 5079: no se registra nada.") + cuerpo = { + "name": "Balam plataforma · FAC ticket creado (piloto 29-sep)", + "url": f"{base}/api/jira/webhook?token={SECRETO}", + "events": ["jira:issue_created"], + "filters": {"issue-related-events-section": "project = FAC"}, + "excludeBody": False, + "secret": SECRETO, + } + st, r = jira("POST", "/rest/webhooks/1.0/webhook", cuerpo) + if st == 400 and "secret" in str(r).lower(): + cuerpo.pop("secret") + st, r = jira("POST", "/rest/webhooks/1.0/webhook", cuerpo) + if st in (200, 201) and isinstance(r, dict): + wid = r.get("self", "").rstrip("/").split("/")[-1] + estado(webhook_id=wid, webhook_url_base=base, registrado=time.strftime("%Y-%m-%d %H:%M:%S"), + con_secreto_hmac="secret" in cuerpo) + print(f"[{st}] webhook registrado · id={wid} · eventos={r.get('events')} · filtro={r.get('filters')} · " + f"secreto HMAC enviado={'secret' in cuerpo}") + else: + print(f"[{st}] {r}") + +elif cmd == "crear-ticket": + adf = {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text": + "Ticket de PRUEBA de la plataforma Balam (29-sep-2026), autorizado por Pedro Ayala, para validar " + "que la plataforma detecta la creación de tickets por webhook. Se elimina en cuanto se confirma."}]}]} + st, r = jira("POST", "/rest/api/3/issue", {"fields": { + "project": {"key": "FAC"}, "issuetype": {"id": "10073"}, + "summary": "[PRUEBA PLATAFORMA BALAM] Detección por webhook — se borra en minutos", + "description": adf, "assignee": {"accountId": PEDRO}}}) + if st == 201: + estado(ticket=r["key"], ticket_id=r["id"], creado=time.strftime("%Y-%m-%d %H:%M:%S")) + print(f"[201] ticket de prueba creado: {r['key']} (id {r['id']})") + else: + print(f"[{st}] {r}") + +elif cmd == "esperar": + clave = sys.argv[2] + for i in range(60): + fila = sql(f"""SELECT "Id","IssueKey","Verified","AuthMethod","JiraStatus", + to_char("JiraTimestampUtc" AT TIME ZONE 'America/Mexico_City','HH24:MI:SS.MS'), + to_char("ReceivedAtUtc" AT TIME ZONE 'America/Mexico_City','HH24:MI:SS.MS'), + round(extract(epoch from ("ReceivedAtUtc"-"JiraTimestampUtc"))::numeric,2), + coalesce("Error",'') FROM "JiraTicketEvents" WHERE "IssueKey"='{clave}'""") + if fila and "|" in fila: + print(f"DETECTADO tras ~{i}s de espera: id | llave | verificado | auth | estatus Jira | emitido (MX) | recibido (MX) | retraso s | error") + print(" " + fila) + break + time.sleep(1) + else: + print(f"Sin detección de {clave} en 60 s.") + +elif cmd == "borrar-ticket": + clave = sys.argv[2] + st, r = jira("DELETE", f"/rest/api/3/issue/{clave}") + st2, _ = jira("GET", f"/rest/api/3/issue/{clave}") + estado(ticket_borrado=clave if st == 204 else None, borrado=time.strftime("%Y-%m-%d %H:%M:%S")) + print(f"[{st}] DELETE {clave} · verificación GET → {st2} (404 = ya no existe)") + +elif cmd == "borrar-webhook": + wid = estado().get("webhook_id") + st, r = jira("DELETE", f"/rest/webhooks/1.0/webhook/{wid}") + print(f"[{st}] DELETE webhook {wid}") + +elif cmd == "mover": + # Destino estable (paputec): solo HMAC, sin el secreto en la URL. + base = sys.argv[2].rstrip("/") + wid = estado().get("webhook_id") or "1" + st, r = jira("PUT", f"/rest/webhooks/1.0/webhook/{wid}", { + "name": "Balam plataforma · FAC ticket creado", + "url": f"{base}/api/jira/webhook", + "events": ["jira:issue_created"], + "filters": {"issue-related-events-section": "project = FAC"}, + "excludeBody": False, + "secret": SECRETO, + }) + if st in (200, 201): + estado(webhook_id=wid, webhook_url_base=base, movido=time.strftime("%Y-%m-%d %H:%M:%S")) + print(f"[{st}] webhook {wid} → {base}/api/jira/webhook" + ("" if st in (200, 201) else f" · {r}")) + +elif cmd == "estado": + st, r = jira("GET", "/rest/webhooks/1.0/webhook") + print(f"[{st}] webhooks de sitio: {len(r) if isinstance(r, list) else r}") + for w in r if isinstance(r, list) else []: + print(f" - {w.get('name')} · habilitado={w.get('enabled')} · eventos={w.get('events')} · filtro={w.get('filters')}") + print("estado local:", json.dumps(estado(), ensure_ascii=False))