paykit_lib/payment_request/
api.rs1use tracing::instrument;
2
3use crate::{
4 error::map_error, EncryptedLink, PaykitAppId, PrivateApplicationMessage, PrivateMessageKind,
5 Result,
6};
7
8use super::{
9 types::{
10 PaymentProof, PaymentRequest, PaymentRequestAcceptance, PaymentRequestCancellation,
11 PaymentRequestEvent, PaymentRequestEventMessage, PaymentRequestRejection,
12 },
13 wire::{
14 parse_acceptance_json, parse_cancellation_json, parse_conversion_quote_json,
15 parse_event_header, parse_payment_proof_json, parse_payment_request_json,
16 parse_rejection_json, serialize_acceptance_json, serialize_cancellation_json,
17 serialize_conversion_quote_json, serialize_payment_proof_json,
18 serialize_payment_request_json, serialize_rejection_json,
19 },
20 PaymentConversionQuote,
21};
22
23fn parse_event(kind: PrivateMessageKind, raw: &str) -> Option<Result<PaymentRequestEvent>> {
31 match kind {
32 PrivateMessageKind::PaymentRequest => {
33 Some(parse_payment_request_json(raw).map(PaymentRequestEvent::Request))
34 }
35 PrivateMessageKind::PaymentRequestAcceptance => {
36 Some(parse_acceptance_json(raw).map(PaymentRequestEvent::Acceptance))
37 }
38 PrivateMessageKind::PaymentRequestRejection => {
39 Some(parse_rejection_json(raw).map(PaymentRequestEvent::Rejection))
40 }
41 PrivateMessageKind::PaymentRequestCancellation => {
42 Some(parse_cancellation_json(raw).map(PaymentRequestEvent::Cancellation))
43 }
44 PrivateMessageKind::PaymentConversionQuote => {
45 Some(parse_conversion_quote_json(raw).map(PaymentRequestEvent::ConversionQuote))
46 }
47 PrivateMessageKind::PaymentProof => {
48 Some(parse_payment_proof_json(raw).map(PaymentRequestEvent::Proof))
49 }
50 PrivateMessageKind::PrivatePaymentList
53 | PrivateMessageKind::DeliveryConfirmation
54 | PrivateMessageKind::ReceiptAccess
55 | PrivateMessageKind::AllowanceProposal
56 | PrivateMessageKind::AllowanceAcceptance
57 | PrivateMessageKind::AllowanceRejection
58 | PrivateMessageKind::AllowanceEnd => None,
59 }
60}
61
62pub fn parse_payment_request_event_message(
68 message: &PrivateApplicationMessage,
69) -> Option<PaymentRequestEventMessage> {
70 let kind = message.known_kind()?;
71 let event = parse_event(kind, &message.raw_json)?.map_err(|err| err.to_string());
72 let (app_id, event_id, payment_request_id) = parse_event_header(&message.raw_json);
73 Some(PaymentRequestEventMessage {
74 kind,
75 app_id,
76 event_id,
77 payment_request_id,
78 raw_json: message.raw_json.clone(),
79 event,
80 })
81}
82
83pub fn serialize_payment_request_event(
85 app_id: &PaykitAppId,
86 event: &PaymentRequestEvent,
87) -> Result<String> {
88 match event {
89 PaymentRequestEvent::Request(event) => serialize_payment_request_json(app_id, event),
90 PaymentRequestEvent::Acceptance(event) => serialize_acceptance_json(app_id, event),
91 PaymentRequestEvent::Rejection(event) => serialize_rejection_json(app_id, event),
92 PaymentRequestEvent::Cancellation(event) => serialize_cancellation_json(app_id, event),
93 PaymentRequestEvent::Proof(event) => serialize_payment_proof_json(app_id, event),
94 PaymentRequestEvent::ConversionQuote(event) => {
95 serialize_conversion_quote_json(app_id, event)
96 }
97 }
98}
99
100#[instrument(skip(link, event))]
105pub async fn send_payment_request(
106 link: &mut EncryptedLink,
107 app_id: &PaykitAppId,
108 event: &PaymentRequest,
109) -> Result<()> {
110 let json = serialize_payment_request_json(app_id, event)
111 .map_err(|err| map_error("send_payment_request", err))?;
112 link.send_payment_request_message(json.as_bytes())
113 .await
114 .map_err(|err| map_error("send_payment_request", err))
115}
116
117#[instrument(skip(link, event))]
121pub async fn send_payment_request_acceptance(
122 link: &mut EncryptedLink,
123 app_id: &PaykitAppId,
124 event: &PaymentRequestAcceptance,
125) -> Result<()> {
126 let json = serialize_acceptance_json(app_id, event)
127 .map_err(|err| map_error("send_payment_request_acceptance", err))?;
128 link.send_payment_request_acceptance_message(json.as_bytes())
129 .await
130 .map_err(|err| map_error("send_payment_request_acceptance", err))
131}
132
133#[instrument(skip(link, event))]
137pub async fn send_payment_request_rejection(
138 link: &mut EncryptedLink,
139 app_id: &PaykitAppId,
140 event: &PaymentRequestRejection,
141) -> Result<()> {
142 let json = serialize_rejection_json(app_id, event)
143 .map_err(|err| map_error("send_payment_request_rejection", err))?;
144 link.send_payment_request_rejection_message(json.as_bytes())
145 .await
146 .map_err(|err| map_error("send_payment_request_rejection", err))
147}
148
149#[instrument(skip(link, event))]
154pub async fn send_payment_request_cancellation(
155 link: &mut EncryptedLink,
156 app_id: &PaykitAppId,
157 event: &PaymentRequestCancellation,
158) -> Result<()> {
159 let json = serialize_cancellation_json(app_id, event)
160 .map_err(|err| map_error("send_payment_request_cancellation", err))?;
161 link.send_payment_request_cancellation_message(json.as_bytes())
162 .await
163 .map_err(|err| map_error("send_payment_request_cancellation", err))
164}
165
166#[instrument(skip(link, event))]
170pub async fn send_payment_proof(
171 link: &mut EncryptedLink,
172 app_id: &PaykitAppId,
173 event: &PaymentProof,
174) -> Result<()> {
175 let json = serialize_payment_proof_json(app_id, event)
176 .map_err(|err| map_error("send_payment_proof", err))?;
177 link.send_payment_proof_message(json.as_bytes())
178 .await
179 .map_err(|err| map_error("send_payment_proof", err))
180}
181
182#[instrument(skip(link, event))]
185pub async fn send_payment_conversion_quote(
186 link: &mut EncryptedLink,
187 app_id: &PaykitAppId,
188 event: &PaymentConversionQuote,
189) -> Result<()> {
190 let json = serialize_conversion_quote_json(app_id, event)
191 .map_err(|err| map_error("send_payment_conversion_quote", err))?;
192 link.send_payment_conversion_quote_message(json.as_bytes())
193 .await
194 .map_err(|err| map_error("send_payment_conversion_quote", err))
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn app_id() -> PaykitAppId {
202 PaykitAppId::new("test-app").unwrap()
203 }
204
205 #[test]
212 fn test_parse_event_non_request_kind_ignored() {
213 let raw = "{\"secret\":\"SENTINEL_DECRYPTED_PLAINTEXT\"}";
215 for kind in [
216 PrivateMessageKind::PrivatePaymentList,
217 PrivateMessageKind::DeliveryConfirmation,
218 PrivateMessageKind::ReceiptAccess,
219 PrivateMessageKind::AllowanceProposal,
220 PrivateMessageKind::AllowanceAcceptance,
221 PrivateMessageKind::AllowanceRejection,
222 PrivateMessageKind::AllowanceEnd,
223 ] {
224 assert!(
225 parse_event(kind, raw).is_none(),
226 "expected non-request kind {kind} to be ignored",
227 );
228 }
229 }
230
231 use crate::{EventId, PaymentRequestId};
232
233 fn event_id() -> EventId {
234 EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d201").expect("hard-coded UUID v4 is valid")
235 }
236
237 fn request_id() -> PaymentRequestId {
238 PaymentRequestId::new("b7f9c2a1-6d43-4b0e-a8d4-0fe2c712ab33")
239 .expect("hard-coded UUID v4 is valid")
240 }
241
242 fn message_with_request_header(raw_json: &str) -> PrivateApplicationMessage {
246 PrivateApplicationMessage {
247 version: Some(1),
248 kind: Some(PrivateMessageKind::PaymentRequest.as_str().to_string()),
249 app_id: Some(app_id().as_str().to_string()),
250 raw_json: raw_json.to_string(),
251 }
252 }
253
254 fn assert_serialize_round_trip(event: PaymentRequestEvent) {
258 let serialized = serialize_payment_request_event(&app_id(), &event)
259 .expect("construction-valid event must serialize");
260 let message = PrivateApplicationMessage {
261 version: Some(1),
262 kind: Some(event.kind().as_str().to_string()),
263 app_id: Some(app_id().as_str().to_string()),
264 raw_json: serialized,
265 };
266 let parsed = parse_payment_request_event_message(&message)
267 .expect("serialized Payment Request event kind must be routed");
268 assert!(
269 parsed.is_valid(),
270 "round-tripped event must stay valid: {:?}",
271 parsed.validation_error()
272 );
273 assert_eq!(parsed.kind(), event.kind());
274 assert_eq!(parsed.event_id(), Some(event.event_id()));
275 assert_eq!(
276 parsed.payment_request_id(),
277 Some(event.payment_request_id())
278 );
279 assert_eq!(parsed.parsed_event(), Some(&event));
280 }
281
282 #[test]
289 fn test_parse_payment_request_event_message_raw_json_kind_wins_over_header() {
290 let message =
291 message_with_request_header(r#"{"version":1,"kind":"paykit.receipt_access"}"#);
292 assert!(parse_payment_request_event_message(&message).is_none());
293 }
294
295 #[test]
299 fn test_parse_payment_request_event_message_unusable_raw_json_kind_is_none() {
300 for raw_json in [
301 r#"{"version":1}"#,
302 r#"{"version":1,"kind":42}"#,
303 r#"{"version":1,"kind":null}"#,
304 r#"{"version":1,"kind":true}"#,
305 r#"{"version":1,"kind":["paykit.payment_request"]}"#,
306 r#"{"version":1,"kind":{"kind":"paykit.payment_request"}}"#,
307 r#"{"version":1,"kind":"paykit.unknown_kind"}"#,
308 ] {
309 let message = message_with_request_header(raw_json);
310 assert!(
311 parse_payment_request_event_message(&message).is_none(),
312 "expected None for raw JSON {raw_json}"
313 );
314 }
315 }
316
317 #[test]
318 fn test_payment_request_acceptance_serialize_round_trip() {
319 assert_serialize_round_trip(PaymentRequestEvent::Acceptance(
320 PaymentRequestAcceptance::new(event_id(), request_id()),
321 ));
322 }
323
324 #[test]
325 fn test_payment_request_rejection_serialize_round_trip_covers_reason_presence() {
326 assert_serialize_round_trip(PaymentRequestEvent::Rejection(
327 PaymentRequestRejection::new(
328 event_id(),
329 request_id(),
330 Some("amount no longer payable".to_string()),
331 ),
332 ));
333
334 let without_reason = PaymentRequestEvent::Rejection(PaymentRequestRejection::new(
335 event_id(),
336 request_id(),
337 None,
338 ));
339 let serialized = serialize_payment_request_event(&app_id(), &without_reason)
340 .expect("construction-valid event must serialize");
341 assert!(
342 !serialized.contains("\"reason\""),
343 "a None reason must be omitted from the wire JSON"
344 );
345 assert_serialize_round_trip(without_reason);
346 }
347
348 #[test]
349 fn test_payment_request_cancellation_serialize_round_trip_covers_reason_presence() {
350 assert_serialize_round_trip(PaymentRequestEvent::Cancellation(
351 PaymentRequestCancellation::new(
352 event_id(),
353 request_id(),
354 Some("request superseded".to_string()),
355 ),
356 ));
357
358 let without_reason = PaymentRequestEvent::Cancellation(PaymentRequestCancellation::new(
359 event_id(),
360 request_id(),
361 None,
362 ));
363 let serialized = serialize_payment_request_event(&app_id(), &without_reason)
364 .expect("construction-valid event must serialize");
365 assert!(
366 !serialized.contains("\"reason\""),
367 "a None reason must be omitted from the wire JSON"
368 );
369 assert_serialize_round_trip(without_reason);
370 }
371}