<mxfile host="app.diagrams.net" agent="ApiPay Hub docs-portal" type="device" version="24.7.17">
  <diagram id="apipay-transactional-outbox" name="Transactional outbox y su relay">
    <mxGraphModel dx="1800" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
      <root>
        <mxCell id="0" />
        <mxCell id="1" parent="0" />

        <mxCell id="lay-biz" value="Transaccion de negocio - rol apipay_app" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#9E9E9E;dashed=1;dashPattern=8 8;verticalAlign=top;align=left;spacingLeft=10;spacingTop=6;fontColor=#1A1A1A;fontSize=13;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="40" y="60" width="420" height="400" as="geometry" />
        </mxCell>
        <mxCell id="lay-relay" value="OutboxRelay - rol apipay_worker (bean apipayWorkerDataSource)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#9E9E9E;dashed=1;dashPattern=8 8;verticalAlign=top;align=left;spacingLeft=10;spacingTop=6;fontColor=#1A1A1A;fontSize=13;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="860" y="60" width="420" height="420" as="geometry" />
        </mxCell>
        <mxCell id="lay-bug" value="Por que el relay NO puede ir sobre apipay_app" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FDECEA;strokeColor=#C62828;dashed=1;dashPattern=8 8;verticalAlign=top;align=left;spacingLeft=10;spacingTop=6;fontColor=#B71C1C;fontSize=13;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="520" y="340" width="280" height="290" as="geometry" />
        </mxCell>

        <mxCell id="svc" value="PaymentIntentService.confirm()&lt;br&gt;un solo caso de uso, una sola transaccion" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="60" y="110" width="380" height="50" as="geometry" />
        </mxCell>
        <mxCell id="setlocal" value="SET LOCAL app.tenant_id = tenant del request" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="60" y="180" width="380" height="45" as="geometry" />
        </mxCell>
        <mxCell id="upd" value="UPDATE payment_intents SET status = ..." style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="60" y="245" width="380" height="45" as="geometry" />
        </mxCell>
        <mxCell id="ins" value="INSERT INTO apipay.outbox_events&lt;br&gt;published_at NULL - lo escribe OutboxJpaPublisher" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="60" y="310" width="380" height="55" as="geometry" />
        </mxCell>
        <mxCell id="commit" value="COMMIT: o se guardan el estado Y el evento,&lt;br&gt;o no se guarda ninguno de los dos" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;fontColor=#1A1A1A;fontSize=11;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="60" y="385" width="380" height="55" as="geometry" />
        </mxCell>

        <mxCell id="tabla" value="apipay.outbox_events&lt;br&gt;id - tenant_id - topic - payload&lt;br&gt;created_at - published_at" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=14;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="520" y="180" width="280" height="110" as="geometry" />
        </mxCell>

        <mxCell id="sched" value="OutboxRelay.drain() @Scheduled" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="880" y="110" width="380" height="45" as="geometry" />
        </mxCell>
        <mxCell id="txt" value="TransactionTemplate abre la transaccion a mano&lt;br&gt;(con @Transactional se ataba al gestor del DataSource&lt;br&gt;principal, que no cubre las conexiones del worker)" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="880" y="170" width="380" height="65" as="geometry" />
        </mxCell>
        <mxCell id="claim" value="SELECT ... FROM apipay.outbox_events&lt;br&gt;WHERE published_at IS NULL&lt;br&gt;ORDER BY created_at LIMIT :limit&lt;br&gt;FOR UPDATE SKIP LOCKED" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="880" y="250" width="380" height="80" as="geometry" />
        </mxCell>
        <mxCell id="pub" value="KafkaTemplate.send()&lt;br&gt;clave de particion = tenant_id" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="880" y="345" width="380" height="50" as="geometry" />
        </mxCell>
        <mxCell id="mark" value="UPDATE published_at, en la MISMA transaccion&lt;br&gt;que reclamo el lote: si se cerrara antes, dos replicas&lt;br&gt;publicarian el mismo evento" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="880" y="410" width="380" height="60" as="geometry" />
        </mxCell>

        <mxCell id="kafka" value="Kafka / Event Hubs&lt;br&gt;apipay.payment-events.v1&lt;br&gt;apipay.outbound-webhooks.v1&lt;br&gt;apipay.audit.v1" style="shape=process;size=0.06;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="1340" y="180" width="240" height="110" as="geometry" />
        </mxCell>
        <mxCell id="dispatcher" value="webhook-dispatcher&lt;br&gt;materializa las deliveries&lt;br&gt;y deduplica por evt_" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="1340" y="340" width="240" height="80" as="geometry" />
        </mxCell>

        <mxCell id="bug1" value="La policy de outbox_events para apipay_app es&lt;br&gt;tenant_isolation, que evalua:&lt;br&gt;&lt;br&gt;tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid" style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#C62828;fontColor=#1A1A1A;fontSize=10" vertex="1" parent="1">
          <mxGeometry x="535" y="385" width="250" height="115" as="geometry" />
        </mxCell>
        <mxCell id="bug2" value="El relay no fija ningun tenant: la expresion es NULL y NO califica ninguna fila. Resultado: 0 filas leidas, cero eventos publicados y ni un error en el log. Bug real, detectado y corregido; verificado contra PostgreSQL 17." style="rounded=1;arcSize=12;whiteSpace=wrap;html=1;fillColor=#FFCDD2;strokeColor=#C62828;fontColor=#1A1A1A;fontSize=10;fontStyle=1" vertex="1" parent="1">
          <mxGeometry x="535" y="510" width="250" height="105" as="geometry" />
        </mxCell>

        <mxCell id="nota-ok" value="La policy que SI cruza tenants es platform_access: FOR ALL TO apipay_worker USING (true) WITH CHECK (true). Cruzar tenants es precisamente el cometido del relay, y por eso su DataSource es otro. Si el bean apipayWorkerDataSource falta, el contexto falla al arrancar: es infinitamente preferible a un relay que arranca y no publica nada en silencio." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="860" y="500" width="420" height="130" as="geometry" />
        </mxCell>
        <mxCell id="nota-bypass" value="Ningun rol de runtime tiene BYPASSRLS: el aislamiento no se salta, cambia de dueno. Mover el relay a apipay_worker no debilita la RLS, la usa como esta disenada." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="40" y="490" width="420" height="80" as="geometry" />
        </mxCell>
        <mxCell id="nota-skip" value="FOR UPDATE SKIP LOCKED permite N replicas del relay sobre la misma tabla sin publicar dos veces el mismo evento: cada una salta las filas que otra tiene bloqueadas. Por eso payment-api y admin-api pueden mantener cada uno su relay y drenar la misma tabla." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="1340" y="450" width="240" height="180" as="geometry" />
        </mxCell>
        <mxCell id="nota-dualwrite" value="Sin outbox habria dual-write: escribir el estado en PostgreSQL y publicar en Kafka como dos operaciones separadas, con la ventana en que una tiene exito y la otra no. La fila de outbox convierte la publicacion en un efecto de la MISMA transaccion, y el relay la entrega at-least-once." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#283593;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="40" y="590" width="460" height="100" as="geometry" />
        </mxCell>

        <mxCell id="e-svc-set" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="svc" target="setlocal">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-set-upd" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="setlocal" target="upd">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-upd-ins" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="upd" target="ins">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-ins-commit" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="ins" target="commit">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-commit-tabla" value="atomico: sin dual-write" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;strokeWidth=2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.85;entryDx=0;entryDy=0" edge="1" parent="1" source="commit" target="tabla">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="490" y="413" />
              <mxPoint x="490" y="274" />
            </Array>
          </mxGeometry>
        </mxCell>

        <mxCell id="e-sched-txt" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="sched" target="txt">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-txt-claim" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="txt" target="claim">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-tabla-claim" value="lote FIFO reclamado&lt;br&gt;y bloqueado hasta el commit" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;strokeWidth=2;exitX=1;exitY=0.35;exitDx=0;exitDy=0;entryX=0;entryY=0.35;entryDx=0;entryDy=0" edge="1" parent="1" source="tabla" target="claim">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="830" y="219" />
              <mxPoint x="830" y="278" />
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e-claim-pub" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="claim" target="pub">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-pub-mark" value="solo los confirmados&lt;br&gt;por el broker" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="pub" target="mark">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-pub-kafka" value="publica" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.85;entryDx=0;entryDy=0" edge="1" parent="1" source="pub" target="kafka">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1310" y="370" />
              <mxPoint x="1310" y="274" />
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e-mark-tabla" value="published_at&lt;br&gt;no se republica" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.85;entryDx=0;entryDy=0" edge="1" parent="1" source="mark" target="tabla">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="840" y="440" />
              <mxPoint x="840" y="274" />
            </Array>
          </mxGeometry>
        </mxCell>
        <mxCell id="e-kafka-disp" value="consume" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="kafka" target="dispatcher">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-tabla-bug" value="lo que veia el relay roto" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;dashed=1;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="tabla" target="lay-bug">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-bug1-bug2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="bug1" target="bug2">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="e-relay-ok" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;dashed=1;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;labelBackgroundColor=#FFFFFF;endArrow=block;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="lay-relay" target="nota-ok">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
