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}