apple_app_store_receipts/objects/
response_body.rs

1// ref https://developer.apple.com/documentation/appstorereceipts/requestbody
2
3use chrono::{DateTime, Utc};
4use serde::{de, Deserialize, Deserializer};
5use serde_aux::field_attributes::{
6    deserialize_bool_from_anything, deserialize_datetime_utc_from_milliseconds,
7    deserialize_number_from_string,
8};
9use serde_enum_str::Deserialize_enum_str;
10use serde_json::{Map, Value};
11
12use crate::types::status::Status;
13
14#[derive(Debug)]
15pub enum ResponseBody {
16    Success(Box<ResponseBodyWithSuccess>),
17    Error(ResponseBodyWithError),
18}
19impl<'de> Deserialize<'de> for ResponseBody {
20    fn deserialize<D>(deserializer: D) -> Result<ResponseBody, D::Error>
21    where
22        D: Deserializer<'de>,
23    {
24        let map = Map::deserialize(deserializer)?;
25
26        let status: Status = map
27            .get("status")
28            .ok_or_else(|| de::Error::missing_field("status"))
29            .map(Deserialize::deserialize)?
30            .map_err(de::Error::custom)?;
31        let rest = Value::Object(map);
32
33        match status {
34            Status::Success => ResponseBodyWithSuccess::deserialize(rest)
35                .map(|x| ResponseBody::Success(x.into()))
36                .map_err(de::Error::custom),
37            _ => ResponseBodyWithError::deserialize(rest)
38                .map(ResponseBody::Error)
39                .map_err(de::Error::custom),
40        }
41    }
42}
43
44#[derive(Deserialize, Debug)]
45pub struct ResponseBodyWithSuccess {
46    pub status: Status,
47    pub environment: Environment,
48    pub receipt: Receipt,
49    pub latest_receipt: Option<String>,
50    pub latest_receipt_info: Option<Vec<LatestReceiptInfo>>,
51}
52
53#[derive(Deserialize, Debug)]
54pub struct ResponseBodyWithError {
55    pub status: Status,
56    pub environment: Option<Environment>,
57    pub is_retryable: Option<bool>,
58    pub exception: Option<String>,
59}
60
61#[derive(Deserialize, Debug, PartialEq, Eq)]
62pub enum Environment {
63    Sandbox,
64    Production,
65}
66
67#[derive(Deserialize_enum_str, Debug, PartialEq, Eq)]
68pub enum ReceiptType {
69    Production,
70    #[allow(clippy::upper_case_acronyms)]
71    ProductionVPP,
72    ProductionSandbox,
73    #[allow(clippy::upper_case_acronyms)]
74    ProductionVPPSandbox,
75    #[serde(other)]
76    Other(String),
77}
78
79#[derive(Deserialize, Debug)]
80pub struct Receipt {
81    pub receipt_type: ReceiptType,
82
83    pub adam_id: usize,
84    pub app_item_id: usize,
85    pub bundle_id: String,
86    pub application_version: String,
87    pub download_id: Option<usize>,
88
89    pub version_external_identifier: usize,
90
91    #[serde(
92        rename(deserialize = "receipt_creation_date_ms"),
93        deserialize_with = "deserialize_datetime_utc_from_milliseconds"
94    )]
95    pub receipt_creation_date: DateTime<Utc>,
96    pub receipt_creation_date_pst: String,
97
98    #[serde(
99        rename(deserialize = "request_date_ms"),
100        deserialize_with = "deserialize_datetime_utc_from_milliseconds"
101    )]
102    pub request_date: DateTime<Utc>,
103    pub request_date_pst: String,
104
105    #[serde(
106        rename(deserialize = "original_purchase_date_ms"),
107        deserialize_with = "deserialize_datetime_utc_from_milliseconds"
108    )]
109    pub original_purchase_date: DateTime<Utc>,
110    pub original_purchase_date_pst: String,
111
112    pub original_application_version: Option<String>,
113
114    pub in_app: Option<Vec<ReceiptInApp>>,
115
116    #[serde(
117        rename(deserialize = "expiration_date_ms"),
118        default,
119        deserialize_with = "deserialize_datetime_utc_from_milliseconds_option"
120    )]
121    pub expiration_date: Option<DateTime<Utc>>,
122    pub expiration_date_pst: Option<String>,
123
124    #[serde(
125        rename(deserialize = "preorder_date_ms"),
126        default,
127        deserialize_with = "deserialize_datetime_utc_from_milliseconds_option"
128    )]
129    pub preorder_date: Option<DateTime<Utc>>,
130    pub preorder_date_pst: Option<String>,
131}
132
133#[derive(Deserialize, Debug)]
134pub struct Transaction {
135    #[serde(deserialize_with = "deserialize_number_from_string")]
136    pub quantity: usize,
137
138    pub product_id: String,
139
140    pub transaction_id: String,
141    pub original_transaction_id: String,
142
143    #[serde(
144        rename(deserialize = "purchase_date_ms"),
145        deserialize_with = "deserialize_datetime_utc_from_milliseconds"
146    )]
147    pub purchase_date: DateTime<Utc>,
148    pub purchase_date_pst: String,
149
150    #[serde(
151        rename(deserialize = "original_purchase_date_ms"),
152        deserialize_with = "deserialize_datetime_utc_from_milliseconds"
153    )]
154    pub original_purchase_date: DateTime<Utc>,
155    pub original_purchase_date_pst: String,
156
157    #[serde(
158        rename(deserialize = "expires_date_ms"),
159        default,
160        deserialize_with = "deserialize_datetime_utc_from_milliseconds_option"
161    )]
162    pub expires_date: Option<DateTime<Utc>>,
163    pub expires_date_pst: Option<String>,
164
165    #[serde(
166        rename(deserialize = "cancellation_date_ms"),
167        default,
168        deserialize_with = "deserialize_datetime_utc_from_milliseconds_option"
169    )]
170    pub cancellation_date: Option<DateTime<Utc>>,
171    pub cancellation_date_pst: Option<String>,
172
173    pub cancellation_reason: Option<String>,
174
175    pub web_order_line_item_id: Option<String>,
176
177    #[serde(default, deserialize_with = "deserialize_bool_from_anything_option")]
178    pub is_trial_period: Option<bool>,
179
180    pub promotional_offer_id: Option<String>,
181
182    #[serde(default, deserialize_with = "deserialize_bool_from_anything_option")]
183    pub is_in_intro_offer_period: Option<bool>,
184}
185
186#[derive(Deserialize, Debug)]
187pub struct LatestReceiptInfo {
188    #[serde(flatten)]
189    pub transaction: Transaction,
190
191    pub subscription_group_identifier: Option<String>,
192
193    #[serde(default, deserialize_with = "deserialize_bool_from_anything_option")]
194    pub is_upgraded: Option<bool>,
195}
196
197#[derive(Deserialize, Debug)]
198pub struct ReceiptInApp {
199    #[serde(flatten)]
200    pub transaction: Transaction,
201}
202
203//
204//
205//
206fn deserialize_bool_from_anything_option<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
207where
208    D: Deserializer<'de>,
209{
210    deserialize_bool_from_anything(deserializer).map(Some)
211}
212
213fn deserialize_datetime_utc_from_milliseconds_option<'de, D>(
214    deserializer: D,
215) -> Result<Option<DateTime<Utc>>, D::Error>
216where
217    D: Deserializer<'de>,
218{
219    deserialize_datetime_utc_from_milliseconds(deserializer).map(Some)
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    use std::error;
227
228    #[test]
229    fn simple_error() -> Result<(), Box<dyn error::Error>> {
230        match serde_json::from_str(r#"{"status":21007}"#)? {
231            ResponseBody::Error(body) => {
232                assert_eq!(body.status, Status::Error21007);
233                assert_eq!(body.environment, None);
234                assert_eq!(body.is_retryable, None);
235                assert_eq!(body.exception, None)
236            }
237            _ => panic!(),
238        }
239
240        match serde_json::from_str(
241            r#"{"status":21010, "environment":"Production", "is_retryable":false, "exception":"com.apple.jingle.commercelogic.inapplocker.exception.MZInAppLockerAccessException"}"#,
242        )? {
243            ResponseBody::Error(body) => {
244                assert_eq!(body.status, Status::Error21010);
245                assert_eq!(body.environment, Some(Environment::Production));
246                assert_eq!(body.is_retryable, Some(false));
247                assert_eq!(body.exception, Some("com.apple.jingle.commercelogic.inapplocker.exception.MZInAppLockerAccessException".to_owned()));
248            }
249            _ => panic!(),
250        }
251
252        match serde_json::from_str(
253            r#"{"status":21104, "environment":"Production", "is_retryable":true, "exception":"com.apple.jingle.commercelogic.inapplocker.exception.MZInAppLockerAccessException"}"#,
254        )? {
255            ResponseBody::Error(body) => {
256                assert_eq!(body.status, Status::InternalDataAccessError(21104));
257                assert_eq!(body.environment, Some(Environment::Production));
258                assert_eq!(body.is_retryable, Some(true));
259                assert_eq!(body.exception, Some("com.apple.jingle.commercelogic.inapplocker.exception.MZInAppLockerAccessException".to_owned()));
260            }
261            _ => panic!(),
262        }
263
264        Ok(())
265    }
266}