yandex_pay_api/
lib.rs

1mod orders;
2mod orders_cancel;
3mod orders_capture;
4mod orders_id;
5mod orders_refund;
6mod orders_submit;
7mod orders_subscriptions;
8mod orders_subscriptions_id;
9mod orders_subscriptions_recur;
10mod serde_help;
11use std::sync::Arc;
12
13use builder_pattern::Builder;
14use bytes::Bytes;
15pub use orders::*;
16pub use orders_cancel::*;
17pub use orders_capture::*;
18pub use orders_id::*;
19pub use orders_refund::*;
20pub use orders_submit::*;
21pub use orders_subscriptions::*;
22pub use orders_subscriptions_id::*;
23pub use orders_subscriptions_recur::*;
24
25pub trait HttpClient: Clone {
26    fn send<T: serde::de::DeserializeOwned>(
27        &self,
28        request: YandexPayApiRequest,
29    ) -> impl Future<Output = R<T>>;
30}
31
32pub(crate) type R<T = (), E = YandexPayApiError> = std::result::Result<T, E>;
33pub(crate) type Time = chrono::DateTime<chrono::Utc>;
34
35#[derive(Debug, thiserror::Error)]
36#[error("Yandex Pay API error: {0}")]
37pub enum YandexPayApiError {
38    #[error("Yandex Pay reqwest error: {0}")]
39    Reqwest(#[from] reqwest::Error),
40    #[error("Yandex Pay serde error: {0}")]
41    Serde(#[from] serde_json::Error),
42    #[error("Yandex Pay API error: {0}")]
43    Api(YandexPayApiResponseError),
44}
45
46pub(crate) type S = Arc<str>;
47#[cfg(not(feature = "reqwest"))]
48#[derive(Debug, Clone)]
49pub struct YandexPayApi<C: HttpClient> {
50    pub client: C,
51    pub base_url: S,
52    pub api_key: S,
53}
54
55#[cfg(feature = "reqwest")]
56#[derive(Debug, Clone)]
57pub struct YandexPayApi<C: HttpClient = reqwest::Client> {
58    pub client: C,
59    pub base_url: S,
60    pub api_key: S,
61}
62impl<C: HttpClient> YandexPayApi<C> {
63    pub fn new(base_url: S, api_key: S, client: C) -> Self {
64        YandexPayApi {
65            client,
66            base_url,
67            api_key,
68        }
69    }
70
71    pub fn get_base_url(&self) -> &str {
72        &self.base_url
73    }
74
75    pub fn get_api_key(&self) -> &str {
76        &self.api_key
77    }
78}
79
80/// Yandex Pay API
81impl<C: HttpClient> YandexPayApi<C> {
82    /// Запрос на создание ссылки на оплату заказа.
83    ///
84    /// Запрос используется для создания и получения ссылки на оплату заказа.
85    pub async fn create_order(&self, request: CreateOrderRequest) -> R<CreateOrderResponse> {
86        let url = format!("{}/api/merchant/v1/orders", self.base_url);
87        let bytes = serde_json::to_vec(&request)?;
88        let r = YandexPayApiRequest::new()
89            .url(url)
90            .method(Method::Post)
91            .body(Some(bytes.into()))
92            .api_key(self.api_key.clone())
93            .build();
94        let response = self.client.send(r).await?;
95        Ok(response)
96    }
97    /// Запрос на получение деталей заказа.
98    ///
99    /// Запрос возвращает детали заказа и список транзакций по возврату.
100    pub async fn get_order(&self, order_id: impl Into<String>) -> R<OrderResponseData> {
101        let url = format!(
102            "{}/api/merchant/v1/orders/{}",
103            self.base_url,
104            order_id.into()
105        );
106        let r = YandexPayApiRequest::new()
107            .url(url)
108            .api_key(self.api_key.clone())
109            .build();
110        let response = self.client.send(r).await?;
111        Ok(response)
112    }
113    /// Запрос на отмену платежа.
114    ///
115    /// Доступно только для платежей в статусе AUTHORIZED. В случае успеха статус платежа изменится на VOIDED.
116    pub async fn cancel_order(
117        &self,
118        order_id: impl Into<String>,
119        request: CancelOrderRequest,
120    ) -> R<OperationResponseData> {
121        let url = format!(
122            "{}/api/merchant/v1/orders/{}/cancel",
123            self.base_url,
124            order_id.into()
125        );
126        let bytes = serde_json::to_vec(&request)?;
127        let r = YandexPayApiRequest::new()
128            .url(url)
129            .api_key(self.api_key.clone())
130            .method(Method::Post)
131            .body(Some(bytes.into()))
132            .build();
133        let response = self.client.send(r).await?;
134        Ok(response)
135    }
136    /// Запрос на возврат средств за заказ.
137    ///
138    /// Доступно только для платежей в статусе CAPTURED и PARTIALLY_REFUNDED. В случае успешного выполнения запроса изменится статус платежа:
139    ///
140    ///     на REFUNDED, если был произведен полный возврат;
141    ///
142    ///     на PARTIALLY_REFUNDED, если после совершения возврата в заказе остались ещё товары.
143    ///
144    /// Метод является асинхронным.
145    ///
146    /// Узнать результат возврата можно через механизм событий или вызов метода состояния операции. Событие OPERATION_STATUS_UPDATED будет отправлено как в случае успеха, так и при возникновении ошибки в процессе совершения возврата.
147    ///
148    /// Для выполнения полного возврата достаточно передать refundAmount, равный сумме заказа.
149    ///
150    /// Для выполнения частичного возврата дополнительно нужно передать итоговую корзину предоставляемых товаров и услуг. Сформировать итоговую корзину можно одним из способов:
151    ///     передать целевое состояние корзины после выполнения возврата с помощью поля targetCart. Если это поле не указано, то считается, что корзина возвращается полностью.
152    ///
153    ///     Поле targetShipping применимо только к Yandex Pay Checkout. В остальных случаях следует оставить это поле пустым. Если это поле не указано, то считается, что стоимость доставки возвращается полностью.
154    ///     
155    ///     передать данные о товарах, подлежащих возврату, с помошью поля refundCart: в поле укажите, сколько единиц товара нужно вернуть или на какую сумму следует уменьшить стоимость товара. Если поле не указано, возврат осуществляется на всю корзину.
156    ///
157    /// Примечание
158    ///
159    ///     Для данной стратегии рекомендуется указывать идентификатор операции externalOperationId, который служит токеном идемпотентности. Это позволит избежать риска повторных возвратов.
160    pub async fn refund_order(
161        &self,
162        order_id: impl Into<String>,
163        request: RefundRequest,
164    ) -> R<OperationResponseData> {
165        let url = format!(
166            "{}/api/merchant/v2/orders/{}/refund",
167            self.base_url,
168            order_id.into()
169        );
170        let bytes = serde_json::to_vec(&request)?;
171        let r = YandexPayApiRequest::new()
172            .url(url)
173            .api_key(self.api_key.clone())
174            .method(Method::Post)
175            .body(Some(bytes.into()))
176            .build();
177        let response = self.client.send(r).await?;
178        Ok(response)
179    }
180    /// Запрос на списание средств за заказ.
181    ///
182    /// Списание заблокированных средств. Доступно только для платежей в статусе AUTHORIZED. При успешном результате запроса статус платежа изменится на CAPTURED.
183    ///
184    /// В случае передачи суммы подтверждения меньшей, чем оригинальная, оставшаяся часть платежа будет возвращена. В данном случае нужно передать итоговую корзину предоставляемых товаров и услуг. Итоговая корзина должна формироваться из текущей корзины исключением некоторых позиций, по которым производился возврат.
185    pub async fn capture_order(
186        &self,
187        order_id: impl Into<String>,
188        request: CaptureOrderRequest,
189    ) -> R<OperationResponseData> {
190        let url = format!(
191            "{}/api/merchant/v1/orders/{}/capture",
192            self.base_url,
193            order_id.into()
194        );
195        let bytes = serde_json::to_vec(&request)?;
196        let r = YandexPayApiRequest::new()
197            .url(url)
198            .api_key(self.api_key.clone())
199            .method(Method::Post)
200            .body(Some(bytes.into()))
201            .build();
202        let response = self.client.send(r).await?;
203        Ok(response)
204    }
205    /// Запрос на отмену платежа.
206    ///
207    /// Доступно для платежей в любом статусе. Запрещает дальнейшую оплату заказа, а также, если оплата уже произошла, производит полный возврат средств клиенту. В случае успеха статус платежа изменится на FAILED.
208    pub async fn rollback_order(&self, order_id: impl Into<String>) -> R<serde_json::Value> {
209        let url = format!(
210            "{}/api/merchant/v1/orders/{}/rollback",
211            self.base_url,
212            order_id.into()
213        );
214        let r = YandexPayApiRequest::new()
215            .url(url)
216            .api_key(self.api_key.clone())
217            .method(Method::Post)
218            .build();
219        let response = self.client.send(r).await?;
220        Ok(response)
221    }
222
223    /// Запрос на подтверждение оплаты для Сплита с оплатой при получении.
224    ///
225    /// Доступно для платежей в статусе CONFIRMED. При успешном результате запроса статус изменится на CAPTURED.
226    ///
227    /// Если состав корзины на этапе подтверждения заказа отличается от состава корзины на этапе оформления, передайте новые значения для полей cart и orderAmount.
228    pub async fn submit_order(
229        &self,
230        order_id: impl Into<String>,
231        request: SubmitRequest,
232    ) -> R<OperationResponseData> {
233        let url = format!(
234            "{}/api/merchant/v1/orders/{}/submit",
235            self.base_url,
236            order_id.into()
237        );
238        let bytes = serde_json::to_vec(&request)?;
239        let r = YandexPayApiRequest::new()
240            .url(url)
241            .api_key(self.api_key.clone())
242            .method(Method::Post)
243            .body(Some(bytes.into()))
244            .build();
245        let response = self.client.send(r).await?;
246        Ok(response)
247    }
248
249    pub async fn get_operation(
250        &self,
251        external_operation_id: impl Into<String>,
252    ) -> R<OperationResponseData> {
253        let url = format!(
254            "{}/api/merchant/v1/operations/{}",
255            self.base_url,
256            external_operation_id.into()
257        );
258        let r = YandexPayApiRequest::new()
259            .url(url)
260            .api_key(self.api_key.clone())
261            .method(Method::Get)
262            .build();
263        let response = self.client.send(r).await?;
264        Ok(response)
265    }
266    /// Запрос на создание подписки.
267    ///
268    /// Используется для создания подписки и получения ссылки для ее оформления.
269    pub async fn create_subscription(
270        &self,
271        subscription: CreateSubscriptionRequest,
272    ) -> Result<CreateSubscriptionResponseData, YandexPayApiError> {
273        let url = format!("{}/api/merchant/v1/subscriptions", self.base_url);
274        let bytes = serde_json::to_vec(&subscription)?;
275        let r = YandexPayApiRequest::new()
276            .url(url)
277            .api_key(self.api_key.clone())
278            .method(Method::Post)
279            .body(Some(bytes.into()))
280            .build();
281        let response = self.client.send(r).await?;
282        Ok(response)
283    }
284
285    /// Запрос для очередного списания по подписке.
286    ///
287    /// Запрос используется для безакцептного списания денежных средств с привязанного к подписке счета или карты. Для списания нужно передать номер заказа, корзину, сумму списания, и номер стартового заказа, который был использован при создании подписки.
288    pub async fn recur_subscription(
289        &self,
290        subscription: CreateRecurrentChargeRequest,
291    ) -> Result<RecurSubscriptionResponseData, YandexPayApiError> {
292        let url = format!("{}/api/merchant/v1/subscriptions/recur", self.base_url);
293        let bytes = serde_json::to_vec(&subscription)?;
294        let r = YandexPayApiRequest::new()
295            .url(url)
296            .api_key(self.api_key.clone())
297            .method(Method::Post)
298            .body(Some(bytes.into()))
299            .build();
300        let response = self.client.send(r).await?;
301        Ok(response)
302    }
303
304    ///Запрос на получение информации по подписке.
305    ///
306    /// Возвращает идентификатор подписки, состояние подписки и привязанного способа оплаты.
307    pub async fn get_subscription(
308        &self,
309        // ID подписки
310        customer_subscription_id: impl Into<String>,
311        request: GetSubscriptionRequest,
312    ) -> Result<CustomerSubscriptionResponseData, YandexPayApiError> {
313        let url = format!(
314            "{}/api/merchant/v1/subscriptions/{}",
315            self.base_url,
316            customer_subscription_id.into()
317        );
318        let bytes = serde_json::to_vec(&request)?;
319        let r = YandexPayApiRequest::new()
320            .url(url)
321            .api_key(self.api_key.clone())
322            .method(Method::Get)
323            .body(Some(bytes.into()))
324            .build();
325        let response = self.client.send(r).await?;
326        Ok(response)
327    }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
331pub enum Method {
332    Get,
333    Post,
334}
335
336#[derive(Debug, Clone, Builder)]
337pub struct YandexPayApiRequest {
338    #[default(None)]
339    //serialized body
340    pub body: Option<Bytes>,
341    #[default(Method::Get)]
342    //Method
343    pub method: Method,
344    #[into]
345    //url
346    pub url: S,
347    #[into]
348    //Authorization Token
349    pub api_key: S,
350    #[into]
351    #[default(default_request_id())]
352    //Request Id
353    pub request_id: S,
354    #[default(9999)]
355    //In milliseconds
356    pub request_timeout: u32,
357    #[default(0)]
358    //Current attempt number
359    pub request_attempt: u32,
360}
361
362fn default_request_id() -> S {
363    uuid::Uuid::now_v7().to_string().into()
364}
365#[cfg(feature = "reqwest")]
366impl HttpClient for reqwest::Client {
367    fn send<T: serde::de::DeserializeOwned>(
368        &self,
369        request: YandexPayApiRequest,
370    ) -> impl Future<Output = R<T>> {
371        let client = self.clone();
372
373        async move {
374            let body = request.body.clone();
375            let method = match request.method {
376                Method::Get => reqwest::Method::GET,
377                Method::Post => reqwest::Method::POST,
378            };
379            let mut request_builder = client
380                .request(method, &*request.url)
381                .header("Authorization", format!("Api-Key {}", request.api_key))
382                .header("X-Request-Id", &*request.request_id)
383                .header("X-Request-Timeout", request.request_timeout.to_string())
384                .header("X-Request-Attempt", request.request_attempt.to_string())
385                .header("Content-Type", "application/json");
386            if let Some(body) = body {
387                request_builder = request_builder.body(body);
388            }
389            let response = request_builder.send().await?;
390
391            if response.status().is_success() {
392                let result = response.text().await?;
393                let result = serde_json::from_str::<YandexPayApiResponse<T>>(&result)?;
394                Ok(result.data)
395            } else {
396                let error_message = response.text().await?;
397                tracing::error!("{}", error_message);
398                let error = serde_json::from_str::<YandexPayApiResponseError>(&error_message)?;
399                Err(YandexPayApiError::Api(error))
400            }
401        }
402    }
403}
404
405#[derive(Debug, serde::Deserialize)]
406pub struct YandexPayApiResponse<T> {
407    pub data: T,
408    pub code: Option<u32>,
409    pub status: Option<String>,
410}
411
412#[derive(Debug, serde::Deserialize)]
413pub struct YandexPayApiResponseError {
414    pub code: Option<u32>,
415    pub status: Option<String>,
416    #[serde(default = "Default::default")]
417    pub message: S,
418}
419
420impl std::fmt::Display for YandexPayApiResponseError {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(
423            f,
424            "Yandex Pay API error: StatusCode: {:?}, Status: {:?}, Message: {}",
425            self.code, self.status, self.message
426        )
427    }
428}