Skip to main content

stateset_core/models/
integration_mapping.rs

1//! Integration mapping domain models
2//!
3//! An integration mapping translates a value from an external system into the
4//! internal canonical value (and vice-versa) for a given integration and
5//! mapping group — e.g. mapping a Shopify carrier name to a Trackstar carrier
6//! enum, or an external order status to an internal one. Mappings are unique on
7//! `(integration, mapping_group, field_name, external_value)`.
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use stateset_primitives::IntegrationMappingId;
12
13/// A single external→internal value mapping for an integration.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct IntegrationMapping {
16    /// Unique mapping ID.
17    pub id: IntegrationMappingId,
18    /// Backing integration (e.g. `shopify`, `trackstar`).
19    pub integration: String,
20    /// Logical mapping group (e.g. `carrier`, `order_status`, `payment_method`).
21    pub mapping_group: String,
22    /// Field name within the group (e.g. `carrier_code`).
23    pub field_name: String,
24    /// The value as seen in the external system.
25    pub external_value: String,
26    /// The canonical internal value it maps to.
27    pub internal_value: String,
28    /// Whether the mapping is active.
29    pub is_active: bool,
30    /// When the mapping was created.
31    pub created_at: DateTime<Utc>,
32    /// When the mapping was last updated.
33    pub updated_at: DateTime<Utc>,
34}
35
36/// Input for creating an integration mapping.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct CreateIntegrationMapping {
39    /// Backing integration.
40    pub integration: String,
41    /// Mapping group.
42    pub mapping_group: String,
43    /// Field name.
44    pub field_name: String,
45    /// External value.
46    pub external_value: String,
47    /// Internal value.
48    pub internal_value: String,
49}
50
51/// Input for updating an integration mapping (partial).
52#[derive(Debug, Clone, Serialize, Deserialize, Default)]
53pub struct UpdateIntegrationMapping {
54    /// Updated internal value.
55    pub internal_value: Option<String>,
56    /// Updated active state.
57    pub is_active: Option<bool>,
58}
59
60/// Filter for listing integration mappings.
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct IntegrationMappingFilter {
63    /// Filter by integration.
64    pub integration: Option<String>,
65    /// Filter by mapping group.
66    pub mapping_group: Option<String>,
67    /// Filter by field name.
68    pub field_name: Option<String>,
69    /// Filter by active state.
70    pub is_active: Option<bool>,
71    /// Maximum results.
72    pub limit: Option<u32>,
73    /// Offset for pagination.
74    pub offset: Option<u32>,
75}
76
77/// A lookup key for resolving a mapping.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MappingLookup {
80    /// Integration.
81    pub integration: String,
82    /// Mapping group.
83    pub mapping_group: String,
84    /// Field name.
85    pub field_name: String,
86    /// External value to resolve.
87    pub external_value: String,
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn create_input_round_trips_via_serde() {
96        let input = CreateIntegrationMapping {
97            integration: "shopify".into(),
98            mapping_group: "carrier".into(),
99            field_name: "carrier_code".into(),
100            external_value: "USPS Ground".into(),
101            internal_value: "usps".into(),
102        };
103        let json = serde_json::to_string(&input).unwrap();
104        let back: CreateIntegrationMapping = serde_json::from_str(&json).unwrap();
105        assert_eq!(back.external_value, "USPS Ground");
106        assert_eq!(back.internal_value, "usps");
107    }
108
109    #[test]
110    fn update_defaults_are_none() {
111        let u = UpdateIntegrationMapping::default();
112        assert!(u.internal_value.is_none());
113        assert!(u.is_active.is_none());
114    }
115
116    #[test]
117    fn filter_defaults_are_none() {
118        let f = IntegrationMappingFilter::default();
119        assert!(f.integration.is_none());
120        assert!(f.mapping_group.is_none());
121        assert!(f.limit.is_none());
122    }
123}