1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use stateset_primitives::{CustomerId, OrderId, ProductId, ReturnId};
7use utoipa::{IntoParams, ToSchema};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Deserialize, Serialize, Default, IntoParams)]
16#[into_params(parameter_in = Query)]
17pub struct PaginationParams {
18 pub limit: Option<u32>,
20 pub offset: Option<u32>,
22}
23
24impl PaginationParams {
25 pub const DEFAULT_LIMIT: u32 = 50;
27 pub const MAX_LIMIT: u32 = 200;
29
30 #[must_use]
32 pub fn resolved_limit(&self) -> u32 {
33 resolve_limit(self.limit)
34 }
35
36 #[must_use]
38 pub fn resolved_offset(&self) -> u32 {
39 self.offset.unwrap_or(0)
40 }
41}
42
43#[must_use]
44fn resolve_limit(limit: Option<u32>) -> u32 {
45 limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).clamp(1, PaginationParams::MAX_LIMIT)
46}
47
48#[must_use]
50pub const fn overfetch_limit(limit: u32) -> u32 {
51 limit.saturating_add(1)
52}
53
54pub fn finalize_page<T>(items: &mut Vec<T>, requested_limit: u32) -> bool {
56 let requested_limit = requested_limit as usize;
57 let has_more = items.len() > requested_limit;
58 if has_more {
59 items.truncate(requested_limit);
60 }
61 has_more
62}
63
64use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
69
70#[must_use]
72pub fn encode_cursor(sort_key: &str, id: &str) -> String {
73 let payload = format!("{sort_key}\x00{id}");
74 URL_SAFE_NO_PAD.encode(payload.as_bytes())
75}
76
77#[must_use]
79pub fn decode_cursor(cursor: &str) -> Option<(String, String)> {
80 let bytes = URL_SAFE_NO_PAD.decode(cursor).ok()?;
81 let s = String::from_utf8(bytes).ok()?;
82 let (sort_key, id) = s.split_once('\x00')?;
83 Some((sort_key.to_string(), id.to_string()))
84}
85
86#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
92#[into_params(parameter_in = Query)]
93pub struct OrderFilterParams {
94 pub limit: Option<u32>,
96 pub offset: Option<u32>,
98 pub after: Option<String>,
100 pub customer_id: Option<String>,
102 pub status: Option<String>,
104 pub payment_status: Option<String>,
106 pub fulfillment_status: Option<String>,
108 pub from_date: Option<String>,
110 pub to_date: Option<String>,
112}
113
114impl OrderFilterParams {
115 #[must_use]
117 pub fn resolved_limit(&self) -> u32 {
118 resolve_limit(self.limit)
119 }
120
121 #[must_use]
123 pub fn resolved_offset(&self) -> u32 {
124 self.offset.unwrap_or(0)
125 }
126}
127
128#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
130#[into_params(parameter_in = Query)]
131pub struct CustomerFilterParams {
132 pub limit: Option<u32>,
134 pub offset: Option<u32>,
136 pub after: Option<String>,
138 pub email: Option<String>,
140 pub status: Option<String>,
142 pub tag: Option<String>,
144 pub accepts_marketing: Option<bool>,
146}
147
148impl CustomerFilterParams {
149 #[must_use]
151 pub fn resolved_limit(&self) -> u32 {
152 resolve_limit(self.limit)
153 }
154
155 #[must_use]
157 pub fn resolved_offset(&self) -> u32 {
158 self.offset.unwrap_or(0)
159 }
160}
161
162#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
164#[into_params(parameter_in = Query)]
165pub struct ProductFilterParams {
166 pub limit: Option<u32>,
168 pub offset: Option<u32>,
170 pub after: Option<String>,
172 pub status: Option<String>,
174 pub product_type: Option<String>,
176 pub search: Option<String>,
178 pub category: Option<String>,
180 pub min_price: Option<String>,
182 pub max_price: Option<String>,
184 pub in_stock: Option<bool>,
186}
187
188impl ProductFilterParams {
189 #[must_use]
191 pub fn resolved_limit(&self) -> u32 {
192 resolve_limit(self.limit)
193 }
194
195 #[must_use]
197 pub fn resolved_offset(&self) -> u32 {
198 self.offset.unwrap_or(0)
199 }
200}
201
202#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
204#[into_params(parameter_in = Query)]
205pub struct ReturnFilterParams {
206 pub limit: Option<u32>,
208 pub offset: Option<u32>,
210 pub after: Option<String>,
212 pub order_id: Option<String>,
214 pub customer_id: Option<String>,
216 pub status: Option<String>,
218 pub reason: Option<String>,
220 pub from_date: Option<String>,
222 pub to_date: Option<String>,
224}
225
226impl ReturnFilterParams {
227 #[must_use]
229 pub fn resolved_limit(&self) -> u32 {
230 resolve_limit(self.limit)
231 }
232
233 #[must_use]
235 pub fn resolved_offset(&self) -> u32 {
236 self.offset.unwrap_or(0)
237 }
238}
239
240#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
246pub struct CreateOrderRequest {
247 #[schema(value_type = String, format = "uuid")]
249 pub customer_id: CustomerId,
250 pub items: Vec<CreateOrderItemRequest>,
252 pub currency: Option<String>,
254 pub shipping_address: Option<AddressDto>,
256 pub billing_address: Option<AddressDto>,
258 pub notes: Option<String>,
260 pub payment_method: Option<String>,
262 pub shipping_method: Option<String>,
264}
265
266#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
268pub struct CreateOrderItemRequest {
269 #[schema(value_type = String, format = "uuid")]
270 pub product_id: ProductId,
271 pub variant_id: Option<Uuid>,
272 pub sku: String,
273 pub name: String,
274 pub quantity: i32,
275 #[schema(value_type = String)]
276 pub unit_price: Decimal,
277 #[schema(value_type = Option<String>)]
278 pub discount: Option<Decimal>,
279 #[schema(value_type = Option<String>)]
280 pub tax_amount: Option<Decimal>,
281}
282
283#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
285pub struct AddressDto {
286 pub line1: String,
287 pub line2: Option<String>,
288 pub city: String,
289 pub state: Option<String>,
290 pub postal_code: String,
291 pub country: String,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
296pub struct OrderResponse {
297 #[schema(value_type = String, format = "uuid")]
298 pub id: OrderId,
299 pub order_number: String,
300 #[schema(value_type = String, format = "uuid")]
301 pub customer_id: CustomerId,
302 pub status: String,
303 #[schema(value_type = String)]
304 pub total_amount: Decimal,
305 pub currency: String,
306 pub payment_status: String,
307 pub fulfillment_status: String,
308 pub items: Vec<OrderItemResponse>,
309 pub created_at: DateTime<Utc>,
310 pub updated_at: DateTime<Utc>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
315pub struct OrderItemResponse {
316 pub id: Uuid,
317 #[schema(value_type = String, format = "uuid")]
318 pub product_id: ProductId,
319 pub sku: String,
320 pub name: String,
321 pub quantity: i32,
322 #[schema(value_type = String)]
323 pub unit_price: Decimal,
324 #[schema(value_type = String)]
325 pub total: Decimal,
326}
327
328#[derive(Debug, Clone, Serialize, ToSchema)]
330pub struct OrderListResponse {
331 pub orders: Vec<OrderResponse>,
332 pub total: usize,
333 pub limit: u32,
334 pub offset: u32,
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub next_cursor: Option<String>,
338 pub has_more: bool,
340}
341
342#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
348pub struct CreateCustomerRequest {
349 pub email: String,
350 pub first_name: String,
351 pub last_name: String,
352 pub phone: Option<String>,
353 pub accepts_marketing: Option<bool>,
354 pub tags: Option<Vec<String>>,
355 #[schema(value_type = Option<Object>)]
356 pub metadata: Option<serde_json::Value>,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
361pub struct CustomerResponse {
362 #[schema(value_type = String, format = "uuid")]
363 pub id: CustomerId,
364 pub email: String,
365 pub first_name: String,
366 pub last_name: String,
367 pub phone: Option<String>,
368 pub status: String,
369 pub accepts_marketing: bool,
370 pub created_at: DateTime<Utc>,
371 pub updated_at: DateTime<Utc>,
372}
373
374#[derive(Debug, Clone, Default, Deserialize, Serialize, ToSchema)]
376pub struct UpdateCustomerRequest {
377 pub email: Option<String>,
378 pub first_name: Option<String>,
379 pub last_name: Option<String>,
380 pub phone: Option<String>,
381 pub status: Option<String>,
382 pub accepts_marketing: Option<bool>,
383 pub tags: Option<Vec<String>>,
384 #[schema(value_type = Option<Object>)]
385 pub metadata: Option<serde_json::Value>,
386}
387
388#[derive(Debug, Clone, Serialize, ToSchema)]
390pub struct CustomerListResponse {
391 pub customers: Vec<CustomerResponse>,
392 pub total: usize,
393 pub limit: u32,
394 pub offset: u32,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub next_cursor: Option<String>,
398 pub has_more: bool,
400}
401
402#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
408pub struct CreateProductRequest {
409 pub name: String,
410 pub slug: Option<String>,
411 pub description: Option<String>,
412 pub product_type: Option<String>,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
417pub struct ProductResponse {
418 #[schema(value_type = String, format = "uuid")]
419 pub id: ProductId,
420 pub name: String,
421 pub slug: String,
422 pub description: String,
423 pub status: String,
424 pub product_type: String,
425 pub created_at: DateTime<Utc>,
426 pub updated_at: DateTime<Utc>,
427}
428
429#[derive(Debug, Clone, Default, Deserialize, Serialize, ToSchema)]
431pub struct UpdateProductRequest {
432 pub name: Option<String>,
433 pub description: Option<String>,
434 pub status: Option<String>,
435 pub product_type: Option<String>,
436}
437
438#[derive(Debug, Clone, Serialize, ToSchema)]
440pub struct ProductListResponse {
441 pub products: Vec<ProductResponse>,
442 pub total: usize,
443 pub limit: u32,
444 pub offset: u32,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub next_cursor: Option<String>,
448 pub has_more: bool,
450}
451
452#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
458pub struct InventoryAdjustRequest {
459 #[schema(value_type = String)]
461 pub quantity: Decimal,
462 pub reason: String,
464 pub location_id: Option<i32>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
472pub struct InventoryResponse {
473 pub sku: String,
474 pub name: String,
475 #[schema(value_type = String)]
476 pub total_on_hand: Decimal,
477 #[schema(value_type = String)]
478 pub total_allocated: Decimal,
479 #[schema(value_type = String)]
480 pub total_available: Decimal,
481}
482
483#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
489#[into_params(parameter_in = Query)]
490pub struct InventoryFilterParams {
491 pub limit: Option<u32>,
493 pub offset: Option<u32>,
495 pub sku: Option<String>,
497 pub below_reorder_point: Option<bool>,
499 pub is_active: Option<bool>,
501}
502
503impl InventoryFilterParams {
504 #[must_use]
506 pub fn resolved_limit(&self) -> u32 {
507 resolve_limit(self.limit)
508 }
509
510 #[must_use]
512 pub fn resolved_offset(&self) -> u32 {
513 self.offset.unwrap_or(0)
514 }
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
519pub struct InventoryItemResponse {
520 pub id: i64,
521 pub sku: String,
522 pub name: String,
523 pub description: Option<String>,
524 pub unit_of_measure: String,
525 pub is_active: bool,
526 pub created_at: DateTime<Utc>,
527 pub updated_at: DateTime<Utc>,
528}
529
530#[derive(Debug, Clone, Serialize, ToSchema)]
532pub struct InventoryListResponse {
533 pub items: Vec<InventoryItemResponse>,
534 pub total: usize,
535 pub limit: u32,
536 pub offset: u32,
537 pub has_more: bool,
538}
539
540#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
546#[into_params(parameter_in = Query)]
547pub struct ShipmentFilterParams {
548 pub limit: Option<u32>,
550 pub offset: Option<u32>,
552 pub order_id: Option<String>,
554 pub status: Option<String>,
556 pub carrier: Option<String>,
558 pub tracking_number: Option<String>,
560}
561
562impl ShipmentFilterParams {
563 #[must_use]
565 pub fn resolved_limit(&self) -> u32 {
566 resolve_limit(self.limit)
567 }
568
569 #[must_use]
571 pub fn resolved_offset(&self) -> u32 {
572 self.offset.unwrap_or(0)
573 }
574}
575
576#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
578pub struct CreateShipmentRequest {
579 #[schema(value_type = String, format = "uuid")]
580 pub order_id: stateset_primitives::OrderId,
581 pub carrier: Option<String>,
582 pub tracking_number: Option<String>,
583 pub shipping_method: Option<String>,
584 pub recipient_name: Option<String>,
585 pub notes: Option<String>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
590pub struct ShipmentResponse {
591 #[schema(value_type = String, format = "uuid")]
592 pub id: stateset_primitives::ShipmentId,
593 pub shipment_number: String,
594 #[schema(value_type = String, format = "uuid")]
595 pub order_id: OrderId,
596 pub status: String,
597 pub carrier: String,
598 pub shipping_method: String,
599 pub tracking_number: Option<String>,
600 pub tracking_url: Option<String>,
601 pub recipient_name: String,
602 #[schema(value_type = Option<String>)]
603 pub shipping_cost: Option<Decimal>,
604 pub shipped_at: Option<DateTime<Utc>>,
605 pub estimated_delivery: Option<DateTime<Utc>>,
606 pub delivered_at: Option<DateTime<Utc>>,
607 pub created_at: DateTime<Utc>,
608 pub updated_at: DateTime<Utc>,
609}
610
611#[derive(Debug, Clone, Serialize, ToSchema)]
613pub struct ShipmentListResponse {
614 pub shipments: Vec<ShipmentResponse>,
615 pub total: usize,
616 pub limit: u32,
617 pub offset: u32,
618 pub has_more: bool,
619}
620
621#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
627#[into_params(parameter_in = Query)]
628pub struct PaymentFilterParams {
629 pub limit: Option<u32>,
631 pub offset: Option<u32>,
633 pub order_id: Option<String>,
635 pub customer_id: Option<String>,
637 pub status: Option<String>,
639 pub payment_method: Option<String>,
641 pub processor: Option<String>,
643 pub min_amount: Option<String>,
645 pub max_amount: Option<String>,
647 pub from_date: Option<String>,
649 pub to_date: Option<String>,
651}
652
653impl PaymentFilterParams {
654 #[must_use]
656 pub fn resolved_limit(&self) -> u32 {
657 resolve_limit(self.limit)
658 }
659
660 #[must_use]
662 pub fn resolved_offset(&self) -> u32 {
663 self.offset.unwrap_or(0)
664 }
665}
666
667#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
669pub struct CreatePaymentRequest {
670 #[schema(value_type = String, format = "uuid")]
671 pub order_id: stateset_primitives::OrderId,
672 #[schema(value_type = Option<String>, format = "uuid")]
673 pub customer_id: Option<stateset_primitives::CustomerId>,
674 pub payment_method: Option<String>,
675 #[schema(value_type = String)]
676 pub amount: Decimal,
677 pub currency: Option<String>,
678 pub external_id: Option<String>,
679 pub description: Option<String>,
680}
681
682#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
684pub struct CreateRefundRequest {
685 #[schema(value_type = String)]
686 pub amount: Decimal,
687 pub reason: Option<String>,
688 pub notes: Option<String>,
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
693pub struct PaymentResponse {
694 #[schema(value_type = String, format = "uuid")]
695 pub id: stateset_primitives::PaymentId,
696 pub payment_number: String,
697 #[schema(value_type = Option<String>, format = "uuid")]
698 pub order_id: Option<OrderId>,
699 pub customer_id: Option<String>,
700 pub status: String,
701 pub payment_method: String,
702 #[schema(value_type = String)]
703 pub amount: Decimal,
704 pub currency: String,
705 #[schema(value_type = String)]
706 pub amount_refunded: Decimal,
707 pub external_id: Option<String>,
708 pub processor: Option<String>,
709 pub created_at: DateTime<Utc>,
710 pub updated_at: DateTime<Utc>,
711}
712
713#[derive(Debug, Clone, Serialize, ToSchema)]
715pub struct PaymentListResponse {
716 pub payments: Vec<PaymentResponse>,
717 pub total: usize,
718 pub limit: u32,
719 pub offset: u32,
720 pub has_more: bool,
721}
722
723#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
729#[into_params(parameter_in = Query)]
730pub struct InvoiceFilterParams {
731 pub limit: Option<u32>,
733 pub offset: Option<u32>,
735 pub customer_id: Option<String>,
737 pub order_id: Option<String>,
739 pub status: Option<String>,
741 pub invoice_type: Option<String>,
743 pub overdue_only: Option<bool>,
745 pub from_date: Option<String>,
747 pub to_date: Option<String>,
749}
750
751impl InvoiceFilterParams {
752 #[must_use]
754 pub fn resolved_limit(&self) -> u32 {
755 resolve_limit(self.limit)
756 }
757
758 #[must_use]
760 pub fn resolved_offset(&self) -> u32 {
761 self.offset.unwrap_or(0)
762 }
763}
764
765#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
767pub struct CreateInvoiceRequest {
768 #[schema(value_type = String, format = "uuid")]
769 pub customer_id: stateset_primitives::CustomerId,
770 #[schema(value_type = Option<String>, format = "uuid")]
771 pub order_id: Option<stateset_primitives::OrderId>,
772 pub invoice_type: Option<String>,
773 pub due_date: Option<String>,
774 pub payment_terms: Option<String>,
775 pub currency: Option<String>,
776 pub notes: Option<String>,
777}
778
779#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
781pub struct RecordInvoicePaymentRequest {
782 #[schema(value_type = String)]
783 pub amount: Decimal,
784 pub payment_method: Option<String>,
785 pub reference: Option<String>,
786 pub notes: Option<String>,
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
791pub struct InvoiceResponse {
792 #[schema(value_type = String, format = "uuid")]
793 pub id: stateset_primitives::InvoiceId,
794 pub invoice_number: String,
795 #[schema(value_type = String, format = "uuid")]
796 pub customer_id: CustomerId,
797 #[schema(value_type = Option<String>, format = "uuid")]
798 pub order_id: Option<OrderId>,
799 pub status: String,
800 pub invoice_type: String,
801 pub invoice_date: DateTime<Utc>,
802 pub due_date: DateTime<Utc>,
803 pub currency: String,
804 #[schema(value_type = String)]
805 pub subtotal: Decimal,
806 #[schema(value_type = String)]
807 pub tax_amount: Decimal,
808 #[schema(value_type = String)]
809 pub total: Decimal,
810 #[schema(value_type = String)]
811 pub amount_paid: Decimal,
812 #[schema(value_type = String)]
813 pub balance_due: Decimal,
814 pub created_at: DateTime<Utc>,
815 pub updated_at: DateTime<Utc>,
816}
817
818#[derive(Debug, Clone, Serialize, ToSchema)]
820pub struct InvoiceListResponse {
821 pub invoices: Vec<InvoiceResponse>,
822 pub total: usize,
823 pub limit: u32,
824 pub offset: u32,
825 pub has_more: bool,
826}
827
828#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
834pub struct CreateReturnRequest {
835 #[schema(value_type = String, format = "uuid")]
836 pub order_id: OrderId,
837 pub reason: String,
838 pub reason_details: Option<String>,
839 pub items: Vec<CreateReturnItemRequest>,
840 pub notes: Option<String>,
841}
842
843#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
845pub struct CreateReturnItemRequest {
846 pub order_item_id: Uuid,
847 pub quantity: i32,
848 pub condition: Option<String>,
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
853pub struct ReturnResponse {
854 #[schema(value_type = String, format = "uuid")]
855 pub id: ReturnId,
856 #[schema(value_type = String, format = "uuid")]
857 pub order_id: OrderId,
858 #[schema(value_type = String, format = "uuid")]
859 pub customer_id: CustomerId,
860 pub status: String,
861 pub reason: String,
862 #[schema(value_type = Option<String>)]
863 pub refund_amount: Option<Decimal>,
864 pub created_at: DateTime<Utc>,
865 pub updated_at: DateTime<Utc>,
866}
867
868#[derive(Debug, Clone, Serialize, ToSchema)]
870pub struct ReturnListResponse {
871 pub returns: Vec<ReturnResponse>,
872 pub total: usize,
873 pub limit: u32,
874 pub offset: u32,
875 #[serde(skip_serializing_if = "Option::is_none")]
877 pub next_cursor: Option<String>,
878 pub has_more: bool,
880}
881
882#[derive(Debug, Clone, Serialize, ToSchema)]
888pub struct TenantCacheResponse {
889 pub enabled: bool,
890 pub max_cached_dbs: usize,
891 pub cached_dbs: usize,
892 pub in_use_cached_dbs: usize,
893 pub hits: u64,
894 pub misses: u64,
895 pub evictions: u64,
896 pub rejections: u64,
897}
898
899#[derive(Debug, Clone, Serialize, ToSchema)]
901pub struct HealthResponse {
902 pub status: &'static str,
903 #[serde(skip_serializing_if = "Option::is_none")]
904 pub tenant_cache: Option<TenantCacheResponse>,
905}
906
907#[derive(Debug, Clone, Serialize, ToSchema)]
909pub struct ReadyResponse {
910 pub status: &'static str,
911 pub database: &'static str,
912 #[serde(skip_serializing_if = "Option::is_none")]
913 pub tenant_cache: Option<TenantCacheResponse>,
914}
915
916#[derive(Debug, Clone, Serialize, ToSchema)]
928pub struct VersionResponse {
929 pub version: &'static str,
931
932 #[serde(skip_serializing_if = "Option::is_none")]
935 pub git_commit: Option<&'static str>,
936
937 #[serde(skip_serializing_if = "Option::is_none")]
939 pub git_ref: Option<&'static str>,
940
941 #[serde(skip_serializing_if = "Option::is_none")]
945 pub release_tag: Option<&'static str>,
946
947 #[serde(skip_serializing_if = "Option::is_none")]
950 pub built_at: Option<&'static str>,
951
952 pub signed: bool,
957}
958
959#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
965#[into_params(parameter_in = Query)]
966pub struct EventStreamParams {
967 pub filter: Option<String>,
969 pub last_event_id: Option<u64>,
974}
975
976impl From<stateset_core::Order> for OrderResponse {
981 fn from(o: stateset_core::Order) -> Self {
982 Self {
983 id: o.id,
984 order_number: o.order_number,
985 customer_id: o.customer_id,
986 status: o.status.to_string(),
987 total_amount: o.total_amount,
988 currency: o.currency.to_string(),
989 payment_status: o.payment_status.to_string(),
990 fulfillment_status: o.fulfillment_status.to_string(),
991 items: o.items.into_iter().map(OrderItemResponse::from).collect(),
992 created_at: o.created_at,
993 updated_at: o.updated_at,
994 }
995 }
996}
997
998impl From<stateset_core::OrderItem> for OrderItemResponse {
999 fn from(i: stateset_core::OrderItem) -> Self {
1000 Self {
1001 id: *i.id.as_uuid(),
1002 product_id: i.product_id,
1003 sku: i.sku,
1004 name: i.name,
1005 quantity: i.quantity,
1006 unit_price: i.unit_price,
1007 total: i.total,
1008 }
1009 }
1010}
1011
1012impl From<stateset_core::Customer> for CustomerResponse {
1013 fn from(c: stateset_core::Customer) -> Self {
1014 Self {
1015 id: c.id,
1016 email: c.email,
1017 first_name: c.first_name,
1018 last_name: c.last_name,
1019 phone: c.phone,
1020 status: c.status.to_string(),
1021 accepts_marketing: c.accepts_marketing,
1022 created_at: c.created_at,
1023 updated_at: c.updated_at,
1024 }
1025 }
1026}
1027
1028impl From<stateset_core::Product> for ProductResponse {
1029 fn from(p: stateset_core::Product) -> Self {
1030 Self {
1031 id: p.id,
1032 name: p.name,
1033 slug: p.slug,
1034 description: p.description,
1035 status: p.status.to_string(),
1036 product_type: p.product_type.to_string(),
1037 created_at: p.created_at,
1038 updated_at: p.updated_at,
1039 }
1040 }
1041}
1042
1043impl From<stateset_core::StockLevel> for InventoryResponse {
1044 fn from(s: stateset_core::StockLevel) -> Self {
1045 Self {
1046 sku: s.sku,
1047 name: s.name,
1048 total_on_hand: s.total_on_hand,
1049 total_allocated: s.total_allocated,
1050 total_available: s.total_available,
1051 }
1052 }
1053}
1054
1055impl From<stateset_core::InventoryItem> for InventoryItemResponse {
1056 fn from(i: stateset_core::InventoryItem) -> Self {
1057 Self {
1058 id: i.id,
1059 sku: i.sku,
1060 name: i.name,
1061 description: i.description,
1062 unit_of_measure: i.unit_of_measure,
1063 is_active: i.is_active,
1064 created_at: i.created_at,
1065 updated_at: i.updated_at,
1066 }
1067 }
1068}
1069
1070impl From<stateset_core::Shipment> for ShipmentResponse {
1071 fn from(s: stateset_core::Shipment) -> Self {
1072 Self {
1073 id: s.id,
1074 shipment_number: s.shipment_number,
1075 order_id: s.order_id,
1076 status: s.status.to_string(),
1077 carrier: s.carrier.to_string(),
1078 shipping_method: s.shipping_method.to_string(),
1079 tracking_number: s.tracking_number,
1080 tracking_url: s.tracking_url,
1081 recipient_name: s.recipient_name,
1082 shipping_cost: s.shipping_cost,
1083 shipped_at: s.shipped_at,
1084 estimated_delivery: s.estimated_delivery,
1085 delivered_at: s.delivered_at,
1086 created_at: s.created_at,
1087 updated_at: s.updated_at,
1088 }
1089 }
1090}
1091
1092impl From<stateset_core::Payment> for PaymentResponse {
1093 fn from(p: stateset_core::Payment) -> Self {
1094 Self {
1095 id: p.id,
1096 payment_number: p.payment_number,
1097 order_id: p.order_id,
1098 customer_id: p.customer_id.map(|c| c.to_string()),
1099 status: p.status.to_string(),
1100 payment_method: p.payment_method.to_string(),
1101 amount: p.amount,
1102 currency: p.currency.to_string(),
1103 amount_refunded: p.amount_refunded,
1104 external_id: p.external_id,
1105 processor: p.processor,
1106 created_at: p.created_at,
1107 updated_at: p.updated_at,
1108 }
1109 }
1110}
1111
1112impl From<stateset_core::Invoice> for InvoiceResponse {
1113 fn from(i: stateset_core::Invoice) -> Self {
1114 Self {
1115 id: i.id,
1116 invoice_number: i.invoice_number,
1117 customer_id: i.customer_id,
1118 order_id: i.order_id,
1119 status: i.status.to_string(),
1120 invoice_type: i.invoice_type.to_string(),
1121 invoice_date: i.invoice_date,
1122 due_date: i.due_date,
1123 currency: i.currency.to_string(),
1124 subtotal: i.subtotal,
1125 tax_amount: i.tax_amount,
1126 total: i.total,
1127 amount_paid: i.amount_paid,
1128 balance_due: i.balance_due,
1129 created_at: i.created_at,
1130 updated_at: i.updated_at,
1131 }
1132 }
1133}
1134
1135impl From<stateset_core::Return> for ReturnResponse {
1136 fn from(r: stateset_core::Return) -> Self {
1137 Self {
1138 id: r.id,
1139 order_id: r.order_id,
1140 customer_id: r.customer_id,
1141 status: r.status.to_string(),
1142 reason: r.reason.to_string(),
1143 refund_amount: r.refund_amount,
1144 created_at: r.created_at,
1145 updated_at: r.updated_at,
1146 }
1147 }
1148}
1149
1150impl From<AddressDto> for stateset_core::Address {
1151 fn from(a: AddressDto) -> Self {
1152 Self {
1153 line1: a.line1,
1154 line2: a.line2,
1155 city: a.city,
1156 state: a.state,
1157 postal_code: a.postal_code,
1158 country: a.country,
1159 }
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use rust_decimal_macros::dec;
1167
1168 #[test]
1173 fn pagination_default_limit() {
1174 let p = PaginationParams::default();
1175 assert_eq!(p.resolved_limit(), PaginationParams::DEFAULT_LIMIT);
1176 assert_eq!(p.resolved_offset(), 0);
1177 }
1178
1179 #[test]
1180 fn pagination_custom_limit() {
1181 let p = PaginationParams { limit: Some(10), offset: Some(20) };
1182 assert_eq!(p.resolved_limit(), 10);
1183 assert_eq!(p.resolved_offset(), 20);
1184 }
1185
1186 #[test]
1187 fn pagination_clamps_to_max() {
1188 let p = PaginationParams { limit: Some(999), offset: None };
1189 assert_eq!(p.resolved_limit(), PaginationParams::MAX_LIMIT);
1190 }
1191
1192 #[test]
1193 fn pagination_clamps_zero_to_one() {
1194 let p = PaginationParams { limit: Some(0), offset: None };
1195 assert_eq!(p.resolved_limit(), 1);
1196 }
1197
1198 #[test]
1207 fn payment_amount_accepts_exact_decimal_string() {
1208 let req: CreatePaymentRequest = serde_json::from_str(
1209 r#"{"order_id":"01234567-89ab-cdef-0123-456789abcdef","amount":"123.456789012345678901"}"#,
1210 )
1211 .expect("string-encoded amount must deserialize exactly");
1212 assert_eq!(req.amount.to_string(), "123.456789012345678901");
1213 }
1214
1215 #[test]
1216 fn refund_amount_accepts_exact_decimal_string() {
1217 let req: CreateRefundRequest = serde_json::from_str(r#"{"amount":"0.30"}"#)
1218 .expect("string-encoded amount must deserialize exactly");
1219 assert_eq!(req.amount.to_string(), "0.30");
1220 }
1221
1222 #[test]
1223 fn refund_amount_still_accepts_json_number() {
1224 let req: CreateRefundRequest = serde_json::from_str(r#"{"amount":49.99}"#)
1225 .expect("plain JSON number amount must keep deserializing");
1226 assert_eq!(req.amount.to_string(), "49.99");
1227 }
1228
1229 #[test]
1230 fn invoice_payment_amount_accepts_exact_decimal_string() {
1231 let req: RecordInvoicePaymentRequest =
1232 serde_json::from_str(r#"{"amount":"1000000000000.000001"}"#)
1233 .expect("string-encoded amount must deserialize exactly");
1234 assert_eq!(req.amount.to_string(), "1000000000000.000001");
1235 }
1236
1237 #[test]
1242 fn create_order_request_roundtrip() {
1243 let req = CreateOrderRequest {
1244 customer_id: CustomerId::new(),
1245 items: vec![CreateOrderItemRequest {
1246 product_id: ProductId::new(),
1247 variant_id: None,
1248 sku: "SKU-1".into(),
1249 name: "Widget".into(),
1250 quantity: 2,
1251 unit_price: dec!(29.99),
1252 discount: None,
1253 tax_amount: None,
1254 }],
1255 currency: Some("USD".into()),
1256 shipping_address: Some(AddressDto {
1257 line1: "123 Main".into(),
1258 line2: None,
1259 city: "NYC".into(),
1260 state: Some("NY".into()),
1261 postal_code: "10001".into(),
1262 country: "US".into(),
1263 }),
1264 billing_address: None,
1265 notes: None,
1266 payment_method: None,
1267 shipping_method: None,
1268 };
1269 let json = serde_json::to_string(&req).unwrap();
1270 let deser: CreateOrderRequest = serde_json::from_str(&json).unwrap();
1271 assert_eq!(deser.items.len(), 1);
1272 assert_eq!(deser.items[0].sku, "SKU-1");
1273 }
1274
1275 #[test]
1276 fn create_customer_request_roundtrip() {
1277 let req = CreateCustomerRequest {
1278 email: "test@example.com".into(),
1279 first_name: "John".into(),
1280 last_name: "Doe".into(),
1281 phone: None,
1282 accepts_marketing: Some(true),
1283 tags: None,
1284 metadata: None,
1285 };
1286 let json = serde_json::to_string(&req).unwrap();
1287 let deser: CreateCustomerRequest = serde_json::from_str(&json).unwrap();
1288 assert_eq!(deser.email, "test@example.com");
1289 }
1290
1291 #[test]
1292 fn create_product_request_roundtrip() {
1293 let req = CreateProductRequest {
1294 name: "Widget".into(),
1295 slug: Some("widget".into()),
1296 description: Some("A fine widget".into()),
1297 product_type: None,
1298 };
1299 let json = serde_json::to_string(&req).unwrap();
1300 let deser: CreateProductRequest = serde_json::from_str(&json).unwrap();
1301 assert_eq!(deser.name, "Widget");
1302 }
1303
1304 #[test]
1305 fn inventory_adjust_request_roundtrip() {
1306 let req = InventoryAdjustRequest {
1307 quantity: dec!(-5),
1308 reason: "Damaged".into(),
1309 location_id: Some(1),
1310 };
1311 let json = serde_json::to_string(&req).unwrap();
1312 let deser: InventoryAdjustRequest = serde_json::from_str(&json).unwrap();
1313 assert_eq!(deser.quantity, dec!(-5));
1314 }
1315
1316 #[test]
1317 fn create_return_request_roundtrip() {
1318 let req = CreateReturnRequest {
1319 order_id: OrderId::new(),
1320 reason: "defective".into(),
1321 reason_details: None,
1322 items: vec![CreateReturnItemRequest {
1323 order_item_id: Uuid::new_v4(),
1324 quantity: 1,
1325 condition: Some("damaged".into()),
1326 }],
1327 notes: None,
1328 };
1329 let json = serde_json::to_string(&req).unwrap();
1330 let deser: CreateReturnRequest = serde_json::from_str(&json).unwrap();
1331 assert_eq!(deser.items.len(), 1);
1332 }
1333
1334 #[test]
1335 fn health_response_serialization() {
1336 let resp = HealthResponse {
1337 status: "ok",
1338 tenant_cache: Some(TenantCacheResponse {
1339 enabled: true,
1340 max_cached_dbs: 256,
1341 cached_dbs: 3,
1342 in_use_cached_dbs: 1,
1343 hits: 20,
1344 misses: 4,
1345 evictions: 2,
1346 rejections: 1,
1347 }),
1348 };
1349 let json = serde_json::to_value(&resp).unwrap();
1350 assert_eq!(json["status"], "ok");
1351 assert_eq!(json["tenant_cache"]["cached_dbs"], 3);
1352 }
1353
1354 #[test]
1355 fn ready_response_serialization() {
1356 let resp = ReadyResponse {
1357 status: "ok",
1358 database: "connected",
1359 tenant_cache: Some(TenantCacheResponse {
1360 enabled: false,
1361 max_cached_dbs: 256,
1362 cached_dbs: 0,
1363 in_use_cached_dbs: 0,
1364 hits: 0,
1365 misses: 0,
1366 evictions: 0,
1367 rejections: 0,
1368 }),
1369 };
1370 let json = serde_json::to_value(&resp).unwrap();
1371 assert_eq!(json["database"], "connected");
1372 assert_eq!(json["tenant_cache"]["enabled"], false);
1373 }
1374
1375 #[test]
1376 fn order_response_serialization() {
1377 let resp = OrderResponse {
1378 id: OrderId::new(),
1379 order_number: "ORD-001".into(),
1380 customer_id: CustomerId::new(),
1381 status: "pending".into(),
1382 total_amount: dec!(59.98),
1383 currency: "USD".into(),
1384 payment_status: "pending".into(),
1385 fulfillment_status: "unfulfilled".into(),
1386 items: vec![],
1387 created_at: Utc::now(),
1388 updated_at: Utc::now(),
1389 };
1390 let json = serde_json::to_value(&resp).unwrap();
1391 assert_eq!(json["status"], "pending");
1392 }
1393
1394 #[test]
1395 fn customer_response_serialization() {
1396 let resp = CustomerResponse {
1397 id: CustomerId::new(),
1398 email: "a@b.com".into(),
1399 first_name: "A".into(),
1400 last_name: "B".into(),
1401 phone: None,
1402 status: "active".into(),
1403 accepts_marketing: false,
1404 created_at: Utc::now(),
1405 updated_at: Utc::now(),
1406 };
1407 let json = serde_json::to_value(&resp).unwrap();
1408 assert_eq!(json["email"], "a@b.com");
1409 }
1410
1411 #[test]
1412 fn product_response_serialization() {
1413 let resp = ProductResponse {
1414 id: ProductId::new(),
1415 name: "W".into(),
1416 slug: "w".into(),
1417 description: "d".into(),
1418 status: "draft".into(),
1419 product_type: "simple".into(),
1420 created_at: Utc::now(),
1421 updated_at: Utc::now(),
1422 };
1423 let json = serde_json::to_value(&resp).unwrap();
1424 assert_eq!(json["name"], "W");
1425 }
1426
1427 #[test]
1428 fn inventory_response_serialization() {
1429 let resp = InventoryResponse {
1430 sku: "SKU-1".into(),
1431 name: "Widget".into(),
1432 total_on_hand: dec!(100),
1433 total_allocated: dec!(10),
1434 total_available: dec!(90),
1435 };
1436 let json = serde_json::to_value(&resp).unwrap();
1437 assert_eq!(json["sku"], "SKU-1");
1438 }
1439
1440 #[test]
1441 fn return_response_serialization() {
1442 let resp = ReturnResponse {
1443 id: ReturnId::new(),
1444 order_id: OrderId::new(),
1445 customer_id: CustomerId::new(),
1446 status: "requested".into(),
1447 reason: "defective".into(),
1448 refund_amount: Some(dec!(29.99)),
1449 created_at: Utc::now(),
1450 updated_at: Utc::now(),
1451 };
1452 let json = serde_json::to_value(&resp).unwrap();
1453 assert_eq!(json["status"], "requested");
1454 }
1455
1456 #[test]
1457 fn address_dto_converts_to_core() {
1458 let dto = AddressDto {
1459 line1: "123 Main".into(),
1460 line2: None,
1461 city: "NYC".into(),
1462 state: Some("NY".into()),
1463 postal_code: "10001".into(),
1464 country: "US".into(),
1465 };
1466 let core: stateset_core::Address = dto.into();
1467 assert_eq!(core.city, "NYC");
1468 }
1469
1470 #[test]
1471 fn event_stream_params_default() {
1472 let p = EventStreamParams::default();
1473 assert!(p.filter.is_none());
1474 }
1475
1476 #[test]
1477 fn order_list_response_serialization() {
1478 let resp = OrderListResponse {
1479 orders: vec![],
1480 total: 0,
1481 limit: 50,
1482 offset: 0,
1483 next_cursor: None,
1484 has_more: false,
1485 };
1486 let json = serde_json::to_value(&resp).unwrap();
1487 assert_eq!(json["total"], 0);
1488 assert_eq!(json["has_more"], false);
1489 assert!(json.get("next_cursor").is_none()); }
1491
1492 #[test]
1493 fn customer_list_response_serialization() {
1494 let resp = CustomerListResponse {
1495 customers: vec![],
1496 total: 0,
1497 limit: 50,
1498 offset: 0,
1499 next_cursor: None,
1500 has_more: false,
1501 };
1502 let json = serde_json::to_value(&resp).unwrap();
1503 assert_eq!(json["total"], 0);
1504 assert_eq!(json["has_more"], false);
1505 }
1506
1507 #[test]
1508 fn product_list_response_serialization() {
1509 let resp = ProductListResponse {
1510 products: vec![],
1511 total: 0,
1512 limit: 50,
1513 offset: 0,
1514 next_cursor: None,
1515 has_more: false,
1516 };
1517 let json = serde_json::to_value(&resp).unwrap();
1518 assert_eq!(json["total"], 0);
1519 assert_eq!(json["has_more"], false);
1520 }
1521
1522 #[test]
1523 fn return_list_response_serialization() {
1524 let resp = ReturnListResponse {
1525 returns: vec![],
1526 total: 0,
1527 limit: 50,
1528 offset: 0,
1529 next_cursor: None,
1530 has_more: false,
1531 };
1532 let json = serde_json::to_value(&resp).unwrap();
1533 assert_eq!(json["total"], 0);
1534 assert_eq!(json["has_more"], false);
1535 }
1536
1537 #[test]
1542 fn cursor_encode_decode_roundtrip() {
1543 let cursor = encode_cursor("2024-01-15T10:30:00Z", "550e8400-e29b-41d4-a716-446655440000");
1544 let (sort_key, id) = decode_cursor(&cursor).unwrap();
1545 assert_eq!(sort_key, "2024-01-15T10:30:00Z");
1546 assert_eq!(id, "550e8400-e29b-41d4-a716-446655440000");
1547 }
1548
1549 #[test]
1550 fn cursor_decode_invalid_base64() {
1551 assert!(decode_cursor("not-valid-base64!!!").is_none());
1552 }
1553
1554 #[test]
1555 fn cursor_decode_missing_separator() {
1556 let encoded = URL_SAFE_NO_PAD.encode(b"no-separator-here");
1557 assert!(decode_cursor(&encoded).is_none());
1558 }
1559
1560 #[test]
1561 fn overfetch_limit_adds_one_row() {
1562 assert_eq!(overfetch_limit(10), 11);
1563 assert_eq!(overfetch_limit(PaginationParams::MAX_LIMIT), PaginationParams::MAX_LIMIT + 1);
1564 }
1565
1566 #[test]
1567 fn finalize_page_marks_exact_boundary_as_not_has_more() {
1568 let mut items = vec![1, 2];
1569 let has_more = finalize_page(&mut items, 2);
1570 assert!(!has_more);
1571 assert_eq!(items, vec![1, 2]);
1572 }
1573
1574 #[test]
1575 fn finalize_page_trims_overfetch_and_sets_has_more() {
1576 let mut items = vec![1, 2, 3];
1577 let has_more = finalize_page(&mut items, 2);
1578 assert!(has_more);
1579 assert_eq!(items, vec![1, 2]);
1580 }
1581
1582 #[test]
1583 fn cursor_next_cursor_serialized_when_present() {
1584 let resp = OrderListResponse {
1585 orders: vec![],
1586 total: 100,
1587 limit: 10,
1588 offset: 0,
1589 next_cursor: Some("abc123".into()),
1590 has_more: true,
1591 };
1592 let json = serde_json::to_value(&resp).unwrap();
1593 assert_eq!(json["next_cursor"], "abc123");
1594 assert_eq!(json["has_more"], true);
1595 }
1596
1597 #[test]
1602 fn order_filter_params_default() {
1603 let p = OrderFilterParams::default();
1604 assert_eq!(p.resolved_limit(), PaginationParams::DEFAULT_LIMIT);
1605 assert_eq!(p.resolved_offset(), 0);
1606 assert!(p.customer_id.is_none());
1607 assert!(p.status.is_none());
1608 }
1609
1610 #[test]
1611 fn order_filter_params_deserialization() {
1612 let p: OrderFilterParams = serde_json::from_value(serde_json::json!({
1613 "limit": 10, "offset": 5, "status": "pending", "customer_id": "abc"
1614 }))
1615 .unwrap();
1616 assert_eq!(p.resolved_limit(), 10);
1617 assert_eq!(p.resolved_offset(), 5);
1618 assert_eq!(p.status.as_deref(), Some("pending"));
1619 assert_eq!(p.customer_id.as_deref(), Some("abc"));
1620 }
1621
1622 #[test]
1623 fn customer_filter_params_deserialization() {
1624 let p: CustomerFilterParams = serde_json::from_value(serde_json::json!({
1625 "email": "test@example.com", "accepts_marketing": true
1626 }))
1627 .unwrap();
1628 assert_eq!(p.email.as_deref(), Some("test@example.com"));
1629 assert_eq!(p.accepts_marketing, Some(true));
1630 }
1631
1632 #[test]
1633 fn product_filter_params_deserialization() {
1634 let p: ProductFilterParams = serde_json::from_value(serde_json::json!({
1635 "search": "widget", "min_price": "10.00", "max_price": "100", "in_stock": true
1636 }))
1637 .unwrap();
1638 assert_eq!(p.search.as_deref(), Some("widget"));
1639 assert_eq!(p.min_price.as_deref(), Some("10.00"));
1640 assert_eq!(p.max_price.as_deref(), Some("100"));
1641 assert_eq!(p.in_stock, Some(true));
1642 }
1643
1644 #[test]
1645 fn return_filter_params_deserialization() {
1646 let p: ReturnFilterParams = serde_json::from_value(serde_json::json!({
1647 "status": "requested", "reason": "defective", "limit": 5
1648 }))
1649 .unwrap();
1650 assert_eq!(p.status.as_deref(), Some("requested"));
1651 assert_eq!(p.reason.as_deref(), Some("defective"));
1652 assert_eq!(p.resolved_limit(), 5);
1653 }
1654
1655 #[test]
1656 fn filter_params_limit_clamps_to_max() {
1657 let p = OrderFilterParams { limit: Some(999), ..Default::default() };
1658 assert_eq!(p.resolved_limit(), PaginationParams::MAX_LIMIT);
1659 }
1660
1661 #[test]
1662 fn filter_params_limit_clamps_zero_to_one() {
1663 let p = OrderFilterParams { limit: Some(0), ..Default::default() };
1664 assert_eq!(p.resolved_limit(), 1);
1665 }
1666}