paymos 1.0.0

Official Rust SDK for the Paymos Merchant API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
use serde::{Deserialize, Deserializer, Serialize, Serializer};

macro_rules! extensible_status {
    ($name:ident { $($variant:ident => $wire:literal),+ $(,)? }) => {
        #[doc = concat!("A forward-compatible `", stringify!($name), "` value.")]
        #[derive(Clone, Debug, Eq, Hash, PartialEq)]
        #[non_exhaustive]
        pub enum $name {
            $(#[doc = concat!("Wire value `", $wire, "`.")] $variant,)+
            /// A newer server value not known by this SDK version.
            Unknown(String),
        }

        impl $name {
            /// Returns the exact snake-case wire value.
            #[must_use]
            pub fn as_str(&self) -> &str {
                match self {
                    $(Self::$variant => $wire,)+
                    Self::Unknown(value) => value,
                }
            }
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str(self.as_str())
            }
        }

        impl Serialize for $name {
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
            where S: Serializer {
                serializer.serialize_str(self.as_str())
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where D: Deserializer<'de> {
                let value = String::deserialize(deserializer)?;
                Ok(match value.as_str() {
                    $($wire => Self::$variant,)+
                    _ => Self::Unknown(value),
                })
            }
        }
    };
}

extensible_status!(InvoiceStatus {
    AwaitingClient => "awaiting_client",
    AwaitingPayment => "awaiting_payment",
    Confirming => "confirming",
    UnderpaidWaiting => "underpaid_waiting",
    Paid => "paid",
    PaidOver => "paid_over",
    Underpaid => "underpaid",
    Expired => "expired",
    Cancelled => "cancelled",
});

extensible_status!(WithdrawalStatus {
    Created => "created",
    PendingReview => "pending_review",
    Signed => "signed",
    Cancelling => "cancelling",
    Completed => "completed",
    Failed => "failed",
    Cancelled => "cancelled",
});

/// Input for creating an invoice. Monetary amounts stay decimal strings.
#[derive(Clone, Debug, Serialize)]
pub struct CreateInvoiceRequest {
    /// Project that owns the invoice.
    pub project_id: String,
    /// Dot-decimal order amount.
    pub amount: String,
    /// Order currency, such as `USD`.
    pub currency: String,
    /// Merchant idempotency identifier.
    pub external_order_id: String,
    /// Optional preselected network code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub network: Option<String>,
    /// Whether the invoice accepts multiple transfers.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_multiple_payments: Option<bool>,
    /// Optional customer-paid fee percentage.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub customer_fee_percent: Option<f64>,
    /// Optional merchant customer identifier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_id: Option<String>,
}

/// Filters accepted by the invoice list endpoint.
#[derive(Clone, Debug, Default)]
pub struct InvoiceListParams {
    /// Maximum items returned by one page.
    pub limit: Option<u32>,
    /// Opaque cursor returned by the preceding page.
    pub cursor: Option<String>,
    /// Status filters. Repeated query values are emitted in sorted order.
    pub status: Option<Vec<InvoiceStatus>>,
    /// Merchant order identifier filter.
    pub external_order_id: Option<String>,
    /// Project filter.
    pub project_id: Option<String>,
    /// Inclusive Unix-seconds lower creation bound.
    pub created_from: Option<i64>,
    /// Inclusive Unix-seconds upper creation bound.
    pub created_to: Option<i64>,
}

impl InvoiceListParams {
    pub(crate) fn query_pairs(&self) -> Result<Vec<(String, String)>, crate::Error> {
        let mut values = Vec::new();
        push_option(&mut values, "limit", self.limit);
        push_string(&mut values, "cursor", self.cursor.as_deref());
        if let Some(statuses) = &self.status {
            if statuses.is_empty() {
                return Err(crate::Error::InvalidArgument(
                    "invoice status filter cannot be empty".to_owned(),
                ));
            }
            values.extend(
                statuses
                    .iter()
                    .map(|status| ("status".to_owned(), status.as_str().to_owned())),
            );
        }
        push_string(
            &mut values,
            "external_order_id",
            self.external_order_id.as_deref(),
        );
        push_string(&mut values, "project_id", self.project_id.as_deref());
        push_option(&mut values, "created_from", self.created_from);
        push_option(&mut values, "created_to", self.created_to);
        Ok(values)
    }
}

/// Merchant order embedded in an invoice.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Order {
    /// Merchant order identifier.
    pub external_id: String,
    /// Optional merchant customer identifier.
    pub client_id: Option<String>,
    /// Dot-decimal order amount.
    pub amount: String,
    /// Order currency.
    pub currency: String,
    /// Selected network, when known.
    pub network: Option<String>,
}

/// Blockchain transfer contributing to an invoice.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Transfer {
    /// Transaction hash.
    pub tx_hash: String,
    /// Dot-decimal transfer amount.
    pub amount: String,
    /// Transfer confirmation status.
    pub status: String,
    /// Creation timestamp in Unix seconds.
    pub created_at: i64,
    /// Confirmation timestamp in Unix seconds.
    pub confirmed_at: Option<i64>,
    /// Confirmations required for settlement.
    pub required_confirmations: Option<u32>,
    /// Estimated confirmation timestamp in Unix seconds.
    pub estimated_confirmation_at: Option<i64>,
    /// Public explorer URL.
    pub explorer_url: Option<String>,
}

/// Payment details selected for an invoice.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Payment {
    /// Token currency.
    pub currency: String,
    /// Network code.
    pub network: String,
    /// Numeric chain identifier.
    pub chain_id: i64,
    /// Token contract address, when applicable.
    pub contract_address: Option<String>,
    /// Expected dot-decimal token amount.
    pub expected: String,
    /// Deposit address, when allocated.
    pub address: Option<String>,
    /// Applied exchange rate.
    pub exchange_rate: Option<String>,
    /// Amount observed on chain.
    pub paid: Option<String>,
    /// Amount still due.
    pub remaining: Option<String>,
    /// Merchant fee.
    pub fee: Option<String>,
    /// Merchant net amount.
    pub net: Option<String>,
    /// Observed transfers.
    pub transfers: Option<Vec<Transfer>>,
}

/// Full invoice contract returned by the Merchant API.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Invoice {
    /// Paymos invoice identifier.
    pub invoice_id: String,
    /// Owning project identifier.
    pub project_id: String,
    /// Current invoice status.
    pub status: InvoiceStatus,
    /// Whether the status can no longer change.
    pub is_final: bool,
    /// Whether this is a sandbox invoice.
    pub is_test: bool,
    /// Hosted payment URL.
    pub payment_url: String,
    /// Merchant order data.
    pub order: Order,
    /// Payment selection and settlement details.
    pub payment: Option<Payment>,
    /// Creation timestamp in Unix seconds.
    pub created_at: i64,
    /// Last update timestamp in Unix seconds.
    pub updated_at: i64,
    /// Expiration timestamp in Unix seconds.
    pub expires_at: Option<i64>,
    /// Completion timestamp in Unix seconds.
    pub completed_at: Option<i64>,
}

/// Compact invoice returned from the list endpoint.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InvoiceListItem {
    /// Paymos invoice identifier.
    pub invoice_id: String,
    /// Owning project identifier.
    pub project_id: String,
    /// Merchant order identifier.
    pub external_order_id: String,
    /// Optional merchant customer identifier.
    pub client_id: Option<String>,
    /// Current status.
    pub status: InvoiceStatus,
    /// Whether the status is terminal.
    pub is_final: bool,
    /// Whether this is a sandbox invoice.
    pub is_test: bool,
    /// Dot-decimal order amount.
    pub amount: String,
    /// Order currency.
    pub currency: String,
    /// Selected network, when known.
    pub network: Option<String>,
    /// Creation timestamp in Unix seconds.
    pub created_at: i64,
    /// Expiration timestamp in Unix seconds.
    pub expires_at: Option<i64>,
    /// Completion timestamp in Unix seconds.
    pub completed_at: Option<i64>,
}

/// Input for creating a withdrawal. Monetary amounts stay decimal strings.
#[derive(Clone, Debug, Serialize)]
pub struct CreateWithdrawalRequest {
    /// Destination blockchain address.
    pub destination_address: String,
    /// Network code.
    pub network: String,
    /// Token currency.
    pub currency: String,
    /// Dot-decimal withdrawal amount.
    pub amount: String,
    /// Merchant idempotency identifier.
    pub external_order_id: String,
}

/// Filters accepted by the withdrawal list endpoint.
#[derive(Clone, Debug, Default)]
pub struct WithdrawalListParams {
    /// Maximum items returned by one page.
    pub limit: Option<u32>,
    /// Opaque cursor returned by the preceding page.
    pub cursor: Option<String>,
    /// Status filters.
    pub status: Option<Vec<WithdrawalStatus>>,
    /// Merchant order identifier filter.
    pub external_order_id: Option<String>,
    /// Inclusive Unix-seconds lower creation bound.
    pub created_from: Option<i64>,
    /// Inclusive Unix-seconds upper creation bound.
    pub created_to: Option<i64>,
}

impl WithdrawalListParams {
    pub(crate) fn query_pairs(&self) -> Result<Vec<(String, String)>, crate::Error> {
        let mut values = Vec::new();
        push_option(&mut values, "limit", self.limit);
        push_string(&mut values, "cursor", self.cursor.as_deref());
        if let Some(statuses) = &self.status {
            if statuses.is_empty() {
                return Err(crate::Error::InvalidArgument(
                    "withdrawal status filter cannot be empty".to_owned(),
                ));
            }
            values.extend(
                statuses
                    .iter()
                    .map(|status| ("status".to_owned(), status.as_str().to_owned())),
            );
        }
        push_string(
            &mut values,
            "external_order_id",
            self.external_order_id.as_deref(),
        );
        push_option(&mut values, "created_from", self.created_from);
        push_option(&mut values, "created_to", self.created_to);
        Ok(values)
    }
}

/// Full withdrawal contract returned by the Merchant API.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Withdrawal {
    /// Paymos withdrawal identifier.
    pub withdrawal_id: String,
    /// Merchant order identifier.
    pub external_order_id: String,
    /// Current withdrawal status.
    pub status: WithdrawalStatus,
    /// Whether the status is terminal.
    pub is_final: bool,
    /// Whether this is a sandbox withdrawal.
    pub is_test: bool,
    /// Dot-decimal requested amount.
    pub amount: String,
    /// Dot-decimal fee.
    pub fee: Option<String>,
    /// Token currency.
    pub currency: String,
    /// Network code.
    pub network: String,
    /// Destination blockchain address.
    pub destination_address: String,
    /// Broadcast transaction hash.
    pub tx_hash: Option<String>,
    /// Public explorer URL.
    pub explorer_url: Option<String>,
    /// Creation timestamp in Unix seconds.
    pub created_at: i64,
    /// Completion timestamp in Unix seconds.
    pub completed_at: Option<i64>,
    /// Failure timestamp in Unix seconds.
    pub failed_at: Option<i64>,
    /// Cancellation timestamp in Unix seconds.
    pub cancelled_at: Option<i64>,
}

/// A page returned from a cursor list endpoint.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CursorPage<T> {
    /// Items in this page.
    pub items: Vec<T>,
    /// Opaque cursor for the next page.
    pub next_cursor: Option<String>,
}

/// Merchant balance for one currency.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Balance {
    /// Currency symbol.
    pub currency: String,
    /// Available dot-decimal amount.
    pub available: String,
}

/// Server clock response.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ServerTime {
    /// Current server time in Unix seconds.
    pub server_time: i64,
}

/// Invoice sandbox simulation stage.
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InvoiceSimulationStage {
    /// Fully pay the invoice.
    Paid,
    /// Pay more than expected.
    Overpaid,
    /// Pay less than expected.
    Underpay,
    /// Cancel the invoice.
    Cancel,
}

fn push_string(values: &mut Vec<(String, String)>, key: &str, value: Option<&str>) {
    if let Some(value) = value {
        values.push((key.to_owned(), value.to_owned()));
    }
}

fn push_option<T: ToString>(values: &mut Vec<(String, String)>, key: &str, value: Option<T>) {
    if let Some(value) = value {
        values.push((key.to_owned(), value.to_string()));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn preserves_unknown_statuses() {
        let status: InvoiceStatus = serde_json::from_str("\"future_status\"").unwrap();
        assert_eq!(status, InvoiceStatus::Unknown("future_status".to_owned()));
        assert_eq!(serde_json::to_string(&status).unwrap(), "\"future_status\"");
    }
}