Skip to main content

smbcloud_ascapi_reports/
lib.rs

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
7use async_trait::async_trait;
8use reqwest::Method;
9use smbcloud_ascapi_core::{Client, Result};
10
11/// Arguments accepted by `GET /v1/salesReports`.
12#[derive(Debug, Clone)]
13pub 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
22impl 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]
42pub trait SalesReportsApi {
43    async fn download_sales_report(&self, request: &SalesReportRequest) -> Result<Vec<u8>>;
44}
45
46#[async_trait]
47impl 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.
61pub mod prelude {
62    pub use crate::SalesReportsApi;
63}
64
65#[cfg(test)]
66mod 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}