Skip to main content

commerce_theory/
risk_privacy.rs

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});