apple_app_store_receipts/objects/
response_body.rs1use 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
203fn 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}