| 1 | //! Sales report downloads from App Store Connect. |
| 2 | //! |
| 3 | //! Apple returns these reports as gzip-compressed TSV rather than JSON:API. |
| 4 | //! This crate owns the request shape; consumers decide how to persist or |
| 5 | //! transform the downloaded file. |
| 6 | |
| 7 | use async_trait::async_trait; |
| 8 | use reqwest::Method; |
| 9 | use smbcloud_ascapi_core::{Client, Result}; |
| 10 | |
| 11 | /// Arguments accepted by `GET /v1/salesReports`. |
| 12 | #[derive(Debug, Clone)] |
| 13 | pub struct SalesReportRequest { |
| 14 | pub vendor_number: String, |
| 15 | pub frequency: String, |
| 16 | pub report_date: String, |
| 17 | pub report_type: String, |
| 18 | pub report_subtype: String, |
| 19 | pub version: Option<String>, |
| 20 | } |
| 21 | |
| 22 | impl SalesReportRequest { |
| 23 | /// Build the documented App Store Connect query filters. |
| 24 | pub fn query(&self) -> Vec<(&str, &str)> { |
| 25 | let mut query = vec![ |
| 26 | ("filter[frequency]", self.frequency.as_str()), |
| 27 | ("filter[reportDate]", self.report_date.as_str()), |
| 28 | ("filter[reportSubType]", self.report_subtype.as_str()), |
| 29 | ("filter[reportType]", self.report_type.as_str()), |
| 30 | ("filter[vendorNumber]", self.vendor_number.as_str()), |
| 31 | ]; |
| 32 | if let Some(version) = self.version.as_deref() { |
| 33 | query.push(("filter[version]", version)); |
| 34 | } |
| 35 | query |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /// Download account-level sales data. The successful body is Apple’s |
| 40 | /// gzip-compressed TSV report. |
| 41 | #[async_trait] |
| 42 | pub trait SalesReportsApi { |
| 43 | async fn download_sales_report(&self, request: &SalesReportRequest) -> Result<Vec<u8>>; |
| 44 | } |
| 45 | |
| 46 | #[async_trait] |
| 47 | impl SalesReportsApi for Client { |
| 48 | async fn download_sales_report(&self, request: &SalesReportRequest) -> Result<Vec<u8>> { |
| 49 | self.request_bytes( |
| 50 | Method::GET, |
| 51 | "/v1/salesReports", |
| 52 | &request.query(), |
| 53 | None::<&()>, |
| 54 | &[("Accept", "application/a-gzip")], |
| 55 | ) |
| 56 | .await |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | /// Traits to import when using this crate. |
| 61 | pub mod prelude { |
| 62 | pub use crate::SalesReportsApi; |
| 63 | } |
| 64 | |
| 65 | #[cfg(test)] |
| 66 | mod tests { |
| 67 | use super::SalesReportRequest; |
| 68 | |
| 69 | #[test] |
| 70 | fn query_contains_defaults_and_optional_version() { |
| 71 | let request = SalesReportRequest { |
| 72 | vendor_number: "12345678".into(), |
| 73 | frequency: "DAILY".into(), |
| 74 | report_date: "2026-09-11".into(), |
| 75 | report_type: "SALES".into(), |
| 76 | report_subtype: "SUMMARY".into(), |
| 77 | version: Some("1_0".into()), |
| 78 | }; |
| 79 | |
| 80 | assert_eq!( |
| 81 | request.query(), |
| 82 | vec![ |
| 83 | ("filter[frequency]", "DAILY"), |
| 84 | ("filter[reportDate]", "2026-09-11"), |
| 85 | ("filter[reportSubType]", "SUMMARY"), |
| 86 | ("filter[reportType]", "SALES"), |
| 87 | ("filter[vendorNumber]", "12345678"), |
| 88 | ("filter[version]", "1_0"), |
| 89 | ] |
| 90 | ); |
| 91 | } |
| 92 | } |