diff --git a/crates/payment_adapter/Readme.adoc b/crates/payment_adapter/Readme.adoc index db7897c..daff1a7 100644 --- a/crates/payment_adapter/Readme.adoc +++ b/crates/payment_adapter/Readme.adoc @@ -27,5 +27,6 @@ pub fn wapc_init() { wapc::register_function("teardown", teardown); wapc::register_function("create_payment", create_payment); wapc::register_function("cancel_order", cancel_order); + wapc::register_function("update_status", update_status); } ``` diff --git a/crates/payment_adapter/src/lib.rs b/crates/payment_adapter/src/lib.rs index 4a59b7f..d80a003 100644 --- a/crates/payment_adapter/src/lib.rs +++ b/crates/payment_adapter/src/lib.rs @@ -9,6 +9,8 @@ pub const CONFIG_POS: u32 = 3; pub enum Error { #[error("Malformed create payment message")] MalformedCreatePayment, + #[error("Malformed payment status update message")] + MalformedStatusUpdate, #[error("Malformed authorize response")] MalformedAuthorize, #[error("Message pack: malformed create payment message")] @@ -84,6 +86,16 @@ pub struct OrderCreated { pub redirect_uri: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[repr(C)] +pub struct UpdateStatus { + pub payload: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[repr(C)] +pub struct StatusUpdated {} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[repr(C)] pub enum HttpMethod { diff --git a/crates/payment_adapter_pay_u/src/lib.rs b/crates/payment_adapter_pay_u/src/lib.rs index dad6de8..c329e62 100644 --- a/crates/payment_adapter_pay_u/src/lib.rs +++ b/crates/payment_adapter_pay_u/src/lib.rs @@ -1,6 +1,7 @@ mod credit; mod deserialize; mod model; +mod notify; mod req; mod res; mod serialize; @@ -66,6 +67,7 @@ impl PayU { wapc::register_function("init", Self::init); wapc::register_function("create_payment", Self::create_payment); wapc::register_function("cancel_order", Self::cancel_order); + wapc::register_function("update_status", Self::update_status); } fn teardown(_msg: &[u8]) -> wapc::CallResult { @@ -95,9 +97,9 @@ impl PayU { } fn create_payment(msg: &[u8]) -> wapc::CallResult { - let c: CreatePayment = match deserialize(msg) { + let c: CreatePayment = match wapc_codec::messagepack::deserialize(msg) { Ok(c) => c, - _ => return Err(Box::new(Error::MalformedCreatePayment)), + _ => return Err(Box::new(Error::MsgPackDeserializationFailed)), }; info!("{:?}", c); @@ -118,6 +120,24 @@ impl PayU { fn cancel_order(_msg: &[u8]) -> wapc::CallResult { Ok(vec![]) } + + fn update_status(msg: &[u8]) -> wapc::CallResult { + let update: UpdateStatus = match wapc_codec::messagepack::deserialize(msg) { + Ok(c) => c, + _ => return Err(Box::new(Error::MsgPackDeserializationFailed)), + }; + let msg: notify::StatusUpdate = match serde_json::from_slice(&update.payload) { + Ok(msg) => msg, + Err(e) => { + error!("{}", e); + return Err(Box::new(Error::MalformedStatusUpdate)); + } + }; + let _status = msg.order.status; + let _xid = msg.order.ext_order_id; + + Ok(wapc_codec::messagepack::serialize(StatusUpdated {}).unwrap()) + } } fn json_header() -> Vec<(String, String)> { diff --git a/crates/payment_adapter_pay_u/src/model.rs b/crates/payment_adapter_pay_u/src/model.rs index 0c8add5..6064ee6 100644 --- a/crates/payment_adapter_pay_u/src/model.rs +++ b/crates/payment_adapter_pay_u/src/model.rs @@ -120,14 +120,14 @@ pub enum CodeLiteral { pub type Result = std::result::Result; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Prop { pub name: String, pub value: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Refund { pub refund_id: String, diff --git a/crates/payment_adapter_pay_u/src/notify.rs b/crates/payment_adapter_pay_u/src/notify.rs new file mode 100644 index 0000000..a41664b --- /dev/null +++ b/crates/payment_adapter_pay_u/src/notify.rs @@ -0,0 +1,162 @@ +//! Notification objects. Those objects will be send on notify_url if was given. +//! +//! To enable notifications for a specific payment, specify the notifyUrl +//! parameter in the payment form. Each payment can receive a different URL to +//! which notifications will be sent. +//! +//! Every notification is sent asynchronously. After your system receives a +//! notification with the status COMPLETED, instruct it to ignore any further +//! notifications. +//! +//! After sending a notification, PayU system requires a 200 HTTP status code in +//! response. If it receives a different status code, it will resend the +//! notification. Your system should also take account of situations where a +//! notification is sent several times with the same status. For each repeated +//! notification, response with code 200 should be sent as well. +//! +//! To ensure trusted communication between PayU and your shop, you must verify +//! the signature value available in the OpenPayu-Signature header each time +//! your system receives any notification from a PayU server. Refer to the +//! Verification of notifications signature for more information. +//! +//! Notifications are sent for orders in the following statuses: PENDING, +//! WAITING_FOR_CONFIRMATION, COMPLETED, CANCELED. +//! +//! Note: if you filter IP addresses, remember to allow IPs used by PayU to send +//! the notifications. These are: +//! +//! ### PRODUCTION +//! +//! > 185.68.12.10, 185.68.12.11, 185.68.12.12, 185.68.12.26, 185.68.12.27, +//! > 185.68.12.28 +//! +//! ### SANDBOX +//! +//! > 185.68.14.10, 185.68.14.11, 185.68.14.12, 185.68.14.26, 185.68.14.27, +//! > 185.68.14.28 +//! +//! ## Payment lifecycle +//! +//! You can configure a separate auto-receive / automatic collection parameter +//! for each payment method via the Management Panel. +//! +//! By default, auto-receive is enabled. The basic payment sequence is as +//! follows: +//! +//! 1) Each successfully authorized payment for an order is captured. +//! 2) The buyer is charged with the order amount. +//! 3) The shop balance is increased by the order amount. +//! 4) PayU calculates its commission to the order. +//! +//! If the auto-receive is turned off, you should capture each order using a PUT +//! method (Order capture) or cancel using DELETE method (Cancellation). +//! +//! If no such action is taken the order is auto-canceled. Automatic +//! cancellation occurs after a number of days indicated for the payment method. +//! +//! + +use serde::{Deserialize, Serialize}; + +use super::deserialize; +use crate::req::*; +use crate::OrderId; + +/// Payment notification object received on notify_url +/// +/// See [crate::req::CreateOrder::notify_url] +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StatusUpdate { + /// Section containing order object + pub order: Order, + /// Moment of accepting the transaction and adding funds from the + /// transaction to the Shop balance. Format: "%Y-%M-%DT%h:%m:%s%z." + /// Example: "2020-06-09T17:52:04.644+02:00". If the millisecond counter + /// is "000" then milliseconds are not sent and the format changes to: + /// "%Y-%M-%DT%h:%m:%s". Is present only for the status "COMPLETED". + pub local_receipt_date_time: Option, + /// Array of objects related to transaction identification. In case of + /// statuses: + /// * `"WAITING_FOR_CONFIRMATION"` and `"COMPLETED"` - Contains one element + /// with two parameters: name and value, + /// * `"PENDING"` - may contain object with aforementioned parameters or it + /// can be empty. + /// + /// Also properties `name` + /// + /// Static value. The payment identifier, displayed on transaction + /// statements as "Trans ID" and within the transaction search option in + /// the Management Panel. + /// + /// Also properties `value` + /// + /// Transaction ID in PayU system (data type - string). + pub properties: Option>, +} + +impl StatusUpdate { + pub fn status(&self) -> PaymentStatus { + self.order.status + } + + /// See [crate::req::OrderCreate::ext_order_id] + pub fn ext_order_id(&self) -> &str { + self.order.ext_order_id.as_deref().unwrap_or_default() + } + + pub fn has_ext_order_id(&self) -> bool { + self.order.ext_order_id.is_some() + } +} + +/// Refund notification object +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RefundUpdate { + pub ext_order_id: String, + pub order_id: OrderId, + pub refund: Refund, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Refund { + pub refund_id: String, + pub amount: String, + pub currency_code: String, + pub status: RefundStatus, + pub status_date_time: String, + pub reason: String, + pub reason_description: String, + pub refund_date: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Order { + pub notify_url: Option, + /// Customer client IP address + pub customer_ip: String, + /// Secret pos ip. This is connected to PayU account + #[serde(deserialize_with = "deserialize::deserialize_i32_newtype")] + pub merchant_pos_id: MerchantPosId, + /// Transaction description + pub description: String, + /// 3 characters currency identifier, ex. PLN + pub currency_code: String, + /// Total price of the order in pennies (e.g. 1000 is 10.00 EUR). + /// Applies also to currencies without subunits (e.g. 1000 is 10 + /// HUF). + #[serde(deserialize_with = "deserialize::deserialize_i32")] + pub total_amount: Price, + /// @see [crate::Buyer] + pub buyer: Option, + /// List of products + pub products: Vec, + #[serde(skip_serializing)] + pub order_create_date: Option, + pub pay_method: Option, + pub status: PaymentStatus, + pub ext_order_id: Option, +} diff --git a/crates/payment_adapter_pay_u/src/req.rs b/crates/payment_adapter_pay_u/src/req.rs index 18da7fa..5c941a4 100644 --- a/crates/payment_adapter_pay_u/src/req.rs +++ b/crates/payment_adapter_pay_u/src/req.rs @@ -5,6 +5,33 @@ use serde::{Deserialize, Serialize}; use crate::credit::Credit; pub use crate::model::*; +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PaymentType { + Pbl, + CardToken, + Installments, +} + +/// Wrapper around pay method. It's used only for deserializing notifications +/// +/// # Examples +/// +/// ``` +/// # use pay_u::PayMethod; +/// let method: PayMethod = serde_json::from_str(r#" +/// { +/// "type": "INSTALLMENTS" +/// } +/// "#).unwrap(); +/// ``` +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayMethod { + #[serde(rename = "type")] + pub payment_type: PaymentType, +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Refund {