Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
When document matching completes, the UI doesn't update. The user must reload the page to see match results. The existing SSE infrastructure handles ingestion and DATEV export events but has no matching event type.
Extend the existing SSE pipeline (Postgres NOTIFY → core.async pub → SSE looper) with a matching event type.
New migration: notify_matching_event() on document table. Fires on UPDATE when matching_status changes to a terminal state (succeeded or failed).
Payload:
{
"event/type": "matching",
"document/id": "<uuid>",
"document/matching-status": "succeeded|failed",
"legal-entity/id": "<uuid>",
"tenant/id": "<uuid>",
"old-status": "pending|in-progress"
}
The trigger only fires when matching_status actually changes (OLD.matching_status IS DISTINCT FROM NEW.matching_status) and only for terminal states. Non-terminal transitions (pending, in-progress) are ignored.
Add MatchingEvent to DocumentEvent multi in erp/ingestion.clj:
(def MatchingEvent
[:map
[:event/type [:= :matching]]
[:document/id schema.document/ID]
[:document/matching-status [:enum "succeeded" "failed"]]
[:legal-entity/id schema.legal-entity/ID]
[:tenant/id schema.tenant/ID]
[:old-status [:maybe :string]]])
The existing publisher coerces via DocumentEvent multi-dispatch — adding :matching to the multi makes it work automatically.
Add :matching case to detail-events in view/shared.clj. On matching event, re-render #section-matches using the existing matches-section function.
Need to resolve: matches-section requires doc-type, which the handler doesn't currently have. Fetch document type from DB when matching event arrives (single query, same as other event handlers do).
No :stop? — the connection stays open for potential export events.
In matches-section, when matching_status is pending or in-progress, render:
[:div {:hx-ext "sse"
:sse-connect (path-for router ::detail-events {:document-id document-id})
:sse-swap "matching-complete"
:hx-target "#section-matches"
:hx-swap "outerHTML"}]
Same pattern as the export SSE div in datev-export-section.
matching worker: set-matching-status!(db, doc-id, {:status "succeeded"})
→ Postgres UPDATE on document.matching_status
→ Trigger fires once (regardless of how many matches were created)
→ pg_notify('document_events', matching_event_payload)
→ Publisher coerces to MatchingEvent, publishes to pub keyed by tenant-id
→ detail-events SSE handler receives event, filters to document-id
→ Calls get-matched-documents (returns all matches created during matching)
→ Re-renders #section-matches with all match cards
→ HTMX swaps the section in the browser
The detail page uses chained SSE connections per lifecycle phase:
status-changed. On terminal status, re-renders #document-area and stops.matches-section) and export (in datev-export-section). Each opens its own connection.matching-complete, HTMX swaps #section-matches.export-status-changed, HTMX swaps #section-datev-export.No risk of premature closing. Each connection handles its own lifecycle.
get-matched-documents returns [], section shows "No counterpart" placeholders.#section-matches div is replaced, not the full #document-area.