1use crate::b2b::*;
2use crate::foundation::*;
3use crate::marketing::*;
4use crate::pricing::*;
5use crate::risk_privacy::*;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum AccountTier {
10 Standard,
11 Preferred,
12 Strategic,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub enum CRMAccountStatus {
18 Prospect,
19 Active,
20 Paused,
21 Closed,
22}
23
24#[must_use]
25pub const fn can_crm_account_transition(
26 source: CRMAccountStatus,
27 target: CRMAccountStatus,
28) -> bool {
29 matches!(
30 (source, target),
31 (
32 CRMAccountStatus::Prospect | CRMAccountStatus::Paused,
33 CRMAccountStatus::Active
34 ) | (
35 CRMAccountStatus::Prospect | CRMAccountStatus::Active | CRMAccountStatus::Paused,
36 CRMAccountStatus::Closed
37 ) | (CRMAccountStatus::Active, CRMAccountStatus::Paused)
38 )
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct CRMAccount {
44 pub(crate) id: AccountId,
45 pub(crate) customer: Customer,
46 pub(crate) tier: AccountTier,
47 pub(crate) status: CRMAccountStatus,
48 pub(crate) lifetime_value: Money,
49 pub(crate) open_balance: Money,
50}
51
52impl CRMAccount {
53 pub const fn try_new(
54 id: AccountId,
55 customer: Customer,
56 tier: AccountTier,
57 status: CRMAccountStatus,
58 lifetime_value: Money,
59 open_balance: Money,
60 ) -> DomainResult<Self> {
61 if open_balance > lifetime_value {
62 return Err(ValidationError::CrmInvariantFailed);
63 }
64 Ok(Self {
65 id,
66 customer,
67 tier,
68 status,
69 lifetime_value,
70 open_balance,
71 })
72 }
73}
74
75#[must_use]
76pub fn crm_account_active(account: &CRMAccount) -> bool {
77 account.status == CRMAccountStatus::Active
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct ActiveCRMAccount {
83 pub(crate) account: CRMAccount,
84}
85
86impl ActiveCRMAccount {
87 pub fn try_new(account: CRMAccount) -> DomainResult<Self> {
88 if !crm_account_active(&account) {
89 return Err(ValidationError::CrmInvariantFailed);
90 }
91 Ok(Self { account })
92 }
93}
94
95pub const fn transition_crm_account(
96 account: CRMAccount,
97 next: CRMAccountStatus,
98) -> DomainResult<CRMAccount> {
99 if !can_crm_account_transition(account.status, next) {
100 return Err(ValidationError::CrmInvariantFailed);
101 }
102 Ok(CRMAccount {
103 status: next,
104 ..account
105 })
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum ContactKind {
111 Primary,
112 Billing,
113 Shipping,
114 Buyer,
115 Support,
116}
117
118domain_struct! {
119 pub struct CRMContact {
120 id: ContactId,
121 account_id: AccountId,
122 customer_id: CustomerId,
123 kind: ContactKind,
124 owner_role: Role,
125 subscription: SubscriptionStatus,
126 retargeting_consent: ConsentStatus,
127 data_permission: DataProcessingPermission,
128 }
129}
130
131#[must_use]
132pub fn contact_can_receive_marketing(contact: &CRMContact) -> bool {
133 can_send_marketing_message(contact.subscription)
134 && can_retarget(contact.retargeting_consent)
135 && data_processing_allowed(&contact.data_permission)
136 && contact.data_permission.purpose() == ConsentPurpose::Marketing
137 && contact.data_permission.basis() == ProcessingBasis::Consent
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142pub struct CRMAccountContact {
143 pub(crate) account: CRMAccount,
144 pub(crate) contact: CRMContact,
145}
146
147impl CRMAccountContact {
148 pub fn try_new(account: CRMAccount, contact: CRMContact) -> DomainResult<Self> {
149 if contact.account_id != account.id || contact.customer_id != account.customer.id {
150 return Err(ValidationError::CrmInvariantFailed);
151 }
152 Ok(Self { account, contact })
153 }
154}
155
156#[derive(Clone, Debug, PartialEq, Eq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub struct PermittedCustomerMessage {
159 pub(crate) interaction_id: InteractionId,
160 pub(crate) contact: CRMContact,
161 pub(crate) sent_at: Timestamp,
162}
163
164impl PermittedCustomerMessage {
165 pub fn try_new(
166 interaction_id: InteractionId,
167 contact: CRMContact,
168 sent_at: Timestamp,
169 ) -> DomainResult<Self> {
170 if !contact_can_receive_marketing(&contact) {
171 return Err(ValidationError::CrmInvariantFailed);
172 }
173 Ok(Self {
174 interaction_id,
175 contact,
176 sent_at,
177 })
178 }
179}
180
181#[derive(Clone, Debug, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct PermittedAccountMessage {
184 pub(crate) account_contact: CRMAccountContact,
185 pub(crate) message: PermittedCustomerMessage,
186}
187
188impl PermittedAccountMessage {
189 pub fn try_new(
190 account_contact: CRMAccountContact,
191 message: PermittedCustomerMessage,
192 ) -> DomainResult<Self> {
193 if message.contact != account_contact.contact {
194 return Err(ValidationError::CrmInvariantFailed);
195 }
196 Ok(Self {
197 account_contact,
198 message,
199 })
200 }
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
205pub enum InteractionKind {
206 Email,
207 Call,
208 Meeting,
209 Chat,
210 SupportNote,
211 OrderNote,
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216pub struct CRMInteraction {
217 pub(crate) id: InteractionId,
218 pub(crate) account_id: AccountId,
219 pub(crate) contact_id: ContactId,
220 pub(crate) kind: InteractionKind,
221 pub(crate) occurred_at: Timestamp,
222 pub(crate) follow_up_due_at: Timestamp,
223}
224
225impl CRMInteraction {
226 pub fn try_new(
227 id: InteractionId,
228 account_id: AccountId,
229 contact_id: ContactId,
230 kind: InteractionKind,
231 occurred_at: Timestamp,
232 follow_up_due_at: Timestamp,
233 ) -> DomainResult<Self> {
234 if follow_up_due_at < occurred_at {
235 return Err(ValidationError::CrmInvariantFailed);
236 }
237 Ok(Self {
238 id,
239 account_id,
240 contact_id,
241 kind,
242 occurred_at,
243 follow_up_due_at,
244 })
245 }
246}
247
248#[derive(Clone, Debug, PartialEq, Eq)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250pub struct CRMInteractionForContact {
251 pub(crate) account_contact: CRMAccountContact,
252 pub(crate) interaction: CRMInteraction,
253}
254
255impl CRMInteractionForContact {
256 pub fn try_new(
257 account_contact: CRMAccountContact,
258 interaction: CRMInteraction,
259 ) -> DomainResult<Self> {
260 if interaction.account_id != account_contact.account.id
261 || interaction.contact_id != account_contact.contact.id
262 {
263 return Err(ValidationError::CrmInvariantFailed);
264 }
265 Ok(Self {
266 account_contact,
267 interaction,
268 })
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub enum LeadStatus {
275 New,
276 Working,
277 Qualified,
278 Disqualified,
279 Converted,
280}
281
282#[must_use]
283pub const fn can_lead_transition(source: LeadStatus, target: LeadStatus) -> bool {
284 matches!(
285 (source, target),
286 (
287 LeadStatus::New,
288 LeadStatus::Working | LeadStatus::Disqualified
289 ) | (
290 LeadStatus::Working,
291 LeadStatus::Qualified | LeadStatus::Disqualified
292 ) | (
293 LeadStatus::Qualified,
294 LeadStatus::Converted | LeadStatus::Disqualified
295 )
296 )
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
301pub struct Lead {
302 pub(crate) id: LeadId,
303 pub(crate) account_id: AccountId,
304 pub(crate) contact_id: ContactId,
305 pub(crate) source_campaign: Option<CampaignId>,
306 pub(crate) status: LeadStatus,
307 pub(crate) estimated_value: Money,
308 pub(crate) currency: Currency,
309 pub(crate) created_at: Timestamp,
310 pub(crate) updated_at: Timestamp,
311}
312
313impl Lead {
314 #[allow(clippy::too_many_arguments)]
315 pub fn try_new(
316 id: LeadId,
317 account_id: AccountId,
318 contact_id: ContactId,
319 source_campaign: Option<CampaignId>,
320 status: LeadStatus,
321 estimated_value: Money,
322 currency: Currency,
323 created_at: Timestamp,
324 updated_at: Timestamp,
325 ) -> DomainResult<Self> {
326 if updated_at < created_at {
327 return Err(ValidationError::CrmInvariantFailed);
328 }
329 Ok(Self {
330 id,
331 account_id,
332 contact_id,
333 source_campaign,
334 status,
335 estimated_value,
336 currency,
337 created_at,
338 updated_at,
339 })
340 }
341}
342
343pub fn transition_lead(lead: Lead, next: LeadStatus, updated_at: Timestamp) -> DomainResult<Lead> {
344 if !can_lead_transition(lead.status, next) || updated_at < lead.created_at {
345 return Err(ValidationError::CrmInvariantFailed);
346 }
347 Ok(Lead {
348 status: next,
349 updated_at,
350 ..lead
351 })
352}
353
354#[derive(Clone, Debug, PartialEq, Eq)]
355#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
356pub struct LeadForContact {
357 pub(crate) account_contact: CRMAccountContact,
358 pub(crate) lead: Lead,
359}
360
361impl LeadForContact {
362 pub fn try_new(account_contact: CRMAccountContact, lead: Lead) -> DomainResult<Self> {
363 if lead.account_id != account_contact.account.id
364 || lead.contact_id != account_contact.contact.id
365 {
366 return Err(ValidationError::CrmInvariantFailed);
367 }
368 Ok(Self {
369 account_contact,
370 lead,
371 })
372 }
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
377pub enum OpportunityStage {
378 Prospecting,
379 Qualified,
380 Proposal,
381 Negotiation,
382 Won,
383 Lost,
384}
385
386#[must_use]
387pub const fn can_opportunity_transition(
388 source: OpportunityStage,
389 target: OpportunityStage,
390) -> bool {
391 matches!(
392 (source, target),
393 (
394 OpportunityStage::Prospecting,
395 OpportunityStage::Qualified | OpportunityStage::Lost
396 ) | (
397 OpportunityStage::Qualified,
398 OpportunityStage::Proposal | OpportunityStage::Lost
399 ) | (
400 OpportunityStage::Proposal,
401 OpportunityStage::Negotiation | OpportunityStage::Lost
402 ) | (
403 OpportunityStage::Negotiation,
404 OpportunityStage::Won | OpportunityStage::Lost
405 )
406 )
407}
408
409#[must_use]
410pub const fn opportunity_stage_probability_allowed(
411 stage: OpportunityStage,
412 probability: BasisPoints,
413) -> bool {
414 match stage {
415 OpportunityStage::Won => probability.value() == 10_000,
416 OpportunityStage::Lost => probability.value() == 0,
417 _ => true,
418 }
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
423pub struct SalesOpportunity {
424 pub(crate) id: OpportunityId,
425 pub(crate) account_id: AccountId,
426 pub(crate) contact_id: ContactId,
427 pub(crate) source_lead: Option<LeadId>,
428 pub(crate) stage: OpportunityStage,
429 pub(crate) amount: Money,
430 pub(crate) currency: Currency,
431 pub(crate) probability: BasisPoints,
432 pub(crate) opened_at: Timestamp,
433 pub(crate) updated_at: Timestamp,
434 pub(crate) expected_close_at: Timestamp,
435}
436
437impl SalesOpportunity {
438 #[allow(clippy::too_many_arguments)]
439 pub fn try_new(
440 id: OpportunityId,
441 account_id: AccountId,
442 contact_id: ContactId,
443 source_lead: Option<LeadId>,
444 stage: OpportunityStage,
445 amount: Money,
446 currency: Currency,
447 probability: BasisPoints,
448 opened_at: Timestamp,
449 updated_at: Timestamp,
450 expected_close_at: Timestamp,
451 ) -> DomainResult<Self> {
452 if updated_at < opened_at
453 || expected_close_at < opened_at
454 || !opportunity_stage_probability_allowed(stage, probability)
455 {
456 return Err(ValidationError::CrmInvariantFailed);
457 }
458 Ok(Self {
459 id,
460 account_id,
461 contact_id,
462 source_lead,
463 stage,
464 amount,
465 currency,
466 probability,
467 opened_at,
468 updated_at,
469 expected_close_at,
470 })
471 }
472}
473
474pub fn opportunity_weighted_value(opportunity: &SalesOpportunity) -> DomainResult<Money> {
475 apply_bps(opportunity.probability, opportunity.amount)
476}
477
478pub fn transition_opportunity(
479 opportunity: SalesOpportunity,
480 next: OpportunityStage,
481 probability: BasisPoints,
482 updated_at: Timestamp,
483 expected_close_at: Timestamp,
484) -> DomainResult<SalesOpportunity> {
485 if !can_opportunity_transition(opportunity.stage, next)
486 || !opportunity_stage_probability_allowed(next, probability)
487 || updated_at < opportunity.opened_at
488 || expected_close_at < opportunity.opened_at
489 {
490 return Err(ValidationError::CrmInvariantFailed);
491 }
492 Ok(SalesOpportunity {
493 stage: next,
494 probability,
495 updated_at,
496 expected_close_at,
497 ..opportunity
498 })
499}
500
501#[derive(Clone, Debug, PartialEq, Eq)]
502#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
503pub struct OpportunityForContact {
504 pub(crate) account_contact: CRMAccountContact,
505 pub(crate) opportunity: SalesOpportunity,
506}
507
508impl OpportunityForContact {
509 pub fn try_new(
510 account_contact: CRMAccountContact,
511 opportunity: SalesOpportunity,
512 ) -> DomainResult<Self> {
513 if opportunity.account_id != account_contact.account.id
514 || opportunity.contact_id != account_contact.contact.id
515 {
516 return Err(ValidationError::CrmInvariantFailed);
517 }
518 Ok(Self {
519 account_contact,
520 opportunity,
521 })
522 }
523}
524
525pub fn opportunity_gross_value(opportunities: &[SalesOpportunity]) -> DomainResult<Money> {
526 checked_sum(
527 opportunities.iter().map(|opportunity| opportunity.amount),
528 "opportunity_gross_value",
529 )
530}
531
532pub fn opportunity_weighted_value_total(opportunities: &[SalesOpportunity]) -> DomainResult<Money> {
533 checked_result_sum(
534 opportunities.iter().map(opportunity_weighted_value),
535 "opportunity_weighted_value_total",
536 )
537}
538
539#[must_use]
540pub fn opportunities_use_currency(currency: Currency, opportunities: &[SalesOpportunity]) -> bool {
541 opportunities
542 .iter()
543 .all(|opportunity| opportunity.currency == currency)
544}
545
546#[derive(Clone, Debug, PartialEq, Eq)]
547#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
548pub struct SalesPipeline {
549 pub(crate) currency: Currency,
550 pub(crate) opportunities: Vec<SalesOpportunity>,
551}
552
553impl SalesPipeline {
554 pub fn try_new(currency: Currency, opportunities: Vec<SalesOpportunity>) -> DomainResult<Self> {
555 if !opportunities_use_currency(currency, &opportunities) {
556 return Err(ValidationError::CrmInvariantFailed);
557 }
558 Ok(Self {
559 currency,
560 opportunities,
561 })
562 }
563}
564
565#[derive(Clone, Debug, PartialEq, Eq)]
566#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
567pub struct CustomerSegment {
568 pub(crate) id: SegmentId,
569 pub(crate) name: String,
570 pub(crate) member_count: Nat,
571 pub(crate) min_lifetime_value: Money,
572 pub(crate) max_retention_discount: Money,
573}
574
575impl CustomerSegment {
576 pub fn try_new(
577 id: SegmentId,
578 name: String,
579 member_count: Nat,
580 min_lifetime_value: Money,
581 max_retention_discount: Money,
582 ) -> DomainResult<Self> {
583 if max_retention_discount > min_lifetime_value {
584 return Err(ValidationError::CrmInvariantFailed);
585 }
586 Ok(Self {
587 id,
588 name,
589 member_count,
590 min_lifetime_value,
591 max_retention_discount,
592 })
593 }
594}
595
596#[derive(Clone, Debug, PartialEq, Eq)]
597#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
598pub struct SegmentMembership {
599 pub(crate) account: CRMAccount,
600 pub(crate) segment: CustomerSegment,
601}
602
603impl SegmentMembership {
604 pub fn try_new(account: CRMAccount, segment: CustomerSegment) -> DomainResult<Self> {
605 if account.lifetime_value < segment.min_lifetime_value {
606 return Err(ValidationError::CrmInvariantFailed);
607 }
608 Ok(Self { account, segment })
609 }
610}
611
612#[derive(Clone, Copy, Debug, PartialEq, Eq)]
613#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
614pub enum SupportPriority {
615 Low,
616 Normal,
617 High,
618 Urgent,
619}
620
621#[derive(Clone, Copy, Debug, PartialEq, Eq)]
622#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
623pub enum SupportCaseStatus {
624 Opened,
625 WaitingOnCustomer,
626 WaitingOnInternal,
627 Escalated,
628 Resolved,
629 Closed,
630}
631
632#[must_use]
633pub const fn can_support_case_transition(
634 source: SupportCaseStatus,
635 target: SupportCaseStatus,
636) -> bool {
637 matches!(
638 (source, target),
639 (
640 SupportCaseStatus::Opened,
641 SupportCaseStatus::WaitingOnCustomer
642 | SupportCaseStatus::WaitingOnInternal
643 | SupportCaseStatus::Escalated
644 | SupportCaseStatus::Resolved
645 ) | (
646 SupportCaseStatus::WaitingOnCustomer
647 | SupportCaseStatus::WaitingOnInternal
648 | SupportCaseStatus::Escalated,
649 SupportCaseStatus::Resolved
650 ) | (
651 SupportCaseStatus::WaitingOnCustomer | SupportCaseStatus::WaitingOnInternal,
652 SupportCaseStatus::Escalated
653 ) | (SupportCaseStatus::Resolved, SupportCaseStatus::Closed)
654 )
655}
656
657#[derive(Clone, Copy, Debug, PartialEq, Eq)]
658#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
659pub struct SupportCase {
660 pub(crate) id: SupportCaseId,
661 pub(crate) account_id: AccountId,
662 pub(crate) contact_id: ContactId,
663 pub(crate) order_id: Option<OrderId>,
664 pub(crate) status: SupportCaseStatus,
665 pub(crate) priority: SupportPriority,
666 pub(crate) opened_at: Timestamp,
667 pub(crate) last_updated_at: Timestamp,
668 pub(crate) sla_due_at: Timestamp,
669}
670
671impl SupportCase {
672 #[allow(clippy::too_many_arguments)]
673 pub fn try_new(
674 id: SupportCaseId,
675 account_id: AccountId,
676 contact_id: ContactId,
677 order_id: Option<OrderId>,
678 status: SupportCaseStatus,
679 priority: SupportPriority,
680 opened_at: Timestamp,
681 last_updated_at: Timestamp,
682 sla_due_at: Timestamp,
683 ) -> DomainResult<Self> {
684 if last_updated_at < opened_at || sla_due_at < opened_at {
685 return Err(ValidationError::CrmInvariantFailed);
686 }
687 Ok(Self {
688 id,
689 account_id,
690 contact_id,
691 order_id,
692 status,
693 priority,
694 opened_at,
695 last_updated_at,
696 sla_due_at,
697 })
698 }
699}
700
701pub fn transition_support_case(
702 case_: SupportCase,
703 next: SupportCaseStatus,
704 updated_at: Timestamp,
705) -> DomainResult<SupportCase> {
706 if !can_support_case_transition(case_.status, next) || updated_at < case_.opened_at {
707 return Err(ValidationError::CrmInvariantFailed);
708 }
709 Ok(SupportCase {
710 status: next,
711 last_updated_at: updated_at,
712 ..case_
713 })
714}
715
716#[derive(Clone, Debug, PartialEq, Eq)]
717#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
718pub struct SupportCaseForContact {
719 pub(crate) account_contact: CRMAccountContact,
720 pub(crate) case_: SupportCase,
721}
722
723impl SupportCaseForContact {
724 pub fn try_new(account_contact: CRMAccountContact, case_: SupportCase) -> DomainResult<Self> {
725 if case_.account_id != account_contact.account.id
726 || case_.contact_id != account_contact.contact.id
727 {
728 return Err(ValidationError::CrmInvariantFailed);
729 }
730 Ok(Self {
731 account_contact,
732 case_,
733 })
734 }
735}
736
737#[derive(Clone, Debug, PartialEq, Eq)]
738#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
739pub struct ResolvedSupportCase {
740 pub(crate) case_: SupportCase,
741 pub(crate) resolved_at: Timestamp,
742}
743
744impl ResolvedSupportCase {
745 pub fn try_new(case_: SupportCase, resolved_at: Timestamp) -> DomainResult<Self> {
746 if case_.status != SupportCaseStatus::Resolved
747 || resolved_at < case_.opened_at
748 || resolved_at < case_.last_updated_at
749 || resolved_at > case_.sla_due_at
750 {
751 return Err(ValidationError::CrmInvariantFailed);
752 }
753 Ok(Self { case_, resolved_at })
754 }
755}
756
757#[derive(Clone, Debug, PartialEq, Eq)]
758#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
759pub struct RetentionOffer {
760 pub(crate) account: CRMAccount,
761 pub(crate) segment: CustomerSegment,
762 pub(crate) coupon: Coupon,
763 pub(crate) uses_before: Nat,
764 pub(crate) discount: Money,
765}
766
767impl RetentionOffer {
768 pub fn try_new(
769 account: CRMAccount,
770 segment: CustomerSegment,
771 coupon: Coupon,
772 uses_before: Nat,
773 discount: Money,
774 ) -> DomainResult<Self> {
775 if !crm_account_active(&account)
776 || !coupon_can_be_applied(&coupon, account.lifetime_value, uses_before)
777 || account.lifetime_value < segment.min_lifetime_value
778 || discount > coupon.amount()
779 || coupon.amount() > account.lifetime_value
780 || discount > segment.max_retention_discount
781 {
782 return Err(ValidationError::CrmInvariantFailed);
783 }
784 Ok(Self {
785 account,
786 segment,
787 coupon,
788 uses_before,
789 discount,
790 })
791 }
792}
793
794impl_getters!(CRMAccount {
795 id: AccountId,
796 customer: Customer,
797 tier: AccountTier,
798 status: CRMAccountStatus,
799 lifetime_value: Money,
800 open_balance: Money,
801});
802
803impl_getters!(ActiveCRMAccount {
804 account: CRMAccount
805});
806
807impl_getters!(CRMAccountContact {
808 account: CRMAccount,
809 contact: CRMContact,
810});
811
812impl_getters!(PermittedCustomerMessage {
813 interaction_id: InteractionId,
814 contact: CRMContact,
815 sent_at: Timestamp,
816});
817
818impl_getters!(PermittedAccountMessage {
819 account_contact: CRMAccountContact,
820 message: PermittedCustomerMessage,
821});
822
823impl_getters!(CRMInteraction {
824 id: InteractionId,
825 account_id: AccountId,
826 contact_id: ContactId,
827 kind: InteractionKind,
828 occurred_at: Timestamp,
829 follow_up_due_at: Timestamp,
830});
831
832impl_getters!(CRMInteractionForContact {
833 account_contact: CRMAccountContact,
834 interaction: CRMInteraction,
835});
836
837impl_getters!(Lead {
838 id: LeadId,
839 account_id: AccountId,
840 contact_id: ContactId,
841 source_campaign: Option<CampaignId>,
842 status: LeadStatus,
843 estimated_value: Money,
844 currency: Currency,
845 created_at: Timestamp,
846 updated_at: Timestamp,
847});
848
849impl_getters!(LeadForContact {
850 account_contact: CRMAccountContact,
851 lead: Lead,
852});
853
854impl_getters!(SalesOpportunity {
855 id: OpportunityId,
856 account_id: AccountId,
857 contact_id: ContactId,
858 source_lead: Option<LeadId>,
859 stage: OpportunityStage,
860 amount: Money,
861 currency: Currency,
862 probability: BasisPoints,
863 opened_at: Timestamp,
864 updated_at: Timestamp,
865 expected_close_at: Timestamp,
866});
867
868impl_getters!(OpportunityForContact {
869 account_contact: CRMAccountContact,
870 opportunity: SalesOpportunity,
871});
872
873impl_getters!(SalesPipeline {
874 currency: Currency,
875 opportunities: Vec<SalesOpportunity>,
876});
877
878impl_getters!(CustomerSegment {
879 id: SegmentId,
880 name: String,
881 member_count: Nat,
882 min_lifetime_value: Money,
883 max_retention_discount: Money,
884});
885
886impl_getters!(SegmentMembership {
887 account: CRMAccount,
888 segment: CustomerSegment,
889});
890
891impl_getters!(SupportCase {
892 id: SupportCaseId,
893 account_id: AccountId,
894 contact_id: ContactId,
895 order_id: Option<OrderId>,
896 status: SupportCaseStatus,
897 priority: SupportPriority,
898 opened_at: Timestamp,
899 last_updated_at: Timestamp,
900 sla_due_at: Timestamp,
901});
902
903impl_getters!(SupportCaseForContact {
904 account_contact: CRMAccountContact,
905 case_: SupportCase,
906});
907
908impl_getters!(ResolvedSupportCase {
909 case_: SupportCase,
910 resolved_at: Timestamp,
911});
912
913impl_getters!(RetentionOffer {
914 account: CRMAccount,
915 segment: CustomerSegment,
916 coupon: Coupon,
917 uses_before: Nat,
918 discount: Money,
919});