Skip to main content

coil_customer_sdk/
types.rs

1use crate::{BackendError, BackendErrorKind};
2use std::collections::BTreeMap;
3
4pub type MetadataMap = BTreeMap<String, String>;
5pub type Headers = BTreeMap<String, String>;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CustomerPluginDescriptor {
9    pub id: String,
10    pub display_name: String,
11    pub version: String,
12    pub documentation_url: Option<String>,
13}
14
15impl CustomerPluginDescriptor {
16    pub fn new(
17        id: impl Into<String>,
18        display_name: impl Into<String>,
19        version: impl Into<String>,
20    ) -> Self {
21        Self {
22            id: id.into(),
23            display_name: display_name.into(),
24            version: version.into(),
25            documentation_url: None,
26        }
27    }
28
29    pub fn with_documentation_url(mut self, documentation_url: impl Into<String>) -> Self {
30        self.documentation_url = Some(documentation_url.into());
31        self
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct CustomerAppContext {
37    pub app_id: String,
38    pub environment: String,
39    pub site_id: Option<String>,
40    pub locale: Option<String>,
41}
42
43impl CustomerAppContext {
44    pub fn new(app_id: impl Into<String>, environment: impl Into<String>) -> Self {
45        Self {
46            app_id: app_id.into(),
47            environment: environment.into(),
48            site_id: None,
49            locale: None,
50        }
51    }
52
53    pub fn with_site_id(mut self, site_id: impl Into<String>) -> Self {
54        self.site_id = Some(site_id.into());
55        self
56    }
57
58    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
59        self.locale = Some(locale.into());
60        self
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum PrincipalKind {
66    Anonymous,
67    User,
68    ServiceAccount,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct PrincipalContext {
73    pub kind: PrincipalKind,
74    pub id: Option<String>,
75}
76
77impl PrincipalContext {
78    pub fn anonymous() -> Self {
79        Self {
80            kind: PrincipalKind::Anonymous,
81            id: None,
82        }
83    }
84
85    pub fn user(id: impl Into<String>) -> Self {
86        Self {
87            kind: PrincipalKind::User,
88            id: Some(id.into()),
89        }
90    }
91
92    pub fn service_account(id: impl Into<String>) -> Self {
93        Self {
94            kind: PrincipalKind::ServiceAccount,
95            id: Some(id.into()),
96        }
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct TraceContext {
102    pub trace_id: String,
103    pub request_id: Option<String>,
104}
105
106impl TraceContext {
107    pub fn new(trace_id: impl Into<String>) -> Self {
108        Self {
109            trace_id: trace_id.into(),
110            request_id: None,
111        }
112    }
113
114    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
115        self.request_id = Some(request_id.into());
116        self
117    }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct RequestContext {
122    pub customer_app: CustomerAppContext,
123    pub principal: PrincipalContext,
124    pub trace: TraceContext,
125}
126
127impl RequestContext {
128    pub fn new(
129        customer_app: CustomerAppContext,
130        principal: PrincipalContext,
131        trace: TraceContext,
132    ) -> Self {
133        Self {
134            customer_app,
135            principal,
136            trace,
137        }
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct MoneyAmount {
143    pub currency_code: String,
144    pub minor_units: i64,
145}
146
147impl MoneyAmount {
148    pub fn new(currency_code: impl Into<String>, minor_units: i64) -> Self {
149        Self {
150            currency_code: currency_code.into(),
151            minor_units,
152        }
153    }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct OrderLineDraft {
158    pub sku: String,
159    pub title: String,
160    pub quantity: u32,
161    pub unit_price: MoneyAmount,
162    pub product_kind: String,
163    pub collection_handle: Option<String>,
164    pub entitlement_key: Option<String>,
165    pub metadata: MetadataMap,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct OrderDraft {
170    pub order_id: String,
171    pub currency_code: String,
172    pub subtotal: MoneyAmount,
173    pub total: MoneyAmount,
174    pub lines: Vec<OrderLineDraft>,
175    pub metadata: MetadataMap,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct OrderRejection {
180    pub code: String,
181    pub message: String,
182}
183
184impl OrderRejection {
185    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
186        Self {
187            code: code.into(),
188            message: message.into(),
189        }
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct OrderAdjustment {
195    pub reason: String,
196    pub metadata: MetadataMap,
197}
198
199impl OrderAdjustment {
200    pub fn new(reason: impl Into<String>) -> Self {
201        Self {
202            reason: reason.into(),
203            metadata: MetadataMap::new(),
204        }
205    }
206
207    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
208        self.metadata.insert(key.into(), value.into());
209        self
210    }
211
212    pub fn with_metadata_entries<I, K, V>(mut self, entries: I) -> Self
213    where
214        I: IntoIterator<Item = (K, V)>,
215        K: Into<String>,
216        V: Into<String>,
217    {
218        for (key, value) in entries {
219            self.metadata.insert(key.into(), value.into());
220        }
221        self
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum OrderReviewDecision {
227    Approved,
228    Rejected(OrderRejection),
229    Adjusted(OrderAdjustment),
230}
231
232impl OrderReviewDecision {
233    pub const fn approved() -> Self {
234        Self::Approved
235    }
236
237    pub fn rejected(code: impl Into<String>, message: impl Into<String>) -> Self {
238        Self::Rejected(OrderRejection::new(code, message))
239    }
240
241    pub fn adjusted(reason: impl Into<String>) -> Self {
242        Self::Adjusted(OrderAdjustment::new(reason))
243    }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct CmsPageDraft {
248    pub page_id: String,
249    pub slug: String,
250    pub title: String,
251    pub summary: String,
252    pub body_html: String,
253    pub locale: Option<String>,
254    pub metadata: MetadataMap,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub enum CmsPublishDecision {
259    Allow,
260    Reject { code: String, message: String },
261}
262
263impl CmsPublishDecision {
264    pub fn reject(code: impl Into<String>, message: impl Into<String>) -> Self {
265        Self::Reject {
266            code: code.into(),
267            message: message.into(),
268        }
269    }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct VerifiedWebhook {
274    pub source: String,
275    pub event: String,
276    pub headers: Headers,
277    pub content_type: Option<String>,
278    pub payload: Vec<u8>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum WebhookHandlingResult {
283    Accepted { detail: Option<String> },
284    Rejected { code: String, message: String },
285}
286
287impl WebhookHandlingResult {
288    pub fn accepted(detail: Option<String>) -> Self {
289        Self::Accepted { detail }
290    }
291
292    pub fn rejected(code: impl Into<String>, message: impl Into<String>) -> Self {
293        Self::Rejected {
294            code: code.into(),
295            message: message.into(),
296        }
297    }
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct JobRequest {
302    pub queue: String,
303    pub job_name: String,
304    pub idempotency_key: Option<String>,
305    pub payload_description: String,
306    pub metadata: MetadataMap,
307}
308
309impl JobRequest {
310    pub fn new(
311        queue: impl Into<String>,
312        job_name: impl Into<String>,
313        payload_description: impl Into<String>,
314    ) -> Self {
315        Self {
316            queue: queue.into(),
317            job_name: job_name.into(),
318            idempotency_key: None,
319            payload_description: payload_description.into(),
320            metadata: MetadataMap::new(),
321        }
322    }
323
324    pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
325        self.idempotency_key = Some(idempotency_key.into());
326        self
327    }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct JobReceipt {
332    pub queue: String,
333    pub job_id: String,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct RepositoryQuery {
338    pub repository: String,
339    pub key: Option<String>,
340    pub filters: MetadataMap,
341}
342
343impl RepositoryQuery {
344    pub fn new(repository: impl Into<String>) -> Self {
345        Self {
346            repository: repository.into(),
347            key: None,
348            filters: MetadataMap::new(),
349        }
350    }
351
352    pub fn with_key(mut self, key: impl Into<String>) -> Self {
353        self.key = Some(key.into());
354        self
355    }
356
357    pub fn with_filter(mut self, field: impl Into<String>, value: impl Into<String>) -> Self {
358        self.filters.insert(field.into(), value.into());
359        self
360    }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct RepositoryRecord {
365    pub id: String,
366    pub fields: MetadataMap,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct RepositoryRecordSet {
371    pub repository: String,
372    pub records: Vec<RepositoryRecord>,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct RepositoryWrite {
377    pub repository: String,
378    pub record_id: String,
379    pub fields: MetadataMap,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct RepositoryWriteReceipt {
384    pub repository: String,
385    pub record_id: String,
386    pub version: Option<String>,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct CmsPageRecord {
391    pub page_id: String,
392    pub title: String,
393    pub slug: String,
394    pub summary: String,
395    pub body_html: String,
396    pub status: String,
397    pub live_path: Option<String>,
398}
399
400impl CmsPageRecord {
401    pub const REPOSITORY: &'static str = "cms.pages";
402
403    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
404        Ok(Self {
405            page_id: record.id.clone(),
406            title: required_repository_field(record, "title")?,
407            slug: required_repository_field(record, "slug")?,
408            summary: required_repository_field(record, "summary")?,
409            body_html: required_repository_field(record, "body_html")?,
410            status: required_repository_field(record, "status")?,
411            live_path: optional_repository_field(record, "live_path"),
412        })
413    }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
417pub struct CmsPageUpdate {
418    pub page_id: String,
419    pub title: String,
420    pub slug: String,
421    pub summary: String,
422    pub body_html: String,
423}
424
425impl CmsPageUpdate {
426    pub fn new(
427        page_id: impl Into<String>,
428        title: impl Into<String>,
429        slug: impl Into<String>,
430        summary: impl Into<String>,
431        body_html: impl Into<String>,
432    ) -> Self {
433        Self {
434            page_id: page_id.into(),
435            title: title.into(),
436            slug: slug.into(),
437            summary: summary.into(),
438            body_html: body_html.into(),
439        }
440    }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct CmsNavigationRecord {
445    pub record_id: usize,
446    pub label: String,
447    pub href: String,
448}
449
450impl CmsNavigationRecord {
451    pub const REPOSITORY: &'static str = "cms.navigation";
452
453    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
454        Ok(Self {
455            record_id: record.id.parse::<usize>().map_err(|_| {
456                BackendError::new(
457                    BackendErrorKind::Conflict,
458                    "repository.record.invalid_navigation_id",
459                    format!(
460                        "Repository record `{}` was not a valid CMS navigation record id.",
461                        record.id
462                    ),
463                )
464            })?,
465            label: required_repository_field(record, "label")?,
466            href: required_repository_field(record, "href")?,
467        })
468    }
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct CmsNavigationUpdate {
473    pub record_id: usize,
474    pub label: String,
475    pub href: String,
476}
477
478impl CmsNavigationUpdate {
479    pub fn new(record_id: usize, label: impl Into<String>, href: impl Into<String>) -> Self {
480        Self {
481            record_id,
482            label: label.into(),
483            href: href.into(),
484        }
485    }
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct CmsNavigationAppend {
490    pub label: String,
491    pub href: String,
492}
493
494impl CmsNavigationAppend {
495    pub fn new(label: impl Into<String>, href: impl Into<String>) -> Self {
496        Self {
497            label: label.into(),
498            href: href.into(),
499        }
500    }
501}
502
503#[derive(Debug, Clone, PartialEq, Eq)]
504pub struct CmsRedirectRecord {
505    pub record_id: usize,
506    pub from: String,
507    pub to: String,
508    pub permanent: bool,
509}
510
511impl CmsRedirectRecord {
512    pub const REPOSITORY: &'static str = "cms.redirects";
513
514    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
515        Ok(Self {
516            record_id: record.id.parse::<usize>().map_err(|_| {
517                BackendError::new(
518                    BackendErrorKind::Conflict,
519                    "repository.record.invalid_redirect_id",
520                    format!(
521                        "Repository record `{}` was not a valid CMS redirect record id.",
522                        record.id
523                    ),
524                )
525            })?,
526            from: required_repository_field(record, "from")?,
527            to: required_repository_field(record, "to")?,
528            permanent: required_repository_bool_field(record, "permanent")?,
529        })
530    }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct CmsRedirectUpdate {
535    pub record_id: usize,
536    pub from: String,
537    pub to: String,
538    pub permanent: bool,
539}
540
541impl CmsRedirectUpdate {
542    pub fn new(
543        record_id: usize,
544        from: impl Into<String>,
545        to: impl Into<String>,
546        permanent: bool,
547    ) -> Self {
548        Self {
549            record_id,
550            from: from.into(),
551            to: to.into(),
552            permanent,
553        }
554    }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq)]
558pub struct CmsRedirectAppend {
559    pub from: String,
560    pub to: String,
561    pub permanent: bool,
562}
563
564impl CmsRedirectAppend {
565    pub fn new(from: impl Into<String>, to: impl Into<String>, permanent: bool) -> Self {
566        Self {
567            from: from.into(),
568            to: to.into(),
569            permanent,
570        }
571    }
572}
573
574#[derive(Debug, Clone, PartialEq, Eq)]
575pub struct CommerceCatalogProductRecord {
576    pub handle: String,
577    pub sku: String,
578    pub title: String,
579    pub summary: String,
580    pub price_minor: i64,
581    pub currency: String,
582    pub collection_handle: String,
583    pub is_visible: bool,
584    pub product_kind: String,
585    pub entitlement_key: Option<String>,
586}
587
588impl CommerceCatalogProductRecord {
589    pub const REPOSITORY: &'static str = "commerce.catalog.products";
590
591    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
592        Ok(Self {
593            handle: required_repository_field(record, "handle")?,
594            sku: required_repository_field(record, "sku")?,
595            title: required_repository_field(record, "title")?,
596            summary: required_repository_field(record, "summary")?,
597            price_minor: required_repository_i64_field(record, "price_minor")?,
598            currency: required_repository_field(record, "currency")?,
599            collection_handle: required_repository_field(record, "collection_handle")?,
600            is_visible: required_repository_bool_field(record, "is_visible")?,
601            product_kind: required_repository_field(record, "product_kind")?,
602            entitlement_key: optional_repository_field(record, "entitlement_key"),
603        })
604    }
605}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct CommerceCatalogProductUpdate {
609    pub handle: String,
610    pub title: String,
611    pub summary: String,
612    pub price_minor: i64,
613    pub collection_handle: String,
614    pub is_visible: bool,
615}
616
617impl CommerceCatalogProductUpdate {
618    pub fn new(
619        handle: impl Into<String>,
620        title: impl Into<String>,
621        summary: impl Into<String>,
622        price_minor: i64,
623        collection_handle: impl Into<String>,
624        is_visible: bool,
625    ) -> Self {
626        Self {
627            handle: handle.into(),
628            title: title.into(),
629            summary: summary.into(),
630            price_minor,
631            collection_handle: collection_handle.into(),
632            is_visible,
633        }
634    }
635}
636
637#[derive(Debug, Clone, PartialEq, Eq)]
638pub struct CommerceCatalogCollectionRecord {
639    pub handle: String,
640    pub title: String,
641    pub label: String,
642    pub summary: String,
643    pub is_visible: bool,
644}
645
646impl CommerceCatalogCollectionRecord {
647    pub const REPOSITORY: &'static str = "commerce.catalog.collections";
648
649    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
650        Ok(Self {
651            handle: required_repository_field(record, "handle")?,
652            title: required_repository_field(record, "title")?,
653            label: required_repository_field(record, "label")?,
654            summary: required_repository_field(record, "summary")?,
655            is_visible: required_repository_bool_field(record, "is_visible")?,
656        })
657    }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct CommerceCatalogCollectionUpdate {
662    pub handle: String,
663    pub title: String,
664    pub label: String,
665    pub summary: String,
666    pub is_visible: bool,
667}
668
669impl CommerceCatalogCollectionUpdate {
670    pub fn new(
671        handle: impl Into<String>,
672        title: impl Into<String>,
673        label: impl Into<String>,
674        summary: impl Into<String>,
675        is_visible: bool,
676    ) -> Self {
677        Self {
678            handle: handle.into(),
679            title: title.into(),
680            label: label.into(),
681            summary: summary.into(),
682            is_visible,
683        }
684    }
685}
686
687#[derive(Debug, Clone, PartialEq, Eq)]
688pub struct CommerceOrderRecord {
689    pub order_id: String,
690    pub status: String,
691    pub payment_status: String,
692    pub payment_reference: Option<String>,
693    pub payment_method: Option<String>,
694    pub checkout_email: Option<String>,
695    pub principal_id: Option<String>,
696    pub currency: String,
697    pub total_minor: i64,
698    pub line_count: usize,
699}
700
701impl CommerceOrderRecord {
702    pub const REPOSITORY: &'static str = "commerce.orders";
703
704    pub fn from_repository_record(record: &RepositoryRecord) -> Result<Self, BackendError> {
705        Ok(Self {
706            order_id: record.id.clone(),
707            status: required_repository_field(record, "status")?,
708            payment_status: required_repository_field(record, "payment_status")?,
709            payment_reference: optional_repository_field(record, "payment_reference"),
710            payment_method: optional_repository_field(record, "payment_method"),
711            checkout_email: optional_repository_field(record, "checkout_email"),
712            principal_id: optional_repository_field(record, "principal_id"),
713            currency: required_repository_field(record, "currency")?,
714            total_minor: required_repository_i64_field(record, "total_minor")?,
715            line_count: required_repository_usize_field(record, "line_count")?,
716        })
717    }
718}
719
720fn required_repository_field(
721    record: &RepositoryRecord,
722    field: &str,
723) -> Result<String, BackendError> {
724    record.fields.get(field).cloned().ok_or_else(|| {
725        BackendError::new(
726            BackendErrorKind::Conflict,
727            "repository.record.missing_field",
728            format!(
729                "Repository record `{}` did not expose required field `{field}`.",
730                record.id
731            ),
732        )
733    })
734}
735
736fn optional_repository_field(record: &RepositoryRecord, field: &str) -> Option<String> {
737    record
738        .fields
739        .get(field)
740        .cloned()
741        .and_then(|value| (!value.trim().is_empty()).then_some(value))
742}
743
744fn required_repository_i64_field(
745    record: &RepositoryRecord,
746    field: &str,
747) -> Result<i64, BackendError> {
748    let raw = required_repository_field(record, field)?;
749    raw.parse::<i64>().map_err(|_| {
750        BackendError::new(
751            BackendErrorKind::Conflict,
752            "repository.record.invalid_i64",
753            format!(
754                "Repository record `{}` field `{field}` was not a valid integer.",
755                record.id
756            ),
757        )
758    })
759}
760
761fn required_repository_usize_field(
762    record: &RepositoryRecord,
763    field: &str,
764) -> Result<usize, BackendError> {
765    let raw = required_repository_field(record, field)?;
766    raw.parse::<usize>().map_err(|_| {
767        BackendError::new(
768            BackendErrorKind::Conflict,
769            "repository.record.invalid_usize",
770            format!(
771                "Repository record `{}` field `{field}` was not a valid positive count.",
772                record.id
773            ),
774        )
775    })
776}
777
778fn required_repository_bool_field(
779    record: &RepositoryRecord,
780    field: &str,
781) -> Result<bool, BackendError> {
782    let raw = required_repository_field(record, field)?;
783    match raw.trim().to_ascii_lowercase().as_str() {
784        "true" | "1" | "yes" | "on" => Ok(true),
785        "false" | "0" | "no" | "off" => Ok(false),
786        _ => Err(BackendError::new(
787            BackendErrorKind::Conflict,
788            "repository.record.invalid_bool",
789            format!(
790                "Repository record `{}` field `{field}` was not a valid boolean.",
791                record.id
792            ),
793        )),
794    }
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct AuthCheckRequest {
799    pub capability: String,
800    pub object: String,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub struct AuthCheckResult {
805    pub allowed: bool,
806    pub explanation: Option<String>,
807}
808
809#[derive(Debug, Clone, PartialEq, Eq)]
810pub struct AuthExplainRequest {
811    pub capability: String,
812    pub object: String,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq)]
816pub struct AuthExplanation {
817    pub summary: String,
818    pub traces: Vec<String>,
819}
820
821#[derive(Debug, Clone, PartialEq, Eq)]
822pub struct AuditEntry {
823    pub action: String,
824    pub resource_kind: String,
825    pub resource_id: String,
826    pub outcome: String,
827    pub detail: Option<String>,
828    pub metadata: MetadataMap,
829}
830
831impl AuditEntry {
832    pub fn new(
833        action: impl Into<String>,
834        resource_kind: impl Into<String>,
835        resource_id: impl Into<String>,
836        outcome: impl Into<String>,
837    ) -> Self {
838        Self {
839            action: action.into(),
840            resource_kind: resource_kind.into(),
841            resource_id: resource_id.into(),
842            outcome: outcome.into(),
843            detail: None,
844            metadata: MetadataMap::new(),
845        }
846    }
847
848    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
849        self.detail = Some(detail.into());
850        self
851    }
852}
853
854#[derive(Debug, Clone, PartialEq, Eq)]
855pub struct OutboundHttpRequest {
856    pub integration: String,
857    pub method: String,
858    pub url: String,
859    pub headers: Headers,
860    pub body: Vec<u8>,
861}
862
863#[derive(Debug, Clone, PartialEq, Eq)]
864pub struct OutboundHttpResponse {
865    pub status: u16,
866    pub headers: Headers,
867    pub body: Vec<u8>,
868}
869
870#[derive(Debug, Clone, PartialEq, Eq)]
871pub struct ManagedAsset {
872    pub logical_path: String,
873    pub storage_class: String,
874    pub public_url: Option<String>,
875}
876
877#[derive(Debug, Clone, PartialEq, Eq)]
878pub struct AssetWriteRequest {
879    pub logical_path: String,
880    pub storage_class: String,
881    pub content_type: Option<String>,
882    pub bytes: Vec<u8>,
883    pub metadata: MetadataMap,
884}
885
886#[derive(Debug, Clone, PartialEq, Eq)]
887pub struct AssetWriteReceipt {
888    pub logical_path: String,
889    pub storage_path: String,
890    pub bytes_written: u64,
891}
892
893#[derive(Debug, Clone, PartialEq, Eq)]
894pub struct CommerceProduct {
895    pub sku: String,
896    pub handle: String,
897    pub title: String,
898    pub current_price: MoneyAmount,
899    pub collection_handle: Option<String>,
900    pub metadata: MetadataMap,
901}