1use serde::{Deserialize, Serialize};
2use serde_json::{Map as JsonMap, Value as JsonValue};
3
4use crate::{
5 shared_wire::{deserialize_optional_no_null, BillingPeriodWire, PaymentAmountWire},
6 validation::{
7 invalid_data, invalid_plaintext_json, validate_wire_version_kind,
8 validate_wire_version_kind_str,
9 },
10 EventId, PaykitAppId, PaykitError, PaymentEndpointIdentifier, PaymentReference,
11 PaymentRequestId, PrivateApplicationMessage, PrivateMessageKind, PublicKey, Result,
12};
13
14use super::{Receipt, ReceiptAccess, ReceiptAccessEventMessage, ReceiptDecryptionKey, ReceiptId};
15
16#[derive(Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub(super) struct ReceiptWire {
19 pub(super) version: u8,
20 pub(super) kind: String,
21 pub(super) receipt_id: String,
22 pub(super) payment_reference: String,
23 #[serde(default)]
24 #[serde(deserialize_with = "deserialize_optional_no_null")]
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub(super) payment_request_id: Option<String>,
27 #[serde(default)]
28 #[serde(deserialize_with = "deserialize_optional_no_null")]
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub(super) billing_period: Option<BillingPeriodWire>,
31 pub(super) recipient_public_key: String,
32 pub(super) payment_endpoint_identifier: Option<String>,
33 pub(super) amount: Option<PaymentAmountWire>,
34 pub(super) metadata: JsonMap<String, JsonValue>,
35}
36
37#[derive(Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub(super) struct EncryptedReceiptWire {
40 pub(super) version: u8,
41 pub(super) kind: String,
42 pub(super) algorithm: String,
43 pub(super) nonce: String,
44 pub(super) ciphertext: String,
45}
46
47#[derive(Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub(super) struct ReceiptAccessWire {
50 pub(super) version: u8,
51 pub(super) kind: String,
52 pub(super) app_id: String,
53 pub(super) event_id: String,
54 pub(super) receipt_id: String,
55 pub(super) payment_reference: String,
56 #[serde(default)]
57 #[serde(deserialize_with = "deserialize_optional_no_null")]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub(super) payment_request_id: Option<String>,
60 #[serde(default)]
61 #[serde(deserialize_with = "deserialize_optional_no_null")]
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub(super) billing_period: Option<BillingPeriodWire>,
64 pub(super) location: String,
65 pub(super) key: String,
66}
67
68impl From<&Receipt> for ReceiptWire {
69 fn from(receipt: &Receipt) -> Self {
70 Self {
71 version: 1,
72 kind: "paykit.receipt".to_string(),
73 receipt_id: receipt.receipt_id.as_str().to_string(),
74 payment_reference: receipt.payment_reference.as_str().to_string(),
75 payment_request_id: receipt
76 .payment_request_id
77 .as_ref()
78 .map(|id| id.as_str().to_string()),
79 billing_period: receipt.billing_period.as_ref().map(BillingPeriodWire::from),
80 recipient_public_key: receipt.recipient_public_key.to_string(),
81 payment_endpoint_identifier: receipt
82 .payment_endpoint_identifier
83 .as_ref()
84 .map(|identifier| identifier.as_str().to_string()),
85 amount: receipt.amount.as_ref().map(PaymentAmountWire::from),
86 metadata: receipt.metadata.clone(),
87 }
88 }
89}
90
91impl TryFrom<ReceiptWire> for Receipt {
92 type Error = PaykitError;
93
94 fn try_from(wire: ReceiptWire) -> Result<Self> {
95 validate_wire_version_kind_str(wire.version, &wire.kind, "paykit.receipt", "Receipt")?;
96 let receipt_id =
97 ReceiptId::new(wire.receipt_id).map_err(|err| PaykitError::InvalidData {
98 context: "Receipt contains invalid Receipt ID".into(),
99 source: Some(err.into()),
100 })?;
101 let payment_reference = PaymentReference::new(wire.payment_reference).map_err(|err| {
102 PaykitError::InvalidData {
103 context: "Receipt contains invalid Payment Reference".into(),
104 source: Some(err.into()),
105 }
106 })?;
107 let payment_request_id = wire
108 .payment_request_id
109 .map(PaymentRequestId::new)
110 .transpose()
111 .map_err(|err| PaykitError::InvalidData {
112 context: "Receipt contains invalid Payment Request ID".into(),
113 source: Some(err.into()),
114 })?;
115 let billing_period = wire
116 .billing_period
117 .map(|wire| wire.try_into_with_label("Receipt Billing Period"))
118 .transpose()
119 .map_err(|err| PaykitError::InvalidData {
120 context: "Receipt contains invalid Billing Period".into(),
121 source: Some(err.into()),
122 })?;
123
124 let recipient_public_key = PublicKey::try_from(wire.recipient_public_key.as_str())
128 .map_err(|err| PaykitError::InvalidData {
129 context: "Receipt contains invalid recipient public key".into(),
130 source: anyhow::anyhow!("invalid recipient public key: {err:?}").into(),
131 })?;
132 let payment_endpoint_identifier = wire
133 .payment_endpoint_identifier
134 .map(PaymentEndpointIdentifier::new)
135 .transpose()
136 .map_err(|err| PaykitError::InvalidData {
137 context: "Receipt contains invalid Payment Endpoint Identifier".into(),
138 source: Some(err.into()),
139 })?;
140 let amount = wire
141 .amount
142 .map(|wire| wire.try_into_with_label("Receipt amount"))
143 .transpose()
144 .map_err(|err| PaykitError::InvalidData {
145 context: "Receipt contains invalid Payment Amount".into(),
146 source: Some(err.into()),
147 })?;
148
149 let receipt = Self {
150 receipt_id,
151 payment_reference,
152 payment_request_id,
153 billing_period,
154 recipient_public_key,
155 payment_endpoint_identifier,
156 amount,
157 metadata: wire.metadata,
158 };
159 receipt
160 .validate_request_context()
161 .map_err(|err| PaykitError::InvalidData {
162 context: "Receipt contains invalid Payment Request context".into(),
163 source: Some(err.into()),
164 })?;
165 Ok(receipt)
166 }
167}
168
169impl ReceiptAccessWire {
170 fn from_access(app_id: &PaykitAppId, access: &ReceiptAccess) -> Self {
171 Self {
172 version: access.version,
173 kind: access.kind.as_str().to_string(),
174 app_id: app_id.as_str().to_string(),
175 event_id: access.event_id.as_str().to_string(),
176 receipt_id: access.receipt_id.as_str().to_string(),
177 payment_reference: access.payment_reference.as_str().to_string(),
178 payment_request_id: access
179 .payment_request_id
180 .as_ref()
181 .map(|id| id.as_str().to_string()),
182 billing_period: access.billing_period.as_ref().map(BillingPeriodWire::from),
183 location: access.location.clone(),
184 key: access.key.as_str().to_string(),
185 }
186 }
187}
188
189impl TryFrom<ReceiptAccessWire> for ReceiptAccess {
190 type Error = PaykitError;
191
192 fn try_from(wire: ReceiptAccessWire) -> Result<Self> {
193 validate_wire_version_kind(
194 wire.version,
195 &wire.kind,
196 PrivateMessageKind::ReceiptAccess,
197 "Receipt Access",
198 )?;
199 PaykitAppId::new(&wire.app_id).map_err(|err| PaykitError::InvalidData {
200 context: "Receipt Access contains invalid App ID".into(),
201 source: Some(err.into()),
202 })?;
203 let event_id = EventId::new(wire.event_id).map_err(|err| PaykitError::InvalidData {
204 context: "Receipt Access contains invalid Event ID".into(),
205 source: Some(err.into()),
206 })?;
207 let receipt_id =
208 ReceiptId::new(wire.receipt_id).map_err(|err| PaykitError::InvalidData {
209 context: "Receipt Access contains invalid Receipt ID".into(),
210 source: Some(err.into()),
211 })?;
212 let payment_reference = PaymentReference::new(wire.payment_reference).map_err(|err| {
213 PaykitError::InvalidData {
214 context: "Receipt Access contains invalid Payment Reference".into(),
215 source: Some(err.into()),
216 }
217 })?;
218 let payment_request_id = wire
219 .payment_request_id
220 .map(PaymentRequestId::new)
221 .transpose()
222 .map_err(|err| PaykitError::InvalidData {
223 context: "Receipt Access contains invalid Payment Request ID".into(),
224 source: Some(err.into()),
225 })?;
226 let billing_period = wire
227 .billing_period
228 .map(|wire| wire.try_into_with_label("Receipt Access Billing Period"))
229 .transpose()
230 .map_err(|err| PaykitError::InvalidData {
231 context: "Receipt Access contains invalid Billing Period".into(),
232 source: Some(err.into()),
233 })?;
234
235 let access = Self {
236 version: 1,
237 kind: PrivateMessageKind::ReceiptAccess,
238 event_id,
239 receipt_id,
240 payment_reference,
241 payment_request_id,
242 billing_period,
243 location: wire.location,
244 key: ReceiptDecryptionKey::new(wire.key).map_err(|err| PaykitError::InvalidData {
245 context: "Receipt Access contains invalid Receipt Decryption Key".into(),
246 source: Some(err.into()),
247 })?,
248 };
249 access
250 .validate_request_context()
251 .map_err(|err| PaykitError::InvalidData {
252 context: "Receipt Access contains invalid Payment Request context".into(),
253 source: Some(err.into()),
254 })?;
255 access.validate_wire_location()?;
256 Ok(access)
257 }
258}
259
260pub fn serialize_receipt_access_json(
262 app_id: &PaykitAppId,
263 access: &ReceiptAccess,
264) -> Result<String> {
265 serde_json::to_string(&ReceiptAccessWire::from_access(app_id, access)).map_err(|err| {
266 invalid_data(
267 format!("failed to serialize Receipt Access JSON: {err}"),
268 Some(err.into()),
269 )
270 })
271}
272
273pub fn parse_receipt_access_json(json: &str) -> Result<ReceiptAccess> {
278 let wire: ReceiptAccessWire = serde_json::from_str(json)
284 .map_err(|_| invalid_plaintext_json("failed to parse Receipt Access JSON"))?;
285 ReceiptAccess::try_from(wire)
286}
287
288fn parse_receipt_access_header(
289 raw: &str,
290) -> (Option<PaykitAppId>, Option<EventId>, Option<ReceiptId>) {
291 let Ok(value) = serde_json::from_str::<JsonValue>(raw) else {
292 return (None, None, None);
293 };
294 let app_id = value
295 .get("app_id")
296 .and_then(JsonValue::as_str)
297 .and_then(|id| PaykitAppId::new(id).ok());
298 let event_id = value
299 .get("event_id")
300 .and_then(JsonValue::as_str)
301 .and_then(|id| EventId::new(id).ok());
302 let receipt_id = value
303 .get("receipt_id")
304 .and_then(JsonValue::as_str)
305 .and_then(|id| ReceiptId::new(id).ok());
306 (app_id, event_id, receipt_id)
307}
308
309pub fn parse_receipt_access_event_message(
315 message: &PrivateApplicationMessage,
316) -> Option<ReceiptAccessEventMessage> {
317 let kind = message.known_kind()?;
318 (kind == PrivateMessageKind::ReceiptAccess).then(|| {
319 let (app_id, event_id, receipt_id) = parse_receipt_access_header(&message.raw_json);
320 let access = parse_receipt_access_json(&message.raw_json).map_err(|err| err.to_string());
321 ReceiptAccessEventMessage {
322 kind,
323 app_id,
324 event_id,
325 receipt_id,
326 raw_json: message.raw_json.clone(),
327 access,
328 }
329 })
330}