1use crate::*;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub struct RawCartLine {
8 pub sku: Sku,
9 pub price: Money,
10 pub cost: Money,
11 pub quantity: Quantity,
12 pub discount: Money,
13 pub weight: Weight,
14}
15
16pub fn validate_cart_line(raw: RawCartLine) -> Result<CartLine, ValidationError> {
17 CartLine::try_new(
18 raw.sku,
19 raw.price,
20 raw.cost,
21 raw.quantity,
22 raw.discount,
23 raw.weight,
24 )
25 .map_err(|_| ValidationError::LineDiscountExceedsGross)
26}
27
28#[must_use]
29pub fn cart_line_matches_raw(raw: &RawCartLine, line: &CartLine) -> bool {
30 line.sku() == raw.sku
31 && line.price() == raw.price
32 && line.cost() == raw.cost
33 && line.quantity() == raw.quantity
34 && line.discount() == raw.discount
35 && line.weight() == raw.weight
36}
37
38pub fn validate_cart_lines(raw: Vec<RawCartLine>) -> Result<Vec<CartLine>, ValidationError> {
39 raw.into_iter().map(validate_cart_line).collect()
40}
41
42#[must_use]
43pub fn cart_lines_match_raw(raw: &[RawCartLine], lines: &[CartLine]) -> bool {
44 raw.len() == lines.len()
45 && raw
46 .iter()
47 .zip(lines)
48 .all(|(raw, line)| cart_line_matches_raw(raw, line))
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct RawOrder {
54 pub id: OrderId,
55 pub items: Vec<RawCartLine>,
56 pub coupon_amount: Money,
57 pub shipping_method: ShippingMethod,
58 pub tax: Money,
59 pub currency: Currency,
60 pub status: OrderStatus,
61 pub total: Money,
62}
63
64pub fn validate_order(raw: RawOrder) -> Result<Order, ValidationError> {
65 let items = validate_cart_lines(raw.items)?;
66 if raw.coupon_amount > cart_net_total(&items)? {
67 return Err(ValidationError::CouponExceedsSubtotal);
68 }
69 if !shipping_available(&raw.shipping_method, cart_weight_total(&items)?) {
70 return Err(ValidationError::ShippingUnavailable);
71 }
72 let expected = order_total(&raw.shipping_method, raw.coupon_amount, raw.tax, &items)?;
73 if raw.total != expected {
74 return Err(ValidationError::OrderTotalMismatch);
75 }
76 Order::try_new(
77 raw.id,
78 items,
79 raw.coupon_amount,
80 raw.shipping_method,
81 raw.tax,
82 raw.currency,
83 raw.status,
84 raw.total,
85 )
86}
87
88#[must_use]
89pub fn order_matches_raw(raw: &RawOrder, order: &Order) -> bool {
90 order.id() == raw.id
91 && cart_lines_match_raw(&raw.items, order.items())
92 && order.coupon_amount() == raw.coupon_amount
93 && order.shipping_method() == &raw.shipping_method
94 && order.tax() == raw.tax
95 && order.currency() == raw.currency
96 && order.status() == raw.status
97 && order.total() == raw.total
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct RawStockState {
103 pub sku: Sku,
104 pub total: Quantity,
105 pub reserved: Quantity,
106}
107
108pub fn validate_stock_state(raw: RawStockState) -> Result<StockState, ValidationError> {
109 StockState::try_new(raw.sku, raw.total, raw.reserved)
110 .map_err(|_| ValidationError::StockReservedExceedsTotal)
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub struct RawChannelPricePolicy {
116 pub min_price: Money,
117 pub max_price: Money,
118}
119
120pub fn validate_channel_price_policy(
121 raw: RawChannelPricePolicy,
122) -> Result<ChannelPricePolicy, ValidationError> {
123 ChannelPricePolicy::try_new(raw.min_price, raw.max_price)
124 .map_err(|_| ValidationError::PricePolicyInvalid)
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129pub struct RawProductFeedLine {
130 pub sku: Sku,
131 pub channel: SalesChannel,
132 pub price: Money,
133 pub currency: Currency,
134 pub stock: Quantity,
135 pub stock_state: RawStockState,
136 pub price_policy: RawChannelPricePolicy,
137}
138
139pub fn validate_feed_line(raw: RawProductFeedLine) -> Result<SafeProductFeedLine, ValidationError> {
140 let stock_state = validate_stock_state(raw.stock_state)?;
141 let price_policy = validate_channel_price_policy(raw.price_policy)?;
142 if raw.sku != stock_state.sku() {
143 return Err(ValidationError::FeedSkuMismatch);
144 }
145 if !valid_channel_price(&price_policy, raw.price) {
146 return Err(ValidationError::FeedPriceOutOfPolicy);
147 }
148 if raw.stock > available_stock(&stock_state) {
149 return Err(ValidationError::FeedStockUnavailable);
150 }
151 SafeProductFeedLine::try_new(
152 raw.sku,
153 raw.channel,
154 raw.price,
155 raw.currency,
156 raw.stock,
157 stock_state,
158 price_policy,
159 )
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub struct RawPaymentLedger {
165 pub captured: Money,
166 pub refunded: Money,
167}
168
169pub fn validate_payment_ledger(raw: RawPaymentLedger) -> Result<PaymentLedger, ValidationError> {
170 PaymentLedger::try_new(raw.captured, raw.refunded)
171 .map_err(|_| ValidationError::LedgerRefundedExceedsCaptured)
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub struct RawRefund {
177 pub amount: Money,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182pub struct ValidRefund {
183 pub(crate) ledger: PaymentLedger,
184 pub(crate) amount: Money,
185}
186
187impl ValidRefund {
188 #[must_use]
189 pub const fn ledger(&self) -> &PaymentLedger {
190 &self.ledger
191 }
192
193 #[must_use]
194 pub const fn amount(&self) -> Money {
195 self.amount
196 }
197}
198
199pub fn validate_refund(
200 raw: RawRefund,
201 ledger: PaymentLedger,
202) -> Result<ValidRefund, ValidationError> {
203 if !can_refund(&ledger, raw.amount) {
204 return Err(ValidationError::RefundExceedsRemaining);
205 }
206 Ok(ValidRefund {
207 ledger,
208 amount: raw.amount,
209 })
210}
211
212pub fn issue_valid_refund(refund: &ValidRefund) -> DomainResult<PaymentLedger> {
213 issue_refund(&refund.ledger, refund.amount)
214}
215
216pub fn validate_basis_points(value: Nat) -> Result<BasisPoints, ValidationError> {
217 BasisPoints::try_new(value).map_err(|_| ValidationError::BasisPointsOutOfRange)
218}
219
220pub fn validate_product_catalog_entry(
221 product: Product,
222 variant: ProductVariant,
223) -> Result<ProductCatalogEntry, ValidationError> {
224 ProductCatalogEntry::try_new(product, variant)
225 .map_err(|_| ValidationError::CatalogInvariantFailed)
226}
227
228pub fn validate_listing_content(
229 content: ListingContent,
230 policy: MarketplaceContentPolicy,
231) -> Result<ValidListingContent, ValidationError> {
232 ValidListingContent::try_new(content, policy)
233 .map_err(|_| ValidationError::CatalogInvariantFailed)
234}
235
236pub fn validate_versioned_stock(
237 raw: RawStockState,
238 version: Nat,
239) -> Result<VersionedStock, ValidationError> {
240 let stock = validate_stock_state(raw)?;
241 Ok(VersionedStock::from_stock(stock, version))
242}
243
244pub fn validate_pick_task(
245 sku: Sku,
246 requested: Quantity,
247 bin: BinStock,
248) -> Result<PickTask, ValidationError> {
249 PickTask::try_new(sku, requested, bin).map_err(|_| ValidationError::InventoryInvariantFailed)
250}
251
252pub fn validate_pack_task(
253 source_quantity: Quantity,
254 packed_quantity: Quantity,
255) -> Result<PackTask, ValidationError> {
256 PackTask::try_new(source_quantity, packed_quantity)
257 .map_err(|_| ValidationError::InventoryInvariantFailed)
258}
259
260pub fn validate_warehouse_shipment(
261 packed: Quantity,
262 shipped: Quantity,
263) -> Result<WarehouseShipment, ValidationError> {
264 WarehouseShipment::try_new(packed, shipped)
265 .map_err(|_| ValidationError::InventoryInvariantFailed)
266}
267
268pub fn validate_allocation(
269 node: InventoryNode,
270 quantity: Quantity,
271) -> Result<Allocation, ValidationError> {
272 Allocation::try_new(node, quantity).map_err(|_| ValidationError::InventoryInvariantFailed)
273}
274
275pub fn validate_fulfillment_plan(
276 requested: Quantity,
277 allocations: Vec<Allocation>,
278) -> Result<FulfillmentPlan, ValidationError> {
279 FulfillmentPlan::try_new(requested, allocations)
280 .map_err(|_| ValidationError::InventoryInvariantFailed)
281}
282
283pub fn validate_distinct_fulfillment_plan(
284 requested: Quantity,
285 allocations: Vec<Allocation>,
286) -> Result<DistinctFulfillmentPlan, ValidationError> {
287 DistinctFulfillmentPlan::try_new(requested, allocations)
288 .map_err(|_| ValidationError::InventoryInvariantFailed)
289}
290
291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
293pub struct RawReservationAttempt {
294 pub stock: RawStockState,
295 pub version: Nat,
296 pub quantity: Quantity,
297 pub expected_version: Nat,
298}
299
300pub fn validate_raw_reservation_attempt(
301 raw: RawReservationAttempt,
302) -> Result<ReservationAttempt, ValidationError> {
303 Ok(ReservationAttempt::new(
304 validate_versioned_stock(raw.stock, raw.version)?,
305 raw.quantity,
306 raw.expected_version,
307 ))
308}
309
310pub fn validate_compare_and_swap_reservation(
311 stock: VersionedStock,
312 quantity: Quantity,
313 expected_version: Nat,
314) -> Result<VersionedStock, ValidationError> {
315 compare_and_swap_reserve(&stock, quantity, expected_version)
316 .ok_or(ValidationError::InventoryInvariantFailed)
317}
318
319pub fn validate_raw_compare_and_swap_reservation(
320 raw: RawReservationAttempt,
321) -> Result<VersionedStock, ValidationError> {
322 let attempt = validate_raw_reservation_attempt(raw)?;
323 validate_compare_and_swap_reservation(
324 attempt.stock(),
325 attempt.quantity(),
326 attempt.expected_version(),
327 )
328}
329
330pub fn validate_release_reserved_stock(
331 stock: StockState,
332 quantity: Quantity,
333) -> Result<StockState, ValidationError> {
334 release_reserved_stock(&stock, quantity).map_err(|_| ValidationError::InventoryInvariantFailed)
335}
336
337pub fn validate_confirm_reserved_shipment(
338 stock: StockState,
339 quantity: Quantity,
340) -> Result<StockState, ValidationError> {
341 confirm_reserved_shipment(&stock, quantity)
342 .map_err(|_| ValidationError::InventoryInvariantFailed)
343}
344
345pub fn validate_timed_reservation(
346 stock: StockState,
347 quantity: Quantity,
348 reserved_at: Timestamp,
349 expires_at: Timestamp,
350 status: ReservationStatus,
351) -> Result<TimedReservation, ValidationError> {
352 TimedReservation::try_new(stock, quantity, reserved_at, expires_at, status)
353 .map_err(|_| ValidationError::InventoryInvariantFailed)
354}
355
356pub fn validate_release_expired_reservation(
357 reservation: TimedReservation,
358 now: Timestamp,
359) -> Result<StockState, ValidationError> {
360 release_expired_reservation(&reservation, now)
361 .map_err(|_| ValidationError::InventoryInvariantFailed)
362}
363
364pub fn validate_backorder_request(
365 sku: Sku,
366 requested: Quantity,
367 available_now: Quantity,
368 backordered: Quantity,
369) -> Result<BackorderRequest, ValidationError> {
370 BackorderRequest::try_new(sku, requested, available_now, backordered)
371 .map_err(|_| ValidationError::InventoryInvariantFailed)
372}
373
374pub fn validate_preorder_window(
375 sku: Sku,
376 opens_at: Timestamp,
377 closes_at: Timestamp,
378 capacity: Quantity,
379) -> Result<PreorderWindow, ValidationError> {
380 PreorderWindow::try_new(sku, opens_at, closes_at, capacity)
381 .map_err(|_| ValidationError::InventoryInvariantFailed)
382}
383
384pub fn validate_preorder_reservation(
385 window: PreorderWindow,
386 quantity: Quantity,
387 reserved_at: Timestamp,
388) -> Result<PreorderReservation, ValidationError> {
389 PreorderReservation::try_new(window, quantity, reserved_at)
390 .map_err(|_| ValidationError::InventoryInvariantFailed)
391}
392
393pub fn validate_serialized_inventory_set(
394 units: Vec<SerializedInventoryUnit>,
395) -> Result<SerializedInventorySet, ValidationError> {
396 SerializedInventorySet::try_new(units).map_err(|_| ValidationError::InventoryInvariantFailed)
397}
398
399pub fn validate_usable_inventory_lot(
400 lot: InventoryLot,
401 now: Timestamp,
402) -> Result<InventoryLot, ValidationError> {
403 if lot_usable_at(now, &lot) {
404 Ok(lot)
405 } else {
406 Err(ValidationError::InventoryInvariantFailed)
407 }
408}
409
410pub fn validate_sku_substitution(
411 requested_sku: Sku,
412 substitute_sku: Sku,
413 substitute_stock: StockState,
414 max_substitute_qty: Quantity,
415) -> Result<SkuSubstitution, ValidationError> {
416 SkuSubstitution::try_new(
417 requested_sku,
418 substitute_sku,
419 substitute_stock,
420 max_substitute_qty,
421 )
422 .map_err(|_| ValidationError::InventoryInvariantFailed)
423}
424
425pub fn validate_split_fulfillment_plan(
426 plan: DistinctFulfillmentPlan,
427 first_warehouse: Warehouse,
428 second_warehouse: Warehouse,
429) -> Result<SplitFulfillmentPlan, ValidationError> {
430 SplitFulfillmentPlan::try_new(plan, first_warehouse, second_warehouse)
431 .map_err(|_| ValidationError::InventoryInvariantFailed)
432}
433
434pub const fn validate_typed_order<S: OrderStatusMarker>(
435 id: OrderId,
436 total: Money,
437 currency: Currency,
438) -> Result<TypedOrder<S>, ValidationError> {
439 TypedOrder::try_new(id, total, currency)
440}
441
442pub const fn validate_typed_payment<S: PaymentStateMarker>(
443 id: PaymentId,
444 order_id: OrderId,
445 amount: Money,
446 currency: Currency,
447) -> Result<TypedPayment<S>, ValidationError> {
448 TypedPayment::try_new(id, order_id, amount, currency)
449}
450
451pub fn validate_balanced_journal_entry(
452 postings: Vec<Posting>,
453) -> Result<BalancedJournalEntry, ValidationError> {
454 BalancedJournalEntry::try_new(postings).map_err(|_| ValidationError::AccountingInvariantFailed)
455}
456
457pub fn validate_synced_marketplace_listing(
458 listing: MarketplaceListing,
459 stock: StockState,
460) -> Result<SyncedMarketplaceListing, ValidationError> {
461 SyncedMarketplaceListing::try_new(listing, stock)
462 .map_err(|_| ValidationError::MarketplaceInvariantFailed)
463}
464
465pub fn validate_marketplace_fee_ledger(
466 gross: Money,
467 fee_rate: BasisPoints,
468 fee_rounding_mode: RoundingMode,
469 fee: Money,
470 payout: Money,
471) -> Result<MarketplaceFeeLedger, ValidationError> {
472 MarketplaceFeeLedger::try_new(gross, fee_rate, fee_rounding_mode, fee, payout)
473 .map_err(|_| ValidationError::MarketplaceInvariantFailed)
474}
475
476pub fn validate_marketplace_payout_calculation(
477 gross: Money,
478 payout_rate: BasisPoints,
479 payout_rounding_mode: RoundingMode,
480 payout: Money,
481) -> Result<MarketplacePayoutCalculation, ValidationError> {
482 MarketplacePayoutCalculation::try_new(gross, payout_rate, payout_rounding_mode, payout)
483 .map_err(|_| ValidationError::MarketplaceInvariantFailed)
484}
485
486pub fn validate_marketplace_order(
487 marketplace: Marketplace,
488 external_order_id: MarketplaceOrderId,
489 internal_order: Order,
490 gross_from_marketplace: Money,
491 fee_ledger: MarketplaceFeeLedger,
492) -> Result<MarketplaceOrder, ValidationError> {
493 MarketplaceOrder::try_new(
494 marketplace,
495 external_order_id,
496 internal_order,
497 gross_from_marketplace,
498 fee_ledger,
499 )
500 .map_err(|_| ValidationError::MarketplaceInvariantFailed)
501}
502
503#[allow(clippy::too_many_arguments)]
504pub fn validate_marketing_campaign(
505 id: CampaignId,
506 platform: AdPlatform,
507 ad_type: AdType,
508 destination: AdDestination,
509 status: CampaignStatus,
510 budget: Money,
511 spend: Money,
512 impressions: Nat,
513 clicks: Nat,
514 conversions: Nat,
515 attributed_revenue: Money,
516) -> Result<MarketingCampaign, ValidationError> {
517 MarketingCampaign::try_new(
518 id,
519 platform,
520 ad_type,
521 destination,
522 status,
523 budget,
524 spend,
525 impressions,
526 clicks,
527 conversions,
528 attributed_revenue,
529 )
530 .map_err(|_| ValidationError::MarketingInvariantFailed)
531}
532
533pub fn validate_click_attributed_campaign(
534 campaign: MarketingCampaign,
535) -> Result<ClickAttributedCampaign, ValidationError> {
536 ClickAttributedCampaign::try_new(campaign)
537 .map_err(|_| ValidationError::MarketingInvariantFailed)
538}
539
540pub fn validate_funnel(
541 visitors: Nat,
542 add_to_cart: Nat,
543 checkout_started: Nat,
544 purchases: Nat,
545) -> Result<Funnel, ValidationError> {
546 Funnel::try_new(visitors, add_to_cart, checkout_started, purchases)
547 .map_err(|_| ValidationError::MarketingInvariantFailed)
548}
549
550pub fn validate_order_attribution_ledger(
551 order: Order,
552 credits: Vec<AttributionCredit>,
553) -> Result<OrderAttributionLedger, ValidationError> {
554 OrderAttributionLedger::try_new(order, credits)
555 .map_err(|_| ValidationError::MarketingInvariantFailed)
556}
557
558pub fn validate_experiment_variant(
559 id: Id,
560 traffic_weight: Nat,
561 visitors: Nat,
562 conversions: Nat,
563) -> Result<ExperimentVariant, ValidationError> {
564 ExperimentVariant::try_new(id, traffic_weight, visitors, conversions)
565 .map_err(|_| ValidationError::MarketingInvariantFailed)
566}
567
568pub fn validate_experiment(
569 id: Id,
570 variants: Vec<ExperimentVariant>,
571) -> Result<Experiment, ValidationError> {
572 Experiment::try_new(id, variants).map_err(|_| ValidationError::MarketingInvariantFailed)
573}
574
575#[allow(clippy::too_many_arguments)]
576pub fn validate_trade_price_book_entry(
577 sku: Sku,
578 currency: Currency,
579 unit_cost: Money,
580 retail_unit_price: Money,
581 wholesale_unit_price: Money,
582 retail_margin: Money,
583 wholesale_margin: Money,
584 wholesale_min_qty: Quantity,
585) -> Result<TradePriceBookEntry, ValidationError> {
586 TradePriceBookEntry::try_new(
587 sku,
588 currency,
589 unit_cost,
590 retail_unit_price,
591 wholesale_unit_price,
592 retail_margin,
593 wholesale_margin,
594 wholesale_min_qty,
595 )
596 .map_err(|_| ValidationError::B2BInvariantFailed)
597}
598
599pub fn validate_retail_line(
600 entry: TradePriceBookEntry,
601 quantity: Quantity,
602 discount: Money,
603) -> Result<RetailLine, ValidationError> {
604 RetailLine::try_new(entry, quantity, discount).map_err(|_| ValidationError::B2BInvariantFailed)
605}
606
607pub fn validate_wholesale_line(
608 entry: TradePriceBookEntry,
609 quantity: Quantity,
610 discount: Money,
611) -> Result<WholesaleLine, ValidationError> {
612 WholesaleLine::try_new(entry, quantity, discount)
613 .map_err(|_| ValidationError::B2BInvariantFailed)
614}
615
616pub fn validate_wholesale_credit_account(
617 customer: Customer,
618 credit_limit: Money,
619 outstanding: Money,
620) -> Result<WholesaleCreditAccount, ValidationError> {
621 WholesaleCreditAccount::try_new(customer, credit_limit, outstanding)
622 .map_err(|_| ValidationError::B2BInvariantFailed)
623}
624
625pub fn validate_supplier_daily_capacity(
626 supplier: DropshipSupplier,
627 daily_order_capacity: Nat,
628 accepted_orders: Nat,
629) -> Result<SupplierDailyCapacity, ValidationError> {
630 SupplierDailyCapacity::try_new(supplier, daily_order_capacity, accepted_orders)
631 .map_err(|_| ValidationError::DropshippingInvariantFailed)
632}
633
634#[allow(clippy::too_many_arguments)]
635pub fn validate_dropship_offer(
636 sku: Sku,
637 supplier: DropshipSupplier,
638 supplier_unit_cost: Money,
639 sale_unit_price: Money,
640 supplier_shipping_per_unit: Money,
641 available_qty: Quantity,
642 currency: Currency,
643 active: bool,
644) -> Result<DropshipOffer, ValidationError> {
645 DropshipOffer::try_new(
646 sku,
647 supplier,
648 supplier_unit_cost,
649 sale_unit_price,
650 supplier_shipping_per_unit,
651 available_qty,
652 currency,
653 active,
654 )
655 .map_err(|_| ValidationError::DropshippingInvariantFailed)
656}
657
658pub fn validate_supplier_reservation(
659 offer: DropshipOffer,
660 supplier: DropshipSupplier,
661 quantity: Quantity,
662 status: SupplierReservationStatus,
663) -> Result<SupplierReservation, ValidationError> {
664 SupplierReservation::try_new(offer, supplier, quantity, status)
665 .map_err(|_| ValidationError::DropshippingInvariantFailed)
666}
667
668pub fn validate_dropship_line(
669 offer: DropshipOffer,
670 quantity: Quantity,
671 discount: Money,
672) -> Result<DropshipLine, ValidationError> {
673 DropshipLine::try_new(offer, quantity, discount)
674 .map_err(|_| ValidationError::DropshippingInvariantFailed)
675}
676
677pub fn validate_reserved_dropship_line(
678 line: DropshipLine,
679 reservation: SupplierReservation,
680) -> Result<ReservedDropshipLine, ValidationError> {
681 ReservedDropshipLine::try_new(line, reservation)
682 .map_err(|_| ValidationError::DropshippingInvariantFailed)
683}
684
685pub fn validate_dropship_purchase_order(
686 supplier: DropshipSupplier,
687 lines: Vec<DropshipLine>,
688 quote: DropshipShippingQuote,
689 status: DropshipPOStatus,
690 total_supplier_cost: Money,
691) -> Result<DropshipPurchaseOrder, ValidationError> {
692 DropshipPurchaseOrder::try_new(supplier, lines, quote, status, total_supplier_cost)
693 .map_err(|_| ValidationError::DropshippingInvariantFailed)
694}
695
696pub fn validate_dropship_fulfillment(
697 customer_order: Order,
698 purchase_order: DropshipPurchaseOrder,
699 segment_revenue: Money,
700) -> Result<DropshipFulfillment, ValidationError> {
701 DropshipFulfillment::try_new(customer_order, purchase_order, segment_revenue)
702 .map_err(|_| ValidationError::DropshippingInvariantFailed)
703}
704
705pub fn validate_dropship_return_request(
706 line: DropshipLine,
707 return_qty: Quantity,
708 customer_refund: Money,
709 supplier_credit: Money,
710) -> Result<DropshipReturnRequest, ValidationError> {
711 DropshipReturnRequest::try_new(line, return_qty, customer_refund, supplier_credit)
712 .map_err(|_| ValidationError::DropshippingInvariantFailed)
713}
714
715pub fn validate_guaranteed_dropship_profit_quote(
716 revenue: Money,
717 costs: DropshipProfitCosts,
718 min_profit: Money,
719 profit: Money,
720 signed_profit: SignedMoney,
721) -> Result<GuaranteedDropshipProfitQuote, ValidationError> {
722 GuaranteedDropshipProfitQuote::try_new(revenue, costs, min_profit, profit, signed_profit)
723 .map_err(|_| ValidationError::ProfitInvariantFailed)
724}
725
726pub fn validate_dropship_cost_upper_bounds(
727 actual: DropshipProfitCosts,
728 upper: DropshipProfitCosts,
729) -> Result<DropshipCostUpperBounds, ValidationError> {
730 DropshipCostUpperBounds::try_new(actual, upper)
731 .map_err(|_| ValidationError::ProfitInvariantFailed)
732}
733
734pub fn validate_singleton_competitor_price_benchmark(
735 sku: Sku,
736 currency: Currency,
737 offer: CompetitorOffer,
738) -> Result<CompetitorPriceBenchmark, ValidationError> {
739 CompetitorPriceBenchmark::try_new(sku, currency, vec![offer.clone()], offer)
740 .map_err(|_| ValidationError::CompetitorInvariantFailed)
741}
742
743pub fn validate_competitor_aware_dropship_offer(
744 offer: DropshipOffer,
745 benchmark: CompetitorPriceBenchmark,
746 discount: Money,
747 costs: DropshipProfitCosts,
748 min_profit: Money,
749) -> Result<CompetitorAwareDropshipOffer, ValidationError> {
750 CompetitorAwareDropshipOffer::try_new(offer, benchmark, discount, costs, min_profit)
751 .map_err(|_| ValidationError::CompetitorInvariantFailed)
752}
753
754pub fn validate_brand_pricing_policy(
755 map_price: Money,
756 msrp: Money,
757) -> Result<BrandPricingPolicy, ValidationError> {
758 BrandPricingPolicy::try_new(map_price, msrp)
759 .map_err(|_| ValidationError::MerchandisingInvariantFailed)
760}
761
762pub fn validate_bundle_component(
763 sku: Sku,
764 units_per_bundle: Quantity,
765 stock_available: Quantity,
766) -> Result<BundleComponent, ValidationError> {
767 BundleComponent::try_new(sku, units_per_bundle, stock_available)
768 .map_err(|_| ValidationError::MerchandisingInvariantFailed)
769}
770
771pub fn bundle_components_can_fulfill_all(
772 bundle_qty: Quantity,
773 components: &[BundleComponent],
774) -> DomainResult<bool> {
775 for component in components {
776 if !component_can_fulfill_bundles(bundle_qty, component)? {
777 return Ok(false);
778 }
779 }
780 Ok(true)
781}
782
783pub fn validate_bundle_reservation(
784 bundle_qty: Quantity,
785 components: Vec<BundleComponent>,
786) -> Result<BundleReservation, ValidationError> {
787 if !bundle_components_can_fulfill_all(bundle_qty, &components)
788 .map_err(|_| ValidationError::MerchandisingInvariantFailed)?
789 {
790 return Err(ValidationError::MerchandisingInvariantFailed);
791 }
792 BundleReservation::try_new(bundle_qty, components)
793 .map_err(|_| ValidationError::MerchandisingInvariantFailed)
794}
795
796pub fn validate_accepted_promotion_set(
797 resulting_price: Money,
798 total_discount: Money,
799 discount_cap: Money,
800 profit_floor: Money,
801) -> Result<AcceptedPromotionSet, ValidationError> {
802 AcceptedPromotionSet::try_new(resulting_price, total_discount, discount_cap, profit_floor)
803 .map_err(|_| ValidationError::MerchandisingInvariantFailed)
804}
805
806pub fn validate_search_result_item(
807 item: SearchResultItem,
808) -> Result<ValidSearchResultItem, ValidationError> {
809 ValidSearchResultItem::try_new(item).map_err(|_| ValidationError::MerchandisingInvariantFailed)
810}
811
812pub fn validate_exchange_rate(
813 source: Currency,
814 target: Currency,
815 numerator: Nat,
816 denominator: Nat,
817 observed_at: Timestamp,
818) -> Result<ExchangeRate, ValidationError> {
819 ExchangeRate::try_new(source, target, numerator, denominator, observed_at)
820 .map_err(|_| ValidationError::FinanceInvariantFailed)
821}
822
823pub fn validate_tax_calculation(
824 taxable_amount: Money,
825 rate: TaxRate,
826 rounding_mode: RoundingMode,
827 tax: Money,
828 total: Money,
829) -> Result<TaxCalculation, ValidationError> {
830 TaxCalculation::try_new(taxable_amount, rate, rounding_mode, tax, total)
831 .map_err(|_| ValidationError::FinanceInvariantFailed)
832}
833
834pub fn validate_tax_inclusive_price(
835 gross: Money,
836 net: Money,
837 tax: Money,
838) -> Result<TaxInclusivePrice, ValidationError> {
839 TaxInclusivePrice::try_new(gross, net, tax).map_err(|_| ValidationError::TaxInvariantFailed)
840}
841
842pub fn validate_tax_exclusive_price(
843 net: Money,
844 tax: Money,
845 total: Money,
846) -> Result<TaxExclusivePrice, ValidationError> {
847 TaxExclusivePrice::try_new(net, tax, total).map_err(|_| ValidationError::TaxInvariantFailed)
848}
849
850#[derive(Clone, Debug, PartialEq, Eq)]
851#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
852pub struct RawTaxInvoiceLine {
853 pub sku: Sku,
854 pub quantity: Quantity,
855 pub unit_price: Money,
856 pub discount: Money,
857 pub treatment: TaxTreatment,
858 pub rate: TaxRate,
859 pub rounding_mode: RoundingMode,
860 pub taxable_amount: Money,
861 pub tax: Money,
862 pub total: Money,
863}
864
865pub fn validate_tax_invoice_line(
866 raw: RawTaxInvoiceLine,
867) -> Result<TaxInvoiceLine, ValidationError> {
868 TaxInvoiceLine::try_new(
869 raw.sku,
870 raw.quantity,
871 raw.unit_price,
872 raw.discount,
873 raw.treatment,
874 raw.rate,
875 raw.rounding_mode,
876 raw.taxable_amount,
877 raw.tax,
878 raw.total,
879 )
880 .map_err(|_| ValidationError::TaxInvariantFailed)
881}
882
883pub fn validate_tax_invoice_lines(
884 raw: Vec<RawTaxInvoiceLine>,
885) -> Result<Vec<TaxInvoiceLine>, ValidationError> {
886 raw.into_iter().map(validate_tax_invoice_line).collect()
887}
888
889#[derive(Clone, Debug, PartialEq, Eq)]
890#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
891pub struct RawTaxInvoice {
892 pub id: Id,
893 pub issued_at: Timestamp,
894 pub seller_id: Id,
895 pub buyer_id: CustomerId,
896 pub jurisdiction: TaxJurisdiction,
897 pub currency: Currency,
898 pub lines: Vec<RawTaxInvoiceLine>,
899 pub subtotal: Money,
900 pub tax: Money,
901 pub shipping: Money,
902 pub discount: Money,
903 pub total: Money,
904}
905
906pub fn validate_tax_invoice(raw: RawTaxInvoice) -> Result<TaxInvoice, ValidationError> {
907 let lines = validate_tax_invoice_lines(raw.lines)?;
908 TaxInvoice::try_new(
909 raw.id,
910 raw.issued_at,
911 raw.seller_id,
912 raw.buyer_id,
913 raw.jurisdiction,
914 raw.currency,
915 lines,
916 raw.subtotal,
917 raw.tax,
918 raw.shipping,
919 raw.discount,
920 raw.total,
921 )
922 .map_err(|_| ValidationError::TaxInvariantFailed)
923}
924
925pub fn validate_order_tax_invoice_link(
926 order: Order,
927 invoice: TaxInvoice,
928) -> Result<OrderTaxInvoiceLink, ValidationError> {
929 OrderTaxInvoiceLink::try_new(order, invoice).map_err(|_| ValidationError::TaxInvariantFailed)
930}
931
932pub fn validate_tax_exemption_certificate(
933 customer_id: CustomerId,
934 jurisdiction_id: Id,
935 valid_from: Timestamp,
936 valid_until: Timestamp,
937) -> Result<TaxExemptionCertificate, ValidationError> {
938 TaxExemptionCertificate::try_new(customer_id, jurisdiction_id, valid_from, valid_until)
939 .map_err(|_| ValidationError::TaxInvariantFailed)
940}
941
942pub fn validate_b2b_tax_exemption(
943 customer: Customer,
944 jurisdiction: TaxJurisdiction,
945 certificate: TaxExemptionCertificate,
946 checked_at: Timestamp,
947) -> Result<B2BTaxExemption, ValidationError> {
948 B2BTaxExemption::try_new(customer, jurisdiction, certificate, checked_at)
949 .map_err(|_| ValidationError::TaxInvariantFailed)
950}
951
952#[allow(clippy::too_many_arguments)]
953pub fn validate_marketplace_facilitator_tax(
954 marketplace: Marketplace,
955 jurisdiction: TaxJurisdiction,
956 taxable_amount: Money,
957 rate: TaxRate,
958 rounding_mode: RoundingMode,
959 tax: Money,
960 facilitator_collects: bool,
961 seller_tax_due: Money,
962) -> Result<MarketplaceFacilitatorTax, ValidationError> {
963 MarketplaceFacilitatorTax::try_new(
964 marketplace,
965 jurisdiction,
966 taxable_amount,
967 rate,
968 rounding_mode,
969 tax,
970 facilitator_collects,
971 seller_tax_due,
972 )
973 .map_err(|_| ValidationError::TaxInvariantFailed)
974}
975
976pub fn validate_carrier_quote(
977 service: CarrierService,
978 package: Package,
979 price: Money,
980) -> Result<CarrierQuote, ValidationError> {
981 CarrierQuote::try_new(service, package, price)
982 .map_err(|_| ValidationError::FinanceInvariantFailed)
983}
984
985pub fn validate_reconciliation_within_tolerance(
986 expected: Money,
987 actual: Money,
988 tolerance: Money,
989) -> Result<ReconciliationWithinTolerance, ValidationError> {
990 ReconciliationWithinTolerance::try_new(expected, actual, tolerance)
991 .map_err(|_| ValidationError::FinanceInvariantFailed)
992}
993
994pub fn validate_subscription_plan(
995 price: Money,
996 period_days: Days,
997) -> Result<SubscriptionPlan, ValidationError> {
998 SubscriptionPlan::try_new(price, period_days)
999 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1000}
1001
1002pub fn validate_recurring_subscription(
1003 customer: CustomerId,
1004 plan: SubscriptionPlan,
1005 status: SubscriptionLifecycleStatus,
1006 current_billing_date: Timestamp,
1007 next_billing_date: Timestamp,
1008) -> Result<RecurringSubscription, ValidationError> {
1009 RecurringSubscription::try_new(
1010 customer,
1011 plan,
1012 status,
1013 current_billing_date,
1014 next_billing_date,
1015 )
1016 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1017}
1018
1019pub fn validate_gift_card_redemption(
1020 card: GiftCard,
1021 amount: Money,
1022) -> Result<GiftCardRedemption, ValidationError> {
1023 GiftCardRedemption::try_new(card, amount)
1024 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1025}
1026
1027pub fn validate_chargeback(
1028 payment_amount: Money,
1029 chargeback_amount: Money,
1030) -> Result<Chargeback, ValidationError> {
1031 Chargeback::try_new(payment_amount, chargeback_amount)
1032 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1033}
1034
1035pub fn validate_cashflow_plan(
1036 starting_cash: Money,
1037 required_reserve: Money,
1038 expected_inflows: Money,
1039 expected_outflows: Money,
1040) -> Result<CashflowPlan, ValidationError> {
1041 CashflowPlan::try_new(
1042 starting_cash,
1043 required_reserve,
1044 expected_inflows,
1045 expected_outflows,
1046 )
1047 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1048}
1049
1050pub fn validate_event_backed_cashflow_plan(
1051 starting_cash: Money,
1052 required_reserve: Money,
1053 events: Vec<CashflowEvent>,
1054) -> Result<EventBackedCashflowPlan, ValidationError> {
1055 EventBackedCashflowPlan::try_new(starting_cash, required_reserve, events)
1056 .map_err(|_| ValidationError::PostPurchaseInvariantFailed)
1057}
1058
1059pub fn validate_audited_command(
1060 actor: Role,
1061 action: Action,
1062 order_id: OrderId,
1063 event: AuditEvent,
1064) -> Result<AuditedCommand, ValidationError> {
1065 AuditedCommand::try_new(actor, action, order_id, event)
1066 .map_err(|_| ValidationError::AuditPermissionDenied)
1067}
1068
1069pub fn validate_audited_entity_command(
1070 actor: Role,
1071 action: Action,
1072 subject_id: Id,
1073 event: EntityAuditEvent,
1074) -> Result<AuditedEntityCommand, ValidationError> {
1075 AuditedEntityCommand::try_new(actor, action, subject_id, event)
1076 .map_err(|_| ValidationError::AuditPermissionDenied)
1077}
1078
1079pub fn validate_event_stream(stream: EventStream) -> Result<ValidEventStream, ValidationError> {
1080 ValidEventStream::try_new(stream).map_err(|_| ValidationError::EventStreamInvalid)
1081}
1082
1083pub fn validate_approved_supplier_quality(
1084 supplier: DropshipSupplier,
1085 metrics: SupplierQualityMetrics,
1086 policy: SupplierRiskPolicy,
1087) -> Result<ApprovedSupplierQuality, ValidationError> {
1088 ApprovedSupplierQuality::try_new(supplier, metrics, policy)
1089 .map_err(|_| ValidationError::SupplierQualityInvalid)
1090}
1091
1092#[allow(clippy::too_many_arguments)]
1093pub fn validate_dropship_opportunity_candidate(
1094 sku: Sku,
1095 units: Quantity,
1096 target_price: Money,
1097 required_capital: Money,
1098 expected_profit: Money,
1099 min_profit: Money,
1100 competitor_price: Money,
1101 costs: DropshipProfitCosts,
1102) -> Result<DropshipOpportunityCandidate, ValidationError> {
1103 DropshipOpportunityCandidate::try_new(
1104 sku,
1105 units,
1106 target_price,
1107 required_capital,
1108 expected_profit,
1109 min_profit,
1110 competitor_price,
1111 costs,
1112 )
1113 .map_err(|_| ValidationError::OpportunityInvariantFailed)
1114}
1115
1116pub fn validate_dropship_opportunity_portfolio(
1117 selected: Vec<DropshipOpportunityCandidate>,
1118 investment_fund: Money,
1119) -> Result<DropshipOpportunityPortfolio, ValidationError> {
1120 DropshipOpportunityPortfolio::try_new(selected, investment_fund)
1121 .map_err(|_| ValidationError::OpportunityInvariantFailed)
1122}
1123
1124pub fn validate_crm_account(
1125 id: AccountId,
1126 customer: Customer,
1127 tier: AccountTier,
1128 status: CRMAccountStatus,
1129 lifetime_value: Money,
1130 open_balance: Money,
1131) -> Result<CRMAccount, ValidationError> {
1132 CRMAccount::try_new(id, customer, tier, status, lifetime_value, open_balance)
1133 .map_err(|_| ValidationError::CrmInvariantFailed)
1134}
1135
1136pub fn validate_active_crm_account(
1137 account: CRMAccount,
1138) -> Result<ActiveCRMAccount, ValidationError> {
1139 ActiveCRMAccount::try_new(account).map_err(|_| ValidationError::CrmInvariantFailed)
1140}
1141
1142pub fn validate_crm_account_contact(
1143 account: CRMAccount,
1144 contact: CRMContact,
1145) -> Result<CRMAccountContact, ValidationError> {
1146 CRMAccountContact::try_new(account, contact).map_err(|_| ValidationError::CrmInvariantFailed)
1147}
1148
1149pub fn validate_permitted_customer_message(
1150 interaction_id: InteractionId,
1151 contact: CRMContact,
1152 sent_at: Timestamp,
1153) -> Result<PermittedCustomerMessage, ValidationError> {
1154 PermittedCustomerMessage::try_new(interaction_id, contact, sent_at)
1155 .map_err(|_| ValidationError::CrmInvariantFailed)
1156}
1157
1158pub fn validate_permitted_account_message(
1159 account_contact: CRMAccountContact,
1160 message: PermittedCustomerMessage,
1161) -> Result<PermittedAccountMessage, ValidationError> {
1162 PermittedAccountMessage::try_new(account_contact, message)
1163 .map_err(|_| ValidationError::CrmInvariantFailed)
1164}
1165
1166pub fn validate_crm_interaction(
1167 id: InteractionId,
1168 account_id: AccountId,
1169 contact_id: ContactId,
1170 kind: InteractionKind,
1171 occurred_at: Timestamp,
1172 follow_up_due_at: Timestamp,
1173) -> Result<CRMInteraction, ValidationError> {
1174 CRMInteraction::try_new(
1175 id,
1176 account_id,
1177 contact_id,
1178 kind,
1179 occurred_at,
1180 follow_up_due_at,
1181 )
1182 .map_err(|_| ValidationError::CrmInvariantFailed)
1183}
1184
1185pub fn validate_crm_interaction_for_contact(
1186 account_contact: CRMAccountContact,
1187 interaction: CRMInteraction,
1188) -> Result<CRMInteractionForContact, ValidationError> {
1189 CRMInteractionForContact::try_new(account_contact, interaction)
1190 .map_err(|_| ValidationError::CrmInvariantFailed)
1191}
1192
1193#[allow(clippy::too_many_arguments)]
1194pub fn validate_lead(
1195 id: LeadId,
1196 account_id: AccountId,
1197 contact_id: ContactId,
1198 source_campaign: Option<CampaignId>,
1199 status: LeadStatus,
1200 estimated_value: Money,
1201 currency: Currency,
1202 created_at: Timestamp,
1203 updated_at: Timestamp,
1204) -> Result<Lead, ValidationError> {
1205 Lead::try_new(
1206 id,
1207 account_id,
1208 contact_id,
1209 source_campaign,
1210 status,
1211 estimated_value,
1212 currency,
1213 created_at,
1214 updated_at,
1215 )
1216 .map_err(|_| ValidationError::CrmInvariantFailed)
1217}
1218
1219pub fn validate_lead_for_contact(
1220 account_contact: CRMAccountContact,
1221 lead: Lead,
1222) -> Result<LeadForContact, ValidationError> {
1223 LeadForContact::try_new(account_contact, lead).map_err(|_| ValidationError::CrmInvariantFailed)
1224}
1225
1226#[allow(clippy::too_many_arguments)]
1227pub fn validate_sales_opportunity(
1228 id: OpportunityId,
1229 account_id: AccountId,
1230 contact_id: ContactId,
1231 source_lead: Option<LeadId>,
1232 stage: OpportunityStage,
1233 amount: Money,
1234 currency: Currency,
1235 probability: BasisPoints,
1236 opened_at: Timestamp,
1237 updated_at: Timestamp,
1238 expected_close_at: Timestamp,
1239) -> Result<SalesOpportunity, ValidationError> {
1240 SalesOpportunity::try_new(
1241 id,
1242 account_id,
1243 contact_id,
1244 source_lead,
1245 stage,
1246 amount,
1247 currency,
1248 probability,
1249 opened_at,
1250 updated_at,
1251 expected_close_at,
1252 )
1253 .map_err(|_| ValidationError::CrmInvariantFailed)
1254}
1255
1256pub fn validate_opportunity_for_contact(
1257 account_contact: CRMAccountContact,
1258 opportunity: SalesOpportunity,
1259) -> Result<OpportunityForContact, ValidationError> {
1260 OpportunityForContact::try_new(account_contact, opportunity)
1261 .map_err(|_| ValidationError::CrmInvariantFailed)
1262}
1263
1264pub fn validate_sales_pipeline(
1265 currency: Currency,
1266 opportunities: Vec<SalesOpportunity>,
1267) -> Result<SalesPipeline, ValidationError> {
1268 SalesPipeline::try_new(currency, opportunities).map_err(|_| ValidationError::CrmInvariantFailed)
1269}
1270
1271pub fn validate_customer_segment(
1272 id: SegmentId,
1273 name: String,
1274 member_count: Nat,
1275 min_lifetime_value: Money,
1276 max_retention_discount: Money,
1277) -> Result<CustomerSegment, ValidationError> {
1278 CustomerSegment::try_new(
1279 id,
1280 name,
1281 member_count,
1282 min_lifetime_value,
1283 max_retention_discount,
1284 )
1285 .map_err(|_| ValidationError::CrmInvariantFailed)
1286}
1287
1288pub fn validate_segment_membership(
1289 account: CRMAccount,
1290 segment: CustomerSegment,
1291) -> Result<SegmentMembership, ValidationError> {
1292 SegmentMembership::try_new(account, segment).map_err(|_| ValidationError::CrmInvariantFailed)
1293}
1294
1295#[allow(clippy::too_many_arguments)]
1296pub fn validate_support_case(
1297 id: SupportCaseId,
1298 account_id: AccountId,
1299 contact_id: ContactId,
1300 order_id: Option<OrderId>,
1301 status: SupportCaseStatus,
1302 priority: SupportPriority,
1303 opened_at: Timestamp,
1304 last_updated_at: Timestamp,
1305 sla_due_at: Timestamp,
1306) -> Result<SupportCase, ValidationError> {
1307 SupportCase::try_new(
1308 id,
1309 account_id,
1310 contact_id,
1311 order_id,
1312 status,
1313 priority,
1314 opened_at,
1315 last_updated_at,
1316 sla_due_at,
1317 )
1318 .map_err(|_| ValidationError::CrmInvariantFailed)
1319}
1320
1321pub fn validate_support_case_for_contact(
1322 account_contact: CRMAccountContact,
1323 case_: SupportCase,
1324) -> Result<SupportCaseForContact, ValidationError> {
1325 SupportCaseForContact::try_new(account_contact, case_)
1326 .map_err(|_| ValidationError::CrmInvariantFailed)
1327}
1328
1329pub fn validate_resolved_support_case(
1330 case_: SupportCase,
1331 resolved_at: Timestamp,
1332) -> Result<ResolvedSupportCase, ValidationError> {
1333 ResolvedSupportCase::try_new(case_, resolved_at)
1334 .map_err(|_| ValidationError::CrmInvariantFailed)
1335}
1336
1337pub fn validate_retention_offer(
1338 account: CRMAccount,
1339 segment: CustomerSegment,
1340 coupon: Coupon,
1341 uses_before: Nat,
1342 discount: Money,
1343) -> Result<RetentionOffer, ValidationError> {
1344 RetentionOffer::try_new(account, segment, coupon, uses_before, discount)
1345 .map_err(|_| ValidationError::CrmInvariantFailed)
1346}
1347
1348#[allow(clippy::too_many_arguments)]
1349pub fn validate_logistics_shipment_plan(
1350 id: ShipmentId,
1351 order: Order,
1352 fulfillment: DistinctFulfillmentPlan,
1353 quote: CarrierQuote,
1354 warehouse: Warehouse,
1355 destination: ShippingDestination,
1356 planned_ship_at: Timestamp,
1357 promised_delivery_at: Timestamp,
1358) -> Result<LogisticsShipmentPlan, ValidationError> {
1359 let package = quote.package().clone();
1360 LogisticsShipmentPlan::try_new(
1361 id,
1362 order,
1363 fulfillment,
1364 package,
1365 quote,
1366 warehouse,
1367 destination,
1368 planned_ship_at,
1369 promised_delivery_at,
1370 )
1371 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1372}
1373
1374pub fn validate_logistics_shipment(
1375 id: ShipmentId,
1376 plan: LogisticsShipmentPlan,
1377 status: ShipmentStatus,
1378 created_at: Timestamp,
1379 updated_at: Timestamp,
1380) -> Result<LogisticsShipment, ValidationError> {
1381 LogisticsShipment::try_new(id, plan, status, created_at, updated_at)
1382 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1383}
1384
1385pub fn validate_carrier_handoff(
1386 plan: LogisticsShipmentPlan,
1387 tracking_number: Id,
1388 handed_off_at: Timestamp,
1389 acceptance_scan_at: Timestamp,
1390) -> Result<CarrierHandoff, ValidationError> {
1391 let service = plan.quote().service().clone();
1392 CarrierHandoff::try_new(
1393 plan,
1394 service,
1395 tracking_number,
1396 handed_off_at,
1397 acceptance_scan_at,
1398 )
1399 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1400}
1401
1402#[allow(clippy::too_many_arguments)]
1403pub fn validate_warehouse_transfer(
1404 id: TransferId,
1405 sku: Sku,
1406 from_warehouse: Warehouse,
1407 to_warehouse: Warehouse,
1408 source_stock: StockState,
1409 requested: Quantity,
1410 in_transit: Quantity,
1411 received: Quantity,
1412) -> Result<WarehouseTransfer, ValidationError> {
1413 WarehouseTransfer::try_new(
1414 id,
1415 sku,
1416 from_warehouse,
1417 to_warehouse,
1418 source_stock,
1419 requested,
1420 in_transit,
1421 received,
1422 )
1423 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1424}
1425
1426#[allow(clippy::too_many_arguments)]
1427pub fn validate_return_authorization(
1428 id: ReturnAuthorizationId,
1429 support_case: SupportCase,
1430 order: Order,
1431 ledger: PaymentLedger,
1432 status: ReturnAuthorizationStatus,
1433 lines: Vec<ReturnLine>,
1434 quantity: Quantity,
1435 refund_amount: Money,
1436 requested_at: Timestamp,
1437 decided_at: Timestamp,
1438) -> Result<ReturnAuthorization, ValidationError> {
1439 ReturnAuthorization::try_new(
1440 id,
1441 support_case,
1442 order,
1443 ledger,
1444 status,
1445 lines,
1446 quantity,
1447 refund_amount,
1448 requested_at,
1449 decided_at,
1450 )
1451 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1452}
1453
1454pub fn validate_return_receipt(
1455 authorization: ReturnAuthorization,
1456 received_quantity: Quantity,
1457 refund_issued: Money,
1458 received_at: Timestamp,
1459) -> Result<ReturnReceipt, ValidationError> {
1460 ReturnReceipt::try_new(authorization, received_quantity, refund_issued, received_at)
1461 .map_err(|_| ValidationError::LogisticsInvariantFailed)
1462}
1463
1464pub fn validate_bounded_coupon_application(
1465 coupon: Coupon,
1466 subtotal: Money,
1467 uses_before: Nat,
1468) -> Result<BoundedCouponApplication, ValidationError> {
1469 BoundedCouponApplication::try_new(coupon, subtotal, uses_before)
1470 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1471}
1472
1473pub fn validate_captured_payment_matches_order(
1474 order: Order,
1475 payment: CapturedPayment,
1476) -> Result<CapturedPaymentMatchesOrder, ValidationError> {
1477 CapturedPaymentMatchesOrder::try_new(order, payment)
1478 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1479}
1480
1481pub fn validate_sellable_catalog_entry(
1482 entry: ProductCatalogEntry,
1483) -> Result<SellableCatalogEntry, ValidationError> {
1484 SellableCatalogEntry::try_new(entry).map_err(|_| ValidationError::ImplicitInvariantFailed)
1485}
1486
1487pub fn validate_publishable_feed_line(
1488 line: SafeProductFeedLine,
1489) -> Result<PublishableFeedLine, ValidationError> {
1490 PublishableFeedLine::try_new(line).map_err(|_| ValidationError::ImplicitInvariantFailed)
1491}
1492
1493pub fn validate_sourceable_distributor_product(
1494 product: DistributorProduct,
1495 units: Quantity,
1496) -> Result<SourceableDistributorProduct, ValidationError> {
1497 SourceableDistributorProduct::try_new(product, units)
1498 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1499}
1500
1501pub fn validate_fraud_checked_coupon_application(
1502 application: BoundedCouponApplication,
1503 policy: FraudPolicy,
1504) -> Result<FraudCheckedCouponApplication, ValidationError> {
1505 FraudCheckedCouponApplication::try_new(application, policy)
1506 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1507}
1508
1509pub fn validate_captured_payment_journal_projection(
1510 accounts: AccountingAccounts,
1511 payment: CapturedPayment,
1512) -> Result<CapturedPaymentJournalProjection, ValidationError> {
1513 let journal = payment_captured_journal(&accounts, payment.amount())
1514 .map_err(|_| ValidationError::ImplicitInvariantFailed)?;
1515 CapturedPaymentJournalProjection::try_new(accounts, payment, journal)
1516 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1517}
1518
1519pub fn validate_refund_journal_projection(
1520 accounts: AccountingAccounts,
1521 ledger: PaymentLedger,
1522 amount: Money,
1523) -> Result<RefundJournalProjection, ValidationError> {
1524 let journal = refund_issued_journal(&accounts, amount)
1525 .map_err(|_| ValidationError::ImplicitInvariantFailed)?;
1526 RefundJournalProjection::try_new(accounts, ledger, amount, journal)
1527 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1528}
1529
1530pub fn validate_advertisable_synced_marketplace_listing(
1531 synced: SyncedMarketplaceListing,
1532) -> Result<AdvertisableSyncedMarketplaceListing, ValidationError> {
1533 AdvertisableSyncedMarketplaceListing::try_new(synced)
1534 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1535}
1536
1537pub fn validate_wholesale_credit_checkout(
1538 account: WholesaleCreditAccount,
1539 lines: Vec<WholesaleLine>,
1540 terms: PaymentTerms,
1541 order_total: Money,
1542) -> Result<WholesaleCreditCheckout, ValidationError> {
1543 WholesaleCreditCheckout::try_new(account, lines, terms, order_total)
1544 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1545}
1546
1547pub fn validate_trusted_fresh_competitor_benchmark(
1548 benchmark: CompetitorPriceBenchmark,
1549 now: Timestamp,
1550 max_age: Duration,
1551 trust: TrustLevel,
1552) -> Result<TrustedFreshCompetitorBenchmark, ValidationError> {
1553 TrustedFreshCompetitorBenchmark::try_new(benchmark, now, max_age, trust)
1554 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1555}
1556
1557pub fn validate_map_compliant_competitor_aware_offer(
1558 offer: CompetitorAwareDropshipOffer,
1559 policy: BrandPricingPolicy,
1560) -> Result<MapCompliantCompetitorAwareOffer, ValidationError> {
1561 MapCompliantCompetitorAwareOffer::try_new(offer, policy)
1562 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1563}
1564
1565pub fn validate_fresh_currency_conversion(
1566 source_amount: MoneyAmount,
1567 rate: ExchangeRate,
1568 target_amount: MoneyAmount,
1569 now: Timestamp,
1570 max_age: Duration,
1571) -> Result<FreshCurrencyConversion, ValidationError> {
1572 FreshCurrencyConversion::try_new(source_amount, rate, target_amount, now, max_age)
1573 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1574}
1575
1576pub fn validate_valid_gift_card_redemption_at(
1577 now: Timestamp,
1578 redemption: GiftCardRedemption,
1579) -> Result<ValidGiftCardRedemptionAt, ValidationError> {
1580 ValidGiftCardRedemptionAt::try_new(now, redemption)
1581 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1582}
1583
1584pub fn validate_chargeback_for_captured_payment(
1585 payment: CapturedPayment,
1586 chargeback: Chargeback,
1587) -> Result<ChargebackForCapturedPayment, ValidationError> {
1588 ChargebackForCapturedPayment::try_new(payment, chargeback)
1589 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1590}
1591
1592pub fn validate_actionable_demand_forecast(
1593 forecast: DemandForecast,
1594) -> Result<ActionableDemandForecast, ValidationError> {
1595 ActionableDemandForecast::try_new(forecast)
1596 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1597}
1598
1599pub fn validate_approved_orderable_supplier_quality(
1600 quality: ApprovedSupplierQuality,
1601) -> Result<ApprovedOrderableSupplierQuality, ValidationError> {
1602 ApprovedOrderableSupplierQuality::try_new(quality)
1603 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1604}
1605
1606pub fn validate_converted_lead_opportunity(
1607 lead: Lead,
1608 opportunity: SalesOpportunity,
1609) -> Result<ConvertedLeadOpportunity, ValidationError> {
1610 ConvertedLeadOpportunity::try_new(lead, opportunity)
1611 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1612}
1613
1614pub fn validate_crm_order_contact(
1615 account: CRMAccount,
1616 contact: CRMContact,
1617 order: Order,
1618) -> Result<CRMOrderContact, ValidationError> {
1619 CRMOrderContact::try_new(account, contact, order)
1620 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1621}
1622
1623pub fn validate_shipment_for_crm_order(
1624 crm_order: CRMOrderContact,
1625 plan: LogisticsShipmentPlan,
1626) -> Result<ShipmentForCRMOrder, ValidationError> {
1627 ShipmentForCRMOrder::try_new(crm_order, plan)
1628 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1629}
1630
1631pub fn validate_logistics_exception_support_case(
1632 exception: LogisticsException,
1633 shipment: LogisticsShipmentPlan,
1634 support_case: SupportCase,
1635) -> Result<LogisticsExceptionSupportCase, ValidationError> {
1636 LogisticsExceptionSupportCase::try_new(exception, shipment, support_case)
1637 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1638}
1639
1640pub fn validate_crm_approved_return_handling(
1641 authorization: ReturnAuthorization,
1642 receipt: ReturnReceipt,
1643) -> Result<CRMApprovedReturnHandling, ValidationError> {
1644 CRMApprovedReturnHandling::try_new(authorization, receipt)
1645 .map_err(|_| ValidationError::ImplicitInvariantFailed)
1646}