use tracing::instrument;
use crate::{
error::map_error, EncryptedLink, PaykitAppId, PrivateApplicationMessage, PrivateMessageKind,
Result,
};
use super::{
types::{
PaymentProof, PaymentRequest, PaymentRequestAcceptance, PaymentRequestCancellation,
PaymentRequestEvent, PaymentRequestEventMessage, PaymentRequestRejection,
},
wire::{
parse_acceptance_json, parse_cancellation_json, parse_conversion_quote_json,
parse_event_header, parse_payment_proof_json, parse_payment_request_json,
parse_rejection_json, serialize_acceptance_json, serialize_cancellation_json,
serialize_conversion_quote_json, serialize_payment_proof_json,
serialize_payment_request_json, serialize_rejection_json,
},
PaymentConversionQuote,
};
fn parse_event(kind: PrivateMessageKind, raw: &str) -> Option<Result<PaymentRequestEvent>> {
match kind {
PrivateMessageKind::PaymentRequest => {
Some(parse_payment_request_json(raw).map(PaymentRequestEvent::Request))
}
PrivateMessageKind::PaymentRequestAcceptance => {
Some(parse_acceptance_json(raw).map(PaymentRequestEvent::Acceptance))
}
PrivateMessageKind::PaymentRequestRejection => {
Some(parse_rejection_json(raw).map(PaymentRequestEvent::Rejection))
}
PrivateMessageKind::PaymentRequestCancellation => {
Some(parse_cancellation_json(raw).map(PaymentRequestEvent::Cancellation))
}
PrivateMessageKind::PaymentConversionQuote => {
Some(parse_conversion_quote_json(raw).map(PaymentRequestEvent::ConversionQuote))
}
PrivateMessageKind::PaymentProof => {
Some(parse_payment_proof_json(raw).map(PaymentRequestEvent::Proof))
}
PrivateMessageKind::PrivatePaymentList
| PrivateMessageKind::DeliveryConfirmation
| PrivateMessageKind::ReceiptAccess
| PrivateMessageKind::AllowanceProposal
| PrivateMessageKind::AllowanceAcceptance
| PrivateMessageKind::AllowanceRejection
| PrivateMessageKind::AllowanceEnd => None,
}
}
pub fn parse_payment_request_event_message(
message: &PrivateApplicationMessage,
) -> Option<PaymentRequestEventMessage> {
let kind = message.known_kind()?;
let event = parse_event(kind, &message.raw_json)?.map_err(|err| err.to_string());
let (app_id, event_id, payment_request_id) = parse_event_header(&message.raw_json);
Some(PaymentRequestEventMessage {
kind,
app_id,
event_id,
payment_request_id,
raw_json: message.raw_json.clone(),
event,
})
}
pub fn serialize_payment_request_event(
app_id: &PaykitAppId,
event: &PaymentRequestEvent,
) -> Result<String> {
match event {
PaymentRequestEvent::Request(event) => serialize_payment_request_json(app_id, event),
PaymentRequestEvent::Acceptance(event) => serialize_acceptance_json(app_id, event),
PaymentRequestEvent::Rejection(event) => serialize_rejection_json(app_id, event),
PaymentRequestEvent::Cancellation(event) => serialize_cancellation_json(app_id, event),
PaymentRequestEvent::Proof(event) => serialize_payment_proof_json(app_id, event),
PaymentRequestEvent::ConversionQuote(event) => {
serialize_conversion_quote_json(app_id, event)
}
}
}
#[instrument(skip(link, event))]
pub async fn send_payment_request(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentRequest,
) -> Result<()> {
let json = serialize_payment_request_json(app_id, event)
.map_err(|err| map_error("send_payment_request", err))?;
link.send_payment_request_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_request", err))
}
#[instrument(skip(link, event))]
pub async fn send_payment_request_acceptance(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentRequestAcceptance,
) -> Result<()> {
let json = serialize_acceptance_json(app_id, event)
.map_err(|err| map_error("send_payment_request_acceptance", err))?;
link.send_payment_request_acceptance_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_request_acceptance", err))
}
#[instrument(skip(link, event))]
pub async fn send_payment_request_rejection(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentRequestRejection,
) -> Result<()> {
let json = serialize_rejection_json(app_id, event)
.map_err(|err| map_error("send_payment_request_rejection", err))?;
link.send_payment_request_rejection_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_request_rejection", err))
}
#[instrument(skip(link, event))]
pub async fn send_payment_request_cancellation(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentRequestCancellation,
) -> Result<()> {
let json = serialize_cancellation_json(app_id, event)
.map_err(|err| map_error("send_payment_request_cancellation", err))?;
link.send_payment_request_cancellation_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_request_cancellation", err))
}
#[instrument(skip(link, event))]
pub async fn send_payment_proof(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentProof,
) -> Result<()> {
let json = serialize_payment_proof_json(app_id, event)
.map_err(|err| map_error("send_payment_proof", err))?;
link.send_payment_proof_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_proof", err))
}
#[instrument(skip(link, event))]
pub async fn send_payment_conversion_quote(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &PaymentConversionQuote,
) -> Result<()> {
let json = serialize_conversion_quote_json(app_id, event)
.map_err(|err| map_error("send_payment_conversion_quote", err))?;
link.send_payment_conversion_quote_message(json.as_bytes())
.await
.map_err(|err| map_error("send_payment_conversion_quote", err))
}
#[cfg(test)]
mod tests {
use super::*;
fn app_id() -> PaykitAppId {
PaykitAppId::new("test-app").unwrap()
}
#[test]
fn test_parse_event_non_request_kind_ignored() {
let raw = "{\"secret\":\"SENTINEL_DECRYPTED_PLAINTEXT\"}";
for kind in [
PrivateMessageKind::PrivatePaymentList,
PrivateMessageKind::DeliveryConfirmation,
PrivateMessageKind::ReceiptAccess,
PrivateMessageKind::AllowanceProposal,
PrivateMessageKind::AllowanceAcceptance,
PrivateMessageKind::AllowanceRejection,
PrivateMessageKind::AllowanceEnd,
] {
assert!(
parse_event(kind, raw).is_none(),
"expected non-request kind {kind} to be ignored",
);
}
}
use crate::{EventId, PaymentRequestId};
fn event_id() -> EventId {
EventId::new("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d201").expect("hard-coded UUID v4 is valid")
}
fn request_id() -> PaymentRequestId {
PaymentRequestId::new("b7f9c2a1-6d43-4b0e-a8d4-0fe2c712ab33")
.expect("hard-coded UUID v4 is valid")
}
fn message_with_request_header(raw_json: &str) -> PrivateApplicationMessage {
PrivateApplicationMessage {
version: Some(1),
kind: Some(PrivateMessageKind::PaymentRequest.as_str().to_string()),
app_id: Some(app_id().as_str().to_string()),
raw_json: raw_json.to_string(),
}
}
fn assert_serialize_round_trip(event: PaymentRequestEvent) {
let serialized = serialize_payment_request_event(&app_id(), &event)
.expect("construction-valid event must serialize");
let message = PrivateApplicationMessage {
version: Some(1),
kind: Some(event.kind().as_str().to_string()),
app_id: Some(app_id().as_str().to_string()),
raw_json: serialized,
};
let parsed = parse_payment_request_event_message(&message)
.expect("serialized Payment Request event kind must be routed");
assert!(
parsed.is_valid(),
"round-tripped event must stay valid: {:?}",
parsed.validation_error()
);
assert_eq!(parsed.kind(), event.kind());
assert_eq!(parsed.event_id(), Some(event.event_id()));
assert_eq!(
parsed.payment_request_id(),
Some(event.payment_request_id())
);
assert_eq!(parsed.parsed_event(), Some(&event));
}
#[test]
fn test_parse_payment_request_event_message_raw_json_kind_wins_over_header() {
let message =
message_with_request_header(r#"{"version":1,"kind":"paykit.receipt_access"}"#);
assert!(parse_payment_request_event_message(&message).is_none());
}
#[test]
fn test_parse_payment_request_event_message_unusable_raw_json_kind_is_none() {
for raw_json in [
r#"{"version":1}"#,
r#"{"version":1,"kind":42}"#,
r#"{"version":1,"kind":null}"#,
r#"{"version":1,"kind":true}"#,
r#"{"version":1,"kind":["paykit.payment_request"]}"#,
r#"{"version":1,"kind":{"kind":"paykit.payment_request"}}"#,
r#"{"version":1,"kind":"paykit.unknown_kind"}"#,
] {
let message = message_with_request_header(raw_json);
assert!(
parse_payment_request_event_message(&message).is_none(),
"expected None for raw JSON {raw_json}"
);
}
}
#[test]
fn test_payment_request_acceptance_serialize_round_trip() {
assert_serialize_round_trip(PaymentRequestEvent::Acceptance(
PaymentRequestAcceptance::new(event_id(), request_id()),
));
}
#[test]
fn test_payment_request_rejection_serialize_round_trip_covers_reason_presence() {
assert_serialize_round_trip(PaymentRequestEvent::Rejection(
PaymentRequestRejection::new(
event_id(),
request_id(),
Some("amount no longer payable".to_string()),
),
));
let without_reason = PaymentRequestEvent::Rejection(PaymentRequestRejection::new(
event_id(),
request_id(),
None,
));
let serialized = serialize_payment_request_event(&app_id(), &without_reason)
.expect("construction-valid event must serialize");
assert!(
!serialized.contains("\"reason\""),
"a None reason must be omitted from the wire JSON"
);
assert_serialize_round_trip(without_reason);
}
#[test]
fn test_payment_request_cancellation_serialize_round_trip_covers_reason_presence() {
assert_serialize_round_trip(PaymentRequestEvent::Cancellation(
PaymentRequestCancellation::new(
event_id(),
request_id(),
Some("request superseded".to_string()),
),
));
let without_reason = PaymentRequestEvent::Cancellation(PaymentRequestCancellation::new(
event_id(),
request_id(),
None,
));
let serialized = serialize_payment_request_event(&app_id(), &without_reason)
.expect("construction-valid event must serialize");
assert!(
!serialized.contains("\"reason\""),
"a None reason must be omitted from the wire JSON"
);
assert_serialize_round_trip(without_reason);
}
}