Skip to main content

backbone_outbox/
record.rs

1//! The durable event record — the wire shape carried from a producer's outbox to a consumer's inbox.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// A serialized domain event staged in the outbox and carried to consumers.
8///
9/// The `id` is the single end-to-end dedup key (the `backbone-messaging` envelope id): the producer
10/// stages it, the relay carries it, the consumer dedups on it via [`crate::inbox::once`].
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct OutboxRecord {
13    /// The event id — the dedup key. Take it from the messaging `EventEnvelope::id`.
14    pub id: Uuid,
15    /// The event type name, e.g. `"PaymentSettled"`.
16    pub event_type: String,
17    /// The aggregate type the event belongs to, e.g. `"Payment"`.
18    pub aggregate_type: String,
19    /// The aggregate id (as a string — aggregates key on uuid, code, or number).
20    pub aggregate_id: String,
21    /// The owning tenant — the outbox_events table is fenced by this (ADR-0011). Every staged event
22    /// carries the company it belongs to so the RLS policy can isolate tenant event streams.
23    pub company_id: Uuid,
24    /// The serialized domain event payload.
25    pub payload: serde_json::Value,
26    /// When the event occurred in the domain.
27    pub occurred_at: DateTime<Utc>,
28    /// Correlation id for tracing a business flow across events.
29    pub correlation_id: Option<String>,
30    /// Causation id (the parent event that caused this one).
31    pub causation_id: Option<String>,
32    /// Event schema version (see `extension-contract.md` §4).
33    pub version: i32,
34    /// The org unit the event was staged under (ADR-0029): read back from the table's
35    /// `org_unit_id` column by the relay, which the table fills from the writer's acting unit.
36    /// `None` for a record that was never read from a fenced table, or staged with no unit bound.
37    /// A consumer that writes on behalf of the event (the workflow engine) acts under it.
38    #[serde(default)]
39    pub org_unit_id: Option<Uuid>,
40}
41
42impl OutboxRecord {
43    /// Build a record with a fresh id, no correlation/causation, version 1, `occurred_at = now`.
44    /// (`now` is passed in so callers stay deterministic under test.) `company_id` is the owning
45    /// tenant — required, since the outbox_events table is fenced by it (ADR-0011).
46    pub fn new(
47        event_type: impl Into<String>,
48        aggregate_type: impl Into<String>,
49        aggregate_id: impl Into<String>,
50        company_id: Uuid,
51        payload: serde_json::Value,
52        occurred_at: DateTime<Utc>,
53    ) -> Self {
54        Self {
55            id: Uuid::new_v4(),
56            event_type: event_type.into(),
57            aggregate_type: aggregate_type.into(),
58            aggregate_id: aggregate_id.into(),
59            company_id,
60            payload,
61            occurred_at,
62            correlation_id: None,
63            causation_id: None,
64            version: 1,
65            org_unit_id: None,
66        }
67    }
68
69    /// Set the org unit the event belongs to (what the relay reads back from the table).
70    pub fn with_org_unit_id(mut self, unit: Uuid) -> Self {
71        self.org_unit_id = Some(unit);
72        self
73    }
74
75    /// Set the event id explicitly (e.g. reuse the messaging envelope id).
76    pub fn with_id(mut self, id: Uuid) -> Self {
77        self.id = id;
78        self
79    }
80
81    /// Set the correlation id.
82    pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
83        self.correlation_id = Some(id.into());
84        self
85    }
86
87    /// Set the event schema version (a declared event's `version`).
88    pub fn with_version(mut self, version: i32) -> Self {
89        self.version = version;
90        self
91    }
92
93    /// Set the causation id: the id of the event, or the trigger or workflow step, whose
94    /// handling produced this one. Chains events into the cause-and-effect trail
95    /// `correlation_id` only groups.
96    pub fn with_causation_id(mut self, id: impl Into<String>) -> Self {
97        self.causation_id = Some(id.into());
98        self
99    }
100}