1use std::collections::HashSet;
2
3use crate::crm::*;
4use crate::foundation::*;
5use crate::fulfillment_finance::*;
6use crate::inventory::*;
7use crate::orders::*;
8use crate::pricing::*;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum ShipmentStatus {
13 Planned,
14 Allocated,
15 Packed,
16 InTransit,
17 OutForDelivery,
18 Delivered,
19 Exception,
20 Returned,
21 Cancelled,
22}
23
24#[must_use]
25pub const fn can_shipment_transition(source: ShipmentStatus, target: ShipmentStatus) -> bool {
26 matches!(
27 (source, target),
28 (
29 ShipmentStatus::Planned,
30 ShipmentStatus::Allocated | ShipmentStatus::Cancelled
31 ) | (
32 ShipmentStatus::Allocated,
33 ShipmentStatus::Packed | ShipmentStatus::Cancelled
34 ) | (
35 ShipmentStatus::Packed | ShipmentStatus::Exception,
36 ShipmentStatus::InTransit
37 ) | (
38 ShipmentStatus::InTransit,
39 ShipmentStatus::OutForDelivery | ShipmentStatus::Exception
40 ) | (
41 ShipmentStatus::OutForDelivery,
42 ShipmentStatus::Delivered | ShipmentStatus::Exception
43 ) | (ShipmentStatus::Exception, ShipmentStatus::Returned)
44 )
45}
46
47#[must_use]
48pub fn order_eligible_for_logistics(order: &Order) -> bool {
49 matches!(order.status(), OrderStatus::Paid | OrderStatus::Packed)
50}
51
52domain_struct! {
53 pub struct ShippingDestination {
54 id: Id,
55 zone: ShippingZone,
56 postal_code: Nat,
57 }
58}
59
60#[must_use]
61pub fn cart_contains_sku(sku: Sku, items: &[CartLine]) -> bool {
62 items.iter().any(|line| line.sku() == sku)
63}
64
65#[must_use]
66pub fn allocations_match_cart_skus(items: &[CartLine], allocations: &[Allocation]) -> bool {
67 allocations
68 .iter()
69 .all(|allocation| cart_contains_sku(allocation.node.stock.sku, items))
70}
71
72pub fn cart_sku_quantity_total(sku: Sku, items: &[CartLine]) -> DomainResult<Quantity> {
73 checked_sum(
74 items
75 .iter()
76 .filter(|line| line.sku() == sku)
77 .map(CartLine::quantity),
78 "cart_sku_quantity_total",
79 )
80}
81
82pub fn allocation_sku_quantity_total(
83 sku: Sku,
84 allocations: &[Allocation],
85) -> DomainResult<Quantity> {
86 checked_sum(
87 allocations
88 .iter()
89 .filter(|allocation| allocation.node.stock.sku == sku)
90 .map(|allocation| allocation.quantity),
91 "allocation_sku_quantity_total",
92 )
93}
94
95pub fn cart_sku_support(items: &[CartLine]) -> Vec<Sku> {
96 items.iter().map(CartLine::sku).collect()
97}
98
99#[must_use]
100pub fn allocation_sku_support(allocations: &[Allocation]) -> Vec<Sku> {
101 allocations
102 .iter()
103 .map(|allocation| allocation.node.stock.sku)
104 .collect()
105}
106
107#[must_use]
108pub fn shipment_sku_quantity_support(items: &[CartLine], allocations: &[Allocation]) -> Vec<Sku> {
109 let mut support = cart_sku_support(items);
110 support.extend(allocation_sku_support(allocations));
111 support
112}
113
114pub fn shipment_sku_quantities_match_keys(
115 items: &[CartLine],
116 allocations: &[Allocation],
117 keys: &[Sku],
118) -> DomainResult<bool> {
119 for sku in keys {
120 if cart_sku_quantity_total(*sku, items)?
121 != allocation_sku_quantity_total(*sku, allocations)?
122 {
123 return Ok(false);
124 }
125 }
126 Ok(true)
127}
128
129pub fn allocation_quantities_match_cart_skus(
130 items: &[CartLine],
131 allocations: &[Allocation],
132) -> DomainResult<bool> {
133 shipment_sku_quantities_match_keys(
134 items,
135 allocations,
136 &shipment_sku_quantity_support(items, allocations),
137 )
138}
139
140#[must_use]
141pub fn allocations_use_warehouse(warehouse: &Warehouse, allocations: &[Allocation]) -> bool {
142 allocations
143 .iter()
144 .all(|allocation| allocation.node.warehouse.id == warehouse.id())
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct LogisticsShipmentPlan {
150 pub(crate) id: ShipmentId,
151 pub(crate) order: Order,
152 pub(crate) fulfillment: DistinctFulfillmentPlan,
153 pub(crate) package: Package,
154 pub(crate) quote: CarrierQuote,
155 pub(crate) warehouse: Warehouse,
156 pub(crate) destination: ShippingDestination,
157 pub(crate) planned_ship_at: Timestamp,
158 pub(crate) promised_delivery_at: Timestamp,
159}
160
161impl LogisticsShipmentPlan {
162 #[allow(clippy::too_many_arguments)]
163 pub fn try_new(
164 id: ShipmentId,
165 order: Order,
166 fulfillment: DistinctFulfillmentPlan,
167 package: Package,
168 quote: CarrierQuote,
169 warehouse: Warehouse,
170 destination: ShippingDestination,
171 planned_ship_at: Timestamp,
172 promised_delivery_at: Timestamp,
173 ) -> DomainResult<Self> {
174 if !order_eligible_for_logistics(&order) {
175 return Err(ValidationError::LogisticsInvariantFailed);
176 }
177 if fulfillment.requested() != cart_quantity_total(order.items())? {
178 return Err(ValidationError::LogisticsInvariantFailed);
179 }
180 if !allocations_match_cart_skus(order.items(), fulfillment.allocations()) {
181 return Err(ValidationError::LogisticsInvariantFailed);
182 }
183 if !allocation_quantities_match_cart_skus(order.items(), fulfillment.allocations())? {
184 return Err(ValidationError::LogisticsInvariantFailed);
185 }
186 if !allocations_use_warehouse(&warehouse, fulfillment.allocations()) {
187 return Err(ValidationError::LogisticsInvariantFailed);
188 }
189 if quote.package != package || quote.service.zone != destination.zone {
190 return Err(ValidationError::LogisticsInvariantFailed);
191 }
192 if cart_weight_total(order.items())? > package.weight {
193 return Err(ValidationError::LogisticsInvariantFailed);
194 }
195 if promised_delivery_at < planned_ship_at {
196 return Err(ValidationError::LogisticsInvariantFailed);
197 }
198 Ok(Self {
199 id,
200 order,
201 fulfillment,
202 package,
203 quote,
204 warehouse,
205 destination,
206 planned_ship_at,
207 promised_delivery_at,
208 })
209 }
210}
211
212#[derive(Clone, Debug, PartialEq, Eq)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct LogisticsShipment {
215 pub(crate) id: ShipmentId,
216 pub(crate) plan: LogisticsShipmentPlan,
217 pub(crate) status: ShipmentStatus,
218 pub(crate) created_at: Timestamp,
219 pub(crate) updated_at: Timestamp,
220}
221
222impl LogisticsShipment {
223 pub fn try_new(
224 id: ShipmentId,
225 plan: LogisticsShipmentPlan,
226 status: ShipmentStatus,
227 created_at: Timestamp,
228 updated_at: Timestamp,
229 ) -> DomainResult<Self> {
230 if id != plan.id || updated_at < created_at {
231 return Err(ValidationError::LogisticsInvariantFailed);
232 }
233 Ok(Self {
234 id,
235 plan,
236 status,
237 created_at,
238 updated_at,
239 })
240 }
241}
242
243pub fn transition_shipment(
244 shipment: LogisticsShipment,
245 next: ShipmentStatus,
246 updated_at: Timestamp,
247) -> DomainResult<LogisticsShipment> {
248 if !can_shipment_transition(shipment.status, next) || updated_at < shipment.created_at {
249 return Err(ValidationError::LogisticsInvariantFailed);
250 }
251 Ok(LogisticsShipment {
252 status: next,
253 updated_at,
254 ..shipment
255 })
256}
257
258#[derive(Clone, Debug, PartialEq, Eq)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260pub struct CarrierHandoff {
261 pub(crate) plan: LogisticsShipmentPlan,
262 pub(crate) service: CarrierService,
263 pub(crate) tracking_number: Id,
264 pub(crate) handed_off_at: Timestamp,
265 pub(crate) acceptance_scan_at: Timestamp,
266}
267
268impl CarrierHandoff {
269 pub fn try_new(
270 plan: LogisticsShipmentPlan,
271 service: CarrierService,
272 tracking_number: Id,
273 handed_off_at: Timestamp,
274 acceptance_scan_at: Timestamp,
275 ) -> DomainResult<Self> {
276 if service != plan.quote.service
277 || handed_off_at < plan.planned_ship_at
278 || acceptance_scan_at < handed_off_at
279 {
280 return Err(ValidationError::LogisticsInvariantFailed);
281 }
282 Ok(Self {
283 plan,
284 service,
285 tracking_number,
286 handed_off_at,
287 acceptance_scan_at,
288 })
289 }
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub enum TrackingEventKind {
295 LabelCreated,
296 PickupScan,
297 InTransitScan,
298 OutForDeliveryScan,
299 DeliveredScan,
300 ExceptionScan,
301 ReturnScan,
302}
303
304#[must_use]
305pub const fn can_tracking_progress(source: TrackingEventKind, target: TrackingEventKind) -> bool {
306 matches!(
307 (source, target),
308 (
309 TrackingEventKind::LabelCreated,
310 TrackingEventKind::LabelCreated | TrackingEventKind::PickupScan
311 ) | (
312 TrackingEventKind::PickupScan
313 | TrackingEventKind::InTransitScan
314 | TrackingEventKind::ExceptionScan,
315 TrackingEventKind::InTransitScan
316 ) | (
317 TrackingEventKind::InTransitScan,
318 TrackingEventKind::OutForDeliveryScan | TrackingEventKind::ExceptionScan
319 ) | (
320 TrackingEventKind::OutForDeliveryScan,
321 TrackingEventKind::DeliveredScan | TrackingEventKind::ExceptionScan
322 ) | (
323 TrackingEventKind::ExceptionScan,
324 TrackingEventKind::ReturnScan
325 )
326 )
327}
328
329domain_struct! {
330 pub struct TrackingEvent {
331 id: TrackingEventId,
332 shipment_id: ShipmentId,
333 carrier_id: Id,
334 tracking_number: Id,
335 kind: TrackingEventKind,
336 occurred_at: Timestamp,
337 }
338}
339
340#[must_use]
341pub fn tracking_events_monotone_from(last: Timestamp, events: &[TrackingEvent]) -> bool {
342 let mut cursor = last;
343 for event in events {
344 if event.occurred_at < cursor {
345 return false;
346 }
347 cursor = event.occurred_at;
348 }
349 true
350}
351
352#[must_use]
353pub fn tracking_events_for_shipment(shipment_id: ShipmentId, events: &[TrackingEvent]) -> bool {
354 events.iter().all(|event| event.shipment_id == shipment_id)
355}
356
357#[must_use]
358pub fn tracking_events_for_carrier(
359 carrier_id: Id,
360 tracking_number: Id,
361 events: &[TrackingEvent],
362) -> bool {
363 events
364 .iter()
365 .all(|event| event.carrier_id == carrier_id && event.tracking_number == tracking_number)
366}
367
368#[must_use]
369pub fn tracking_last_observed_from(last: Timestamp, events: &[TrackingEvent]) -> Timestamp {
370 events.last().map_or(last, |event| event.occurred_at)
371}
372
373#[must_use]
374pub fn tracking_event_ids_distinct(events: &[TrackingEvent]) -> bool {
375 let mut seen = HashSet::new();
376 events.iter().all(|event| seen.insert(event.id.value()))
377}
378
379#[must_use]
380pub fn tracking_events_progress_from(
381 last_kind: TrackingEventKind,
382 events: &[TrackingEvent],
383) -> bool {
384 let mut cursor = last_kind;
385 for event in events {
386 if !can_tracking_progress(cursor, event.kind) {
387 return false;
388 }
389 cursor = event.kind;
390 }
391 true
392}
393
394#[derive(Clone, Debug, PartialEq, Eq)]
395#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
396pub struct TrackingHistory {
397 pub(crate) shipment_id: ShipmentId,
398 pub(crate) carrier_id: Id,
399 pub(crate) tracking_number: Id,
400 pub(crate) events: Vec<TrackingEvent>,
401 pub(crate) last_observed_at: Timestamp,
402}
403
404impl TrackingHistory {
405 pub fn try_new(
406 shipment_id: ShipmentId,
407 carrier_id: Id,
408 tracking_number: Id,
409 events: Vec<TrackingEvent>,
410 last_observed_at: Timestamp,
411 ) -> DomainResult<Self> {
412 if !tracking_events_monotone_from(unix_epoch_timestamp(), &events)
413 || !tracking_events_for_shipment(shipment_id, &events)
414 || !tracking_events_for_carrier(carrier_id, tracking_number, &events)
415 || !tracking_event_ids_distinct(&events)
416 || !tracking_events_progress_from(TrackingEventKind::LabelCreated, &events)
417 || last_observed_at != tracking_last_observed_from(unix_epoch_timestamp(), &events)
418 {
419 return Err(ValidationError::LogisticsInvariantFailed);
420 }
421 Ok(Self {
422 shipment_id,
423 carrier_id,
424 tracking_number,
425 events,
426 last_observed_at,
427 })
428 }
429}
430
431#[derive(Clone, Debug, PartialEq, Eq)]
432#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
433pub struct DeliveryPromise {
434 pub(crate) plan: LogisticsShipmentPlan,
435 pub(crate) promised_by: Timestamp,
436}
437
438impl DeliveryPromise {
439 pub fn try_new(plan: LogisticsShipmentPlan, promised_by: Timestamp) -> DomainResult<Self> {
440 if promised_by != plan.promised_delivery_at {
441 return Err(ValidationError::LogisticsInvariantFailed);
442 }
443 Ok(Self { plan, promised_by })
444 }
445}
446
447#[must_use]
448pub fn delivered_by_promise(promise: &DeliveryPromise, delivered_at: Timestamp) -> bool {
449 delivered_at <= promise.promised_by
450}
451
452#[derive(Clone, Debug, PartialEq, Eq)]
453#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
454pub struct DeliveredShipment {
455 pub(crate) promise: DeliveryPromise,
456 pub(crate) history: TrackingHistory,
457 pub(crate) delivery_event: TrackingEvent,
458 pub(crate) delivered_at: Timestamp,
459}
460
461impl DeliveredShipment {
462 pub fn try_new(
463 promise: DeliveryPromise,
464 history: TrackingHistory,
465 delivery_event: TrackingEvent,
466 delivered_at: Timestamp,
467 ) -> DomainResult<Self> {
468 if history.shipment_id != promise.plan.id
469 || history.carrier_id != promise.plan.quote.service.carrier_id
470 || !history.events.contains(&delivery_event)
471 || delivery_event.kind != TrackingEventKind::DeliveredScan
472 || delivery_event.occurred_at != delivered_at
473 || delivery_event.shipment_id != promise.plan.id
474 || delivery_event.carrier_id != history.carrier_id
475 || delivery_event.tracking_number != history.tracking_number
476 || delivered_at < promise.plan.planned_ship_at
477 || !delivered_by_promise(&promise, delivered_at)
478 {
479 return Err(ValidationError::LogisticsInvariantFailed);
480 }
481 Ok(Self {
482 promise,
483 history,
484 delivery_event,
485 delivered_at,
486 })
487 }
488}
489
490#[derive(Clone, Copy, Debug, PartialEq, Eq)]
491#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
492pub enum LogisticsExceptionKind {
493 CarrierDelay,
494 WeatherDelay,
495 AddressIssue,
496 LostPackage,
497 DamagedPackage,
498 CustomerUnavailable,
499}
500
501domain_struct! {
502 pub struct LogisticsException {
503 shipment_id: ShipmentId,
504 kind: LogisticsExceptionKind,
505 raised_at: Timestamp,
506 customer_visible: bool,
507 }
508}
509
510#[derive(Clone, Debug, PartialEq, Eq)]
511#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
512pub struct WarehouseTransfer {
513 pub(crate) id: TransferId,
514 pub(crate) sku: Sku,
515 pub(crate) from_warehouse: Warehouse,
516 pub(crate) to_warehouse: Warehouse,
517 pub(crate) source_stock: StockState,
518 pub(crate) requested: Quantity,
519 pub(crate) in_transit: Quantity,
520 pub(crate) received: Quantity,
521}
522
523impl WarehouseTransfer {
524 #[allow(clippy::too_many_arguments)]
525 pub fn try_new(
526 id: TransferId,
527 sku: Sku,
528 from_warehouse: Warehouse,
529 to_warehouse: Warehouse,
530 source_stock: StockState,
531 requested: Quantity,
532 in_transit: Quantity,
533 received: Quantity,
534 ) -> DomainResult<Self> {
535 if source_stock.sku() != sku
536 || from_warehouse.id() == to_warehouse.id()
537 || requested > available_stock(&source_stock)
538 || in_transit > requested
539 || received > in_transit
540 {
541 return Err(ValidationError::LogisticsInvariantFailed);
542 }
543 Ok(Self {
544 id,
545 sku,
546 from_warehouse,
547 to_warehouse,
548 source_stock,
549 requested,
550 in_transit,
551 received,
552 })
553 }
554}
555
556#[derive(Clone, Copy, Debug, PartialEq, Eq)]
557#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
558pub enum ReturnAuthorizationStatus {
559 Requested,
560 Approved,
561 Rejected,
562 Received,
563 Refunded,
564 Closed,
565}
566
567#[must_use]
568pub const fn can_return_authorization_transition(
569 source: ReturnAuthorizationStatus,
570 target: ReturnAuthorizationStatus,
571) -> bool {
572 matches!(
573 (source, target),
574 (
575 ReturnAuthorizationStatus::Requested,
576 ReturnAuthorizationStatus::Approved | ReturnAuthorizationStatus::Rejected
577 ) | (
578 ReturnAuthorizationStatus::Approved,
579 ReturnAuthorizationStatus::Received
580 ) | (
581 ReturnAuthorizationStatus::Received,
582 ReturnAuthorizationStatus::Refunded
583 ) | (
584 ReturnAuthorizationStatus::Refunded,
585 ReturnAuthorizationStatus::Closed
586 )
587 )
588}
589
590domain_struct! {
591 pub struct ReturnLine {
592 sku: Sku,
593 quantity: Quantity,
594 refund_amount: Money,
595 }
596}
597
598pub fn return_lines_quantity_total(lines: &[ReturnLine]) -> DomainResult<Quantity> {
599 checked_sum(
600 lines.iter().map(|line| line.quantity),
601 "return_lines_quantity_total",
602 )
603}
604
605pub fn return_lines_refund_total(lines: &[ReturnLine]) -> DomainResult<Money> {
606 checked_sum(
607 lines.iter().map(|line| line.refund_amount),
608 "return_lines_refund_total",
609 )
610}
611
612#[must_use]
613pub fn return_lines_match_order_skus(items: &[CartLine], lines: &[ReturnLine]) -> bool {
614 lines.iter().all(|line| cart_contains_sku(line.sku, items))
615}
616
617#[derive(Clone, Debug, PartialEq, Eq)]
618#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
619pub struct ReturnAuthorization {
620 pub(crate) id: ReturnAuthorizationId,
621 pub(crate) support_case: SupportCase,
622 pub(crate) order: Order,
623 pub(crate) ledger: PaymentLedger,
624 pub(crate) status: ReturnAuthorizationStatus,
625 pub(crate) lines: Vec<ReturnLine>,
626 pub(crate) quantity: Quantity,
627 pub(crate) refund_amount: Money,
628 pub(crate) requested_at: Timestamp,
629 pub(crate) decided_at: Timestamp,
630}
631
632impl ReturnAuthorization {
633 #[allow(clippy::too_many_arguments)]
634 pub fn try_new(
635 id: ReturnAuthorizationId,
636 support_case: SupportCase,
637 order: Order,
638 ledger: PaymentLedger,
639 status: ReturnAuthorizationStatus,
640 lines: Vec<ReturnLine>,
641 quantity: Quantity,
642 refund_amount: Money,
643 requested_at: Timestamp,
644 decided_at: Timestamp,
645 ) -> DomainResult<Self> {
646 if support_case.order_id != Some(order.id())
647 || !return_lines_match_order_skus(order.items(), &lines)
648 || return_lines_quantity_total(&lines)? != quantity
649 || return_lines_refund_total(&lines)? != refund_amount
650 || quantity > cart_quantity_total(order.items())?
651 || !can_refund(&ledger, refund_amount)
652 || ledger.captured() != order.total()
653 || decided_at < requested_at
654 {
655 return Err(ValidationError::LogisticsInvariantFailed);
656 }
657 Ok(Self {
658 id,
659 support_case,
660 order,
661 ledger,
662 status,
663 lines,
664 quantity,
665 refund_amount,
666 requested_at,
667 decided_at,
668 })
669 }
670}
671
672#[must_use]
673pub fn return_authorization_approved(authorization: &ReturnAuthorization) -> bool {
674 authorization.status == ReturnAuthorizationStatus::Approved
675}
676
677pub fn transition_return_authorization(
678 authorization: ReturnAuthorization,
679 next: ReturnAuthorizationStatus,
680 decided_at: Timestamp,
681) -> DomainResult<ReturnAuthorization> {
682 if !can_return_authorization_transition(authorization.status, next)
683 || decided_at < authorization.requested_at
684 {
685 return Err(ValidationError::LogisticsInvariantFailed);
686 }
687 Ok(ReturnAuthorization {
688 status: next,
689 decided_at,
690 ..authorization
691 })
692}
693
694#[derive(Clone, Debug, PartialEq, Eq)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696pub struct ReturnReceipt {
697 pub(crate) authorization: ReturnAuthorization,
698 pub(crate) received_quantity: Quantity,
699 pub(crate) refund_issued: Money,
700 pub(crate) received_at: Timestamp,
701}
702
703impl ReturnReceipt {
704 pub fn try_new(
705 authorization: ReturnAuthorization,
706 received_quantity: Quantity,
707 refund_issued: Money,
708 received_at: Timestamp,
709 ) -> DomainResult<Self> {
710 if !return_authorization_approved(&authorization)
711 || received_quantity > authorization.quantity
712 || refund_issued > authorization.refund_amount
713 || received_at < authorization.decided_at
714 {
715 return Err(ValidationError::LogisticsInvariantFailed);
716 }
717 Ok(Self {
718 authorization,
719 received_quantity,
720 refund_issued,
721 received_at,
722 })
723 }
724}
725
726impl_getters!(LogisticsShipmentPlan {
727 id: ShipmentId,
728 order: Order,
729 fulfillment: DistinctFulfillmentPlan,
730 package: Package,
731 quote: CarrierQuote,
732 warehouse: Warehouse,
733 destination: ShippingDestination,
734 planned_ship_at: Timestamp,
735 promised_delivery_at: Timestamp,
736});
737
738impl_getters!(LogisticsShipment {
739 id: ShipmentId,
740 plan: LogisticsShipmentPlan,
741 status: ShipmentStatus,
742 created_at: Timestamp,
743 updated_at: Timestamp,
744});
745
746impl_getters!(CarrierHandoff {
747 plan: LogisticsShipmentPlan,
748 service: CarrierService,
749 tracking_number: Id,
750 handed_off_at: Timestamp,
751 acceptance_scan_at: Timestamp,
752});
753
754impl_getters!(TrackingHistory {
755 shipment_id: ShipmentId,
756 carrier_id: Id,
757 tracking_number: Id,
758 events: Vec<TrackingEvent>,
759 last_observed_at: Timestamp,
760});
761
762impl_getters!(DeliveryPromise {
763 plan: LogisticsShipmentPlan,
764 promised_by: Timestamp,
765});
766
767impl_getters!(DeliveredShipment {
768 promise: DeliveryPromise,
769 history: TrackingHistory,
770 delivery_event: TrackingEvent,
771 delivered_at: Timestamp,
772});
773
774impl_getters!(WarehouseTransfer {
775 id: TransferId,
776 sku: Sku,
777 from_warehouse: Warehouse,
778 to_warehouse: Warehouse,
779 source_stock: StockState,
780 requested: Quantity,
781 in_transit: Quantity,
782 received: Quantity,
783});
784
785impl_getters!(ReturnAuthorization {
786 id: ReturnAuthorizationId,
787 support_case: SupportCase,
788 order: Order,
789 ledger: PaymentLedger,
790 status: ReturnAuthorizationStatus,
791 lines: Vec<ReturnLine>,
792 quantity: Quantity,
793 refund_amount: Money,
794 requested_at: Timestamp,
795 decided_at: Timestamp,
796});
797
798impl_getters!(ReturnReceipt {
799 authorization: ReturnAuthorization,
800 received_quantity: Quantity,
801 refund_issued: Money,
802 received_at: Timestamp,
803});