Skip to main content

gingr/mapping/
retail.rs

1use crate::dto;
2use domain::retail;
3
4use super::{Error, ProviderField, Result};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7/// Retail mapping candidate produced from Gingr item DTO fields.
8pub struct ProductCandidate {
9    /// Gingr retail item identifier kept as source evidence for the mapped product.
10    pub provider_item_id: dto::retail::ItemId,
11    /// Provider display label retained for operator context; NVA-specific naming rules are applied downstream.
12    pub name: retail::product::Name,
13    /// Retail product mapped from a Gingr item DTO for inventory and upsell workflows.
14    pub product: retail::Product,
15    /// Provider status string preserved as source evidence until NVA validates a semantic status.
16    pub status: retail::OfferingStatus,
17}
18
19/// Extracts retail fields Gingr exposed for product matching and merchandising.
20pub fn product_candidate(item: &dto::retail::Item) -> Result<ProductCandidate> {
21    let name = item
22        .name
23        .as_deref()
24        .ok_or(Error::MissingRequiredProviderField {
25            field: ProviderField::RetailItemName,
26        })?;
27    let name = retail::product::Name::try_new(name).map_err(|err| Error::InvalidDomainValue {
28        field: ProviderField::RetailItemName,
29        reason: err.to_string(),
30    })?;
31
32    let sku = item
33        .sku
34        .as_deref()
35        .ok_or(Error::MissingRequiredProviderField {
36            field: ProviderField::RetailItemSku,
37        })?;
38    let sku = retail::Sku::try_new(sku).map_err(|err| Error::InvalidDomainValue {
39        field: ProviderField::RetailItemSku,
40        reason: err.to_string(),
41    })?;
42
43    let category = item
44        .category
45        .as_deref()
46        .map(promote_category)
47        .transpose()?
48        .unwrap_or(retail::product::Category::PersonalizedUpsell);
49    let status = if item.active.unwrap_or(true) {
50        retail::OfferingStatus::Active
51    } else {
52        retail::OfferingStatus::Inactive
53    };
54
55    Ok(ProductCandidate {
56        provider_item_id: item.id,
57        name,
58        product: retail::Product::new(sku, category),
59        status,
60    })
61}
62
63fn promote_category(value: &str) -> Result<retail::product::Category> {
64    match value.trim().to_ascii_lowercase().as_str() {
65        "supplement" | "supplements" => Ok(retail::product::Category::Supplement),
66        "in_house_diet" | "in-house diet" | "in house diet" | "food" | "diet" => {
67            Ok(retail::product::Category::InHouseDiet)
68        }
69        "personalized_upsell" | "personalized upsell" | "upsell" | "retail" => {
70            Ok(retail::product::Category::PersonalizedUpsell)
71        }
72        _ => Err(Error::InvalidDomainValue {
73            field: ProviderField::RetailItemCategory,
74            reason: format!("unsupported Gingr retail category {value:?}"),
75        }),
76    }
77}