-
Notifications
You must be signed in to change notification settings - Fork 150
Expand file tree
/
Copy pathpayment_requests.rs
More file actions
153 lines (135 loc) · 4.99 KB
/
Copy pathpayment_requests.rs
File metadata and controls
153 lines (135 loc) · 4.99 KB
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
use axum::extract::{Path, Query, State};
use axum::Json;
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
use crate::auth::extractor::AuthUser;
use crate::error::{bad_request, internal, not_found, ApiResult};
use crate::models::{CreatePaymentRequestRequest, PaymentRequest};
use crate::services::{payment_requests, wallets};
use crate::AppState;
#[derive(Serialize)]
pub struct PaymentRequestView {
pub id: Uuid,
pub merchant_id: Uuid,
pub address: String,
pub network: String,
pub amount_stroops: i64,
pub asset: String,
pub memo: String,
pub status: String,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
/// SEP-0007 payment URI a Stellar wallet can open directly to pay this
/// request. `None` for credit assets we don't have a real issuer address
/// configured for yet (see PRD §9.4) — we don't guess one.
pub sep7_uri: Option<String>,
}
pub async fn create(
State(state): State<AppState>,
auth: AuthUser,
Json(req): Json<CreatePaymentRequestRequest>,
) -> ApiResult<Json<PaymentRequestView>> {
let merchant_id = auth
.merchant_id
.ok_or_else(|| bad_request("no merchant associated with this account"))?;
let wallet = wallets::wallet_by_merchant(&state.db, merchant_id)
.await
.map_err(internal)?
.ok_or_else(|| bad_request("create a wallet before generating payment requests"))?;
// Defaults to XLM, not cNGN like withdrawals: XLM is what's actually
// scannable/testable today (no cNGN issuer address configured yet).
let asset = req.asset.unwrap_or_else(|| "XLM".into());
let pr = payment_requests::create_payment_request(
&state.db,
merchant_id,
wallet.id,
req.amount_stroops,
asset,
req.expires_in_secs,
)
.await
.map_err(map_payment_request_error)?;
Ok(Json(to_view(&pr, &wallet.address, &wallet.network)))
}
pub async fn get(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<PaymentRequestView>> {
let pr = payment_requests::payment_request_by_id(&state.db, id)
.await
.map_err(internal)?
.ok_or_else(|| not_found("payment request not found"))?;
let wallet = wallets::wallet_by_id(&state.db, pr.wallet_id)
.await
.map_err(internal)?
.ok_or_else(|| internal("payment request references a missing wallet"))?;
Ok(Json(to_view(&pr, &wallet.address, &wallet.network)))
}
pub async fn list(
State(state): State<AppState>,
auth: AuthUser,
Query(params): Query<ListParams>,
) -> ApiResult<Json<Vec<PaymentRequestView>>> {
let merchant_id = auth
.merchant_id
.ok_or_else(|| bad_request("no merchant associated with this account"))?;
let limit = params.limit.unwrap_or(50).clamp(1, 200);
let rows = payment_requests::payment_requests_by_merchant(&state.db, merchant_id, limit)
.await
.map_err(internal)?;
Ok(Json(rows.iter().map(row_to_view).collect()))
}
#[derive(serde::Deserialize)]
pub struct ListParams {
pub limit: Option<i64>,
}
fn to_view(pr: &PaymentRequest, address: &str, network: &str) -> PaymentRequestView {
PaymentRequestView {
id: pr.id,
merchant_id: pr.merchant_id,
address: address.to_string(),
network: network.to_string(),
amount_stroops: pr.amount_stroops,
asset: pr.asset.clone(),
memo: pr.memo.clone(),
status: payment_requests::effective_status(&pr.status, pr.expires_at),
expires_at: pr.expires_at,
created_at: pr.created_at,
sep7_uri: build_sep7_uri(address, pr.amount_stroops, &pr.asset, &pr.memo),
}
}
fn row_to_view(row: &payment_requests::PaymentRequestWithWallet) -> PaymentRequestView {
PaymentRequestView {
id: row.id,
merchant_id: row.merchant_id,
address: row.address.clone(),
network: row.network.clone(),
amount_stroops: row.amount_stroops,
asset: row.asset.clone(),
memo: row.memo.clone(),
status: payment_requests::effective_status(&row.status, row.expires_at),
expires_at: row.expires_at,
created_at: row.created_at,
sep7_uri: build_sep7_uri(&row.address, row.amount_stroops, &row.asset, &row.memo),
}
}
fn build_sep7_uri(address: &str, amount_stroops: i64, asset: &str, memo: &str) -> Option<String> {
if asset != "XLM" && asset != "native" {
return None;
}
let amount = format!("{}.{:07}", amount_stroops / 10_000_000, amount_stroops % 10_000_000);
Some(format!(
"web+stellar:pay?destination={address}&amount={amount}&memo={memo}&memo_type=MEMO_TEXT"
))
}
fn map_payment_request_error(
err: payment_requests::PaymentRequestError,
) -> (axum::http::StatusCode, Json<crate::error::ApiError>) {
match err {
payment_requests::PaymentRequestError::InvalidAmount => {
bad_request("amount_stroops must be positive")
}
payment_requests::PaymentRequestError::Database(e) => internal(e),
}
}