1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Platform constants for the messaging outbox: the fence-none sentinel company id,
//! the server-side bus channel-key constructors, and the in-tx outbox stage helper.
//!
//! Hand-authored (user-owned; declared in `metaphor.codegen.yaml`). The port of Odoo's
//! `bus` role onto `backbone-outbox` per ADR-0017 — see `docs/adr-notes/channel-key-dispatch.md`
//! and `docs/adr-notes/outbox-fence-and-credentials.md` for the two binding decisions.
use Uuid;
/// ADR-0014 posture 4 (company_fence: none): messaging has no company dimension.
/// The outbox column is NOT NULL, so platform events carry the nil sentinel.
/// Consumers key on channels, not companies — never filter these rows by company.
///
/// This is a CONSTANT, not a config knob: a configurable "default company" would
/// quietly reintroduce the synthesized fence ADR-0014 forbids (see
/// `docs/adr-notes/outbox-fence-and-credentials.md` §a).
pub const MESSAGING_PLATFORM_COMPANY_ID: Uuid = nil;
/// The outbox schema this module stages into (module/schema name: `messaging`).
pub const OUTBOX_SCHEMA: &str = "messaging";
// ============================================================================
// Channel keys (BUS-B2 / ADR-0017 decision 1)
// ============================================================================
//
// Odoo's `bus.bus.channel` was a db-scoped tuple `channel_with_db(dbname, target)`
// serialized as JSON. Under ONE database the db-scoping is obsolete, so the bare
// `"<model>_<id>"` record-channel key remains (`docs/adr-notes/channel-key-dispatch.md`).
//
// Confidentiality is channel-key CONSTRUCTION, not a constraint (BUS-B2): these
// constructors are the ONLY way channel keys come into existence — built server-side
// at stage time from server-known identity, never accepted from a client or consumer.
// There is no subscriber-writable dispatch table anywhere in the design.
/// The record channel for a chatter host: `"<res_model>_<res_id>"` — the port of
/// Odoo's `(model, id)` bus channel. Every host with chatter gets its message and
/// notification events addressed here.
/// The per-partner channel: `res.partner_<id>`. Odoo's bus topology converges
/// user + partner channels onto the partner (`res.users._bus_channel` returns
/// `self.partner_id`) — preserved: a user's inbox events are addressed to their
/// partner's channel.
/// The Discuss channel stream: `discuss.channel_<id>` (MAIL-M37).
/// The per-guest channel: `mail.guest_<id>` (MAIL-M43).
// ============================================================================
// The payload envelope (bus.bus shape, verbatim)
// ============================================================================
/// Build the `bus.bus`-shaped payload envelope every bus-derived event carries:
///
/// ```json
/// { "channel": "<channel-key>", "message": { "type": "...", "payload": { ... } } }
/// ```
///
/// - `channel` is the fully-constructed channel key (from one of the constructors
/// above — callers must NOT pass client-supplied strings).
/// - `message` is the port of `bus.bus.message`: Odoo's own `{"type", "payload"}`
/// notification wrapper preserved as-is so the 8 bus-listener host contracts
/// translate without rewriting every consumer (ADR-0017 decision 1).
// ============================================================================
// The in-tx stage helper
// ============================================================================
/// Stage a bus-derived event to the `messaging` outbox on the caller's OPEN
/// transaction, in-tx with the state change that produced it — so a crash between
/// the write and any downstream publish cannot drop the event (the durability rule
/// from the backbone-notification exemplar).
///
/// - `event_type` stays a NOMINAL type (`"MessagePosted"`, `"SmsCreated"`, ...) — it
/// says what kind of wire event this is; the channel says who it is addressed to.
/// Keeping them orthogonal preserves routing/filtering by type (ADR-0017).
/// - `company_id` on the row is ALWAYS [`MESSAGING_PLATFORM_COMPANY_ID`] — that is
/// the whole point of the sentinel, so callers cannot get it wrong.
pub async
// ============================================================================
// MAIL-B7 — the UserEmailChanged contract (increment 3)
// ============================================================================
//
// The sapiens User host (backbone-sapiens — NOT a backbone-identity module;
// that module doesn't exist) stages this event when a user's email changes.
// The CONSUMER is the composing app's relay handler: it sends the security
// warning to the PREVIOUS address through the mail queue — the whole point is
// that the actor who changed the address cannot suppress a warning to the
// address they took over. Load-bearing: never "fix" this by sending to the
// new address (port-notes MAIL-B7).
/// The outbox event name (greppable — the stager in sapiens and the app's
/// relay consumer must agree on exactly this string).
pub const USER_EMAIL_CHANGED_EVENT: &str = "UserEmailChanged";
/// The staged payload shape. `{user_id, previous_email, new_email, changed_at}`
/// — staged by the sapiens host; consumed by the app's MAIL-B7 relay handler.