1use crate::foundation::*;
2use crate::marketing::*;
3
4domain_struct! {
5 #[allow(clippy::struct_field_names)]
6 pub struct FraudPolicy {
7 max_coupon_uses: Nat,
8 max_orders_per_hour: Nat,
9 max_zero_total_items: Nat,
10 }
11}
12
13#[must_use]
14pub const fn coupon_uses_allowed(policy: &FraudPolicy, uses: Nat) -> bool {
15 uses <= policy.max_coupon_uses
16}
17
18#[must_use]
19pub const fn orders_per_hour_allowed(policy: &FraudPolicy, orders_per_hour: Nat) -> bool {
20 orders_per_hour <= policy.max_orders_per_hour
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum Role {
26 Customer,
27 Support,
28 Warehouse,
29 Manager,
30 Finance,
31 Admin,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum Action {
37 ViewOrder,
38 PackOrder,
39 ShipOrder,
40 IssueRefund,
41 OverridePrice,
42 AdjustStock,
43 DeleteOrder,
44 ManageCRM,
45 CreateSupportCase,
46 ResolveSupportCase,
47 ManageShipment,
48 ApproveReturn,
49}
50
51#[must_use]
52pub const fn can_perform(role: Role, action: Action) -> bool {
53 matches!(
54 (role, action),
55 (Role::Admin, _)
56 | (
57 Role::Support | Role::Manager | Role::Finance,
58 Action::ViewOrder
59 )
60 | (
61 Role::Warehouse,
62 Action::PackOrder
63 | Action::ShipOrder
64 | Action::AdjustStock
65 | Action::ManageShipment
66 )
67 | (
68 Role::Manager,
69 Action::OverridePrice
70 | Action::ManageCRM
71 | Action::ResolveSupportCase
72 | Action::ApproveReturn
73 )
74 | (
75 Role::Support,
76 Action::CreateSupportCase | Action::ResolveSupportCase | Action::ManageCRM
77 )
78 | (Role::Finance, Action::IssueRefund | Action::ApproveReturn)
79 )
80}
81
82domain_struct! {
83 pub struct AuditEvent {
84 actor: Role,
85 action: Action,
86 order_id: OrderId,
87 }
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct AuditedCommand {
93 pub(crate) actor: Role,
94 pub(crate) action: Action,
95 pub(crate) order_id: OrderId,
96 pub(crate) event: AuditEvent,
97}
98
99domain_struct! {
100 pub struct EntityAuditEvent {
101 actor: Role,
102 action: Action,
103 subject_id: Id,
104 }
105}
106
107#[derive(Clone, Debug, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct AuditedEntityCommand {
110 pub(crate) actor: Role,
111 pub(crate) action: Action,
112 pub(crate) subject_id: Id,
113 pub(crate) event: EntityAuditEvent,
114}
115
116impl AuditedEntityCommand {
117 pub fn try_new(
118 actor: Role,
119 action: Action,
120 subject_id: Id,
121 event: EntityAuditEvent,
122 ) -> DomainResult<Self> {
123 if !can_perform(actor, action) {
124 return Err(ValidationError::AuditPermissionDenied);
125 }
126 if event.actor != actor || event.action != action || event.subject_id != subject_id {
127 return Err(ValidationError::Invariant(
128 "entity audit event does not match command",
129 ));
130 }
131 Ok(Self {
132 actor,
133 action,
134 subject_id,
135 event,
136 })
137 }
138}
139
140impl AuditedCommand {
141 pub fn try_new(
142 actor: Role,
143 action: Action,
144 order_id: OrderId,
145 event: AuditEvent,
146 ) -> DomainResult<Self> {
147 if !can_perform(actor, action) {
148 return Err(ValidationError::Invariant("actor cannot perform action"));
149 }
150 if event.actor != actor || event.action != action || event.order_id != order_id {
151 return Err(ValidationError::Invariant(
152 "audit event does not match command",
153 ));
154 }
155 Ok(Self {
156 actor,
157 action,
158 order_id,
159 event,
160 })
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub enum ConsentPurpose {
167 Marketing,
168 Analytics,
169 Personalization,
170 FraudPrevention,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175pub enum ProcessingBasis {
176 Consent,
177 Contract,
178 LegalObligation,
179 LegitimateInterest,
180}
181
182domain_struct! {
183 pub struct DataProcessingPermission {
184 purpose: ConsentPurpose,
185 basis: ProcessingBasis,
186 allowed: bool,
187 }
188}
189
190#[must_use]
191pub const fn data_processing_allowed(permission: &DataProcessingPermission) -> bool {
192 permission.allowed
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub enum DataCategory {
198 CustomerProfile,
199 ContactData,
200 OrderData,
201 PaymentToken,
202 MarketingProfile,
203 SupportNotes,
204 AnalyticsEvent,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub enum AccessPurpose {
210 CustomerSupport,
211 Fulfillment,
212 RefundProcessing,
213 MarketingOperations,
214 FraudReview,
215 Analytics,
216 Administration,
217}
218
219#[must_use]
220pub const fn role_can_access_data(
221 role: Role,
222 purpose: AccessPurpose,
223 category: DataCategory,
224) -> bool {
225 matches!(
226 (role, purpose, category),
227 (Role::Admin, _, _)
228 | (
229 Role::Support,
230 AccessPurpose::CustomerSupport,
231 DataCategory::OrderData | DataCategory::ContactData | DataCategory::SupportNotes
232 )
233 | (
234 Role::Warehouse,
235 AccessPurpose::Fulfillment,
236 DataCategory::OrderData | DataCategory::ContactData
237 )
238 | (
239 Role::Finance,
240 AccessPurpose::RefundProcessing,
241 DataCategory::OrderData | DataCategory::PaymentToken
242 )
243 | (
244 Role::Manager,
245 AccessPurpose::MarketingOperations | AccessPurpose::Administration,
246 DataCategory::MarketingProfile
247 )
248 | (
249 Role::Manager,
250 AccessPurpose::MarketingOperations,
251 DataCategory::ContactData
252 )
253 | (
254 Role::Manager,
255 AccessPurpose::Administration,
256 DataCategory::CustomerProfile
257 )
258 )
259}
260
261#[must_use]
262pub fn processing_allowed_for(
263 permission: &DataProcessingPermission,
264 purpose: ConsentPurpose,
265 basis: ProcessingBasis,
266) -> bool {
267 data_processing_allowed(permission)
268 && permission.purpose == purpose
269 && permission.basis == basis
270}
271
272domain_struct! {
273 pub struct MarketingConsentState {
274 subscription: SubscriptionStatus,
275 retargeting_consent: ConsentStatus,
276 data_permission: DataProcessingPermission,
277 }
278}
279
280#[must_use]
281pub fn marketing_allowed(state: &MarketingConsentState) -> bool {
282 can_send_marketing_message(state.subscription)
283 && can_retarget(state.retargeting_consent)
284 && processing_allowed_for(
285 &state.data_permission,
286 ConsentPurpose::Marketing,
287 ProcessingBasis::Consent,
288 )
289}
290
291#[must_use]
292pub fn withdraw_marketing_consent(state: &MarketingConsentState) -> MarketingConsentState {
293 MarketingConsentState::new(
294 SubscriptionStatus::Unsubscribed,
295 ConsentStatus::Denied,
296 DataProcessingPermission::new(
297 state.data_permission.purpose(),
298 state.data_permission.basis(),
299 false,
300 ),
301 )
302}
303
304domain_struct! {
305 pub struct DataRetentionPolicy {
306 category: DataCategory,
307 retention_window: Duration,
308 }
309}
310
311#[must_use]
312pub fn within_retention_window(
313 policy: &DataRetentionPolicy,
314 now: Timestamp,
315 collected_at: Timestamp,
316) -> bool {
317 collected_at <= now && timestamp_age(now, collected_at) <= policy.retention_window
318}
319
320#[must_use]
321pub fn retention_expired(
322 policy: &DataRetentionPolicy,
323 now: Timestamp,
324 collected_at: Timestamp,
325) -> bool {
326 collected_at <= now && policy.retention_window < timestamp_age(now, collected_at)
327}
328
329#[must_use]
330pub fn can_retain_personal_data(
331 policy: &DataRetentionPolicy,
332 now: Timestamp,
333 collected_at: Timestamp,
334) -> bool {
335 within_retention_window(policy, now, collected_at)
336}
337
338#[derive(Clone, Debug, PartialEq, Eq)]
339#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
340pub struct RetainedPersonalData {
341 pub(crate) subject_id: CustomerId,
342 pub(crate) category: DataCategory,
343 pub(crate) collected_at: Timestamp,
344 pub(crate) checked_at: Timestamp,
345 pub(crate) policy: DataRetentionPolicy,
346}
347
348impl RetainedPersonalData {
349 pub fn try_new(
350 subject_id: CustomerId,
351 category: DataCategory,
352 collected_at: Timestamp,
353 checked_at: Timestamp,
354 policy: DataRetentionPolicy,
355 ) -> DomainResult<Self> {
356 if policy.category != category
357 || !can_retain_personal_data(&policy, checked_at, collected_at)
358 {
359 return Err(ValidationError::Invariant(
360 "personal data cannot be retained",
361 ));
362 }
363 Ok(Self {
364 subject_id,
365 category,
366 collected_at,
367 checked_at,
368 policy,
369 })
370 }
371}
372
373#[derive(Clone, Copy, Debug, PartialEq, Eq)]
374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
375pub enum ErasureStatus {
376 Active,
377 Requested,
378 Completed,
379 BlockedByLegalHold,
380}
381
382#[must_use]
383pub fn personal_data_usable(status: ErasureStatus) -> bool {
384 status == ErasureStatus::Active
385}
386
387#[must_use]
388pub fn can_process_personal_data(
389 status: ErasureStatus,
390 permission: &DataProcessingPermission,
391 purpose: ConsentPurpose,
392 basis: ProcessingBasis,
393) -> bool {
394 personal_data_usable(status) && processing_allowed_for(permission, purpose, basis)
395}
396
397#[must_use]
398pub fn can_complete_erasure(status: ErasureStatus, legal_hold: bool) -> bool {
399 status == ErasureStatus::Requested && !legal_hold
400}
401
402#[must_use]
403pub fn audit_log_appended(
404 before: &[EntityAuditEvent],
405 after: &[EntityAuditEvent],
406 new_events: &[EntityAuditEvent],
407) -> bool {
408 after.len() == before.len() + new_events.len()
409 && after.starts_with(before)
410 && after[before.len()..] == new_events[..]
411}
412
413#[derive(Clone, Debug, PartialEq, Eq)]
414#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
415pub struct AuditedDataAccess {
416 pub(crate) actor: Role,
417 pub(crate) action: Action,
418 pub(crate) purpose: AccessPurpose,
419 pub(crate) category: DataCategory,
420 pub(crate) subject_id: Id,
421 pub(crate) event: EntityAuditEvent,
422}
423
424impl AuditedDataAccess {
425 pub fn try_new(
426 actor: Role,
427 action: Action,
428 purpose: AccessPurpose,
429 category: DataCategory,
430 subject_id: Id,
431 event: EntityAuditEvent,
432 ) -> DomainResult<Self> {
433 if !can_perform(actor, action) || !role_can_access_data(actor, purpose, category) {
434 return Err(ValidationError::AuditPermissionDenied);
435 }
436 if event.actor != actor || event.action != action || event.subject_id != subject_id {
437 return Err(ValidationError::Invariant(
438 "data-access audit event does not match access",
439 ));
440 }
441 Ok(Self {
442 actor,
443 action,
444 purpose,
445 category,
446 subject_id,
447 event,
448 })
449 }
450}
451
452impl_getters!(AuditedCommand {
453 actor: Role,
454 action: Action,
455 order_id: OrderId,
456 event: AuditEvent,
457});
458
459impl_getters!(AuditedEntityCommand {
460 actor: Role,
461 action: Action,
462 subject_id: Id,
463 event: EntityAuditEvent,
464});
465
466impl_getters!(RetainedPersonalData {
467 subject_id: CustomerId,
468 category: DataCategory,
469 collected_at: Timestamp,
470 checked_at: Timestamp,
471 policy: DataRetentionPolicy,
472});
473
474impl_getters!(AuditedDataAccess {
475 actor: Role,
476 action: Action,
477 purpose: AccessPurpose,
478 category: DataCategory,
479 subject_id: Id,
480 event: EntityAuditEvent,
481});