Cairnloop.Outbound.BulkEnvelope (cairnloop v0.5.1)

Copy Markdown View Source

Durable audit envelope for a bulk outbound action (D-13, OBS-02-shaped). Snapshots template_id, rendered_body, and recipient cohort at confirmation time. Per-recipient delivery flows through Outbound.trigger/2 (sealed) carrying the envelope id. Refused attempts persist with status: :refused_cap_exceeded.

Columns

  • :id — UUID generated by the caller at bulk_trigger/2 confirmation time (autogenerate: false — callers must supply an explicit Ecto.UUID.generate/0 value so tests and downstream correlation keys can assert on it).
  • :template_id — string id of the configured template that was used at confirmation.
  • :rendered_body — the fully rendered message body at confirmation time. Never re-rendered at worker run time (CLAUDE.md: snapshot trust facts at decision time).
  • :recipient_conversation_ids — list of :integer conversation ids the operator targeted. Persisted for audit (OBS-02); per-recipient delivery is tracked on the individual system_outbound Message rows created via the sealed Outbound.trigger/2 lane.
  • :count — denormalized cohort size (must be > 0).
  • :effective_cap — the value of max_batch_size/0 at decision time. Snapshotted on BOTH the :submitted and :refused_cap_exceeded paths so OBS-02 readers can compare count against the policy that was actually in effect when the operator confirmed (WR-05). Must be > 0.
  • :requested_by — actor string (operator, system, etc.); nullable so system-initiated bulk actions are representable.
  • :requested_at — confirmation timestamp.
  • :status:submitted (default) or :refused_cap_exceeded. A refused row records that an oversized cohort was attempted — mirrors Governance.propose_blocked's "blocked attempts persist" posture so OBS-02 reads see both lanes from one table.
  • :refused_reason — operator-facing reason string (only set when refusing).

Status enum

:submitted               fan-out enqueued (per-recipient delivery is tracked on Message rows)
:refused_cap_exceeded    cohort exceeded max_batch_size; no fan-out occurred

Validation

  • :id, :template_id, :rendered_body, :recipient_conversation_ids, :count, :effective_cap, and :requested_at are required.
  • :count must be > 0 (a zero-cohort envelope has no audit value).
  • :effective_cap must be > 0.

See priv/repo/migrations/20260527063000_add_outbound_bulk_envelopes.exs for the underlying table definition (D-15).

Summary

Functions

Builds a changeset for the bulk envelope.

Functions

changeset(envelope, attrs)

Builds a changeset for the bulk envelope.

Casts all writable fields and validates required ones (:id, :template_id, :rendered_body, :recipient_conversation_ids, :count, :effective_cap, :requested_at). Enforces count > 0 and effective_cap > 0 (zero values have no audit value).

:requested_by is intentionally optional — system-initiated bulk actions are representable with nil here. :status defaults to :submitted; setting it to :refused_cap_exceeded with a :refused_reason is also valid.