1use std::collections::BTreeMap;
33use std::fmt;
34
35use serde::{Deserialize, Serialize};
36
37use crate::error::CoreError;
38use crate::intent::SpendIntent;
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum PayMode {
47 #[default]
50 PendingPay,
51 AutoDebit,
54 Manual,
56}
57
58impl PayMode {
59 pub fn label(self) -> &'static str {
61 match self {
62 PayMode::PendingPay => "人在环待支付",
63 PayMode::AutoDebit => "免密代扣",
64 PayMode::Manual => "纯闸",
65 }
66 }
67
68 pub fn opens_pending(self) -> bool {
70 matches!(self, PayMode::PendingPay)
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum PendingState {
78 Open,
80 Confirmed,
82 Completed,
84 Voided,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum PendingOutcome {
92 Completed,
94 ExpiredVoid,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100pub struct PendingOrder {
101 pub pending_id: String,
103 pub delegation_id: String,
105 pub intent: SpendIntent,
107 pub approved_amount_cents: u64,
109 pub created_ts: u64,
111 pub expires_ts: u64,
113 pub state: PendingState,
115 pub proof: Option<String>,
117 pub confirmed_ts: Option<u64>,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
127pub struct PendingReceipt {
128 pub pending_id: String,
130 pub approved_amount_cents: u64,
132 pub expires_ts: u64,
134 pub wal_line: Option<u64>,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum PendingError {
141 UnknownPending { pending_id: String },
143 NotOpen {
145 pending_id: String,
146 state: PendingState,
147 },
148 AmountMismatch {
150 pending_id: String,
151 approved_cents: u64,
152 given_cents: u64,
153 },
154 Expired {
156 pending_id: String,
157 expires_ts: u64,
158 now_ts: u64,
159 },
160 InvalidTtl { ttl_secs: u64 },
162 EmptyProof,
164 NotConfirmed {
166 pending_id: String,
167 state: PendingState,
168 },
169 NotYetExpired {
171 pending_id: String,
172 expires_ts: u64,
173 now_ts: u64,
174 },
175 DuplicatePendingId { pending_id: String },
177}
178
179impl fmt::Display for PendingError {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 PendingError::UnknownPending { pending_id } => {
183 write!(f, "待支付单不存在: {pending_id}")
184 }
185 PendingError::NotOpen { pending_id, state } => write!(
186 f,
187 "待支付单 {pending_id} 不在待支付状态(当前 {state:?}),不能再次确认(幂等)"
188 ),
189 PendingError::AmountMismatch {
190 pending_id,
191 approved_cents,
192 given_cents,
193 } => write!(
194 f,
195 "待支付单 {pending_id} 金额不一致:审批 {approved_cents} 分,确认 {given_cents} 分(防夹带,拒)"
196 ),
197 PendingError::Expired {
198 pending_id,
199 expires_ts,
200 now_ts,
201 } => write!(
202 f,
203 "待支付单 {pending_id} 已过期(过期时刻 {expires_ts},当前 {now_ts}),确认被拒"
204 ),
205 PendingError::InvalidTtl { ttl_secs } => {
206 write!(f, "待支付 TTL 非法: {ttl_secs} 秒(必须 > 0)")
207 }
208 PendingError::EmptyProof => {
209 write!(f, "支付凭证为空:确认必须带交易号,回放才可对账")
210 }
211 PendingError::NotConfirmed { pending_id, state } => write!(
212 f,
213 "待支付单 {pending_id} 未处于已确认状态(当前 {state:?}),不能记完成"
214 ),
215 PendingError::NotYetExpired {
216 pending_id,
217 expires_ts,
218 now_ts,
219 } => write!(
220 f,
221 "待支付单 {pending_id} 还没到期(过期时刻 {expires_ts},当前 {now_ts}),不能作废"
222 ),
223 PendingError::DuplicatePendingId { pending_id } => {
224 write!(f, "待支付单号重复: {pending_id}(单号必须唯一)")
225 }
226 }
227 }
228}
229
230impl std::error::Error for PendingError {}
231
232#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
237pub struct PendingLedger {
238 orders: BTreeMap<String, PendingOrder>,
239}
240
241impl PendingLedger {
242 pub fn new() -> Self {
243 Self {
244 orders: BTreeMap::new(),
245 }
246 }
247
248 pub fn is_empty(&self) -> bool {
249 self.orders.is_empty()
250 }
251
252 pub fn len(&self) -> usize {
253 self.orders.len()
254 }
255
256 pub fn get(&self, pending_id: &str) -> Option<&PendingOrder> {
257 self.orders.get(pending_id)
258 }
259
260 pub fn contains_key(&self, pending_id: &str) -> bool {
261 self.orders.contains_key(pending_id)
262 }
263
264 pub fn iter(&self) -> impl Iterator<Item = (&String, &PendingOrder)> {
266 self.orders.iter()
267 }
268
269 pub fn contains_intent(&self, delegation_id: &str, nonce: u64) -> bool {
272 self.orders
273 .values()
274 .any(|o| o.delegation_id == delegation_id && o.intent.nonce == nonce)
275 }
276
277 pub(crate) fn apply_open(&mut self, order: PendingOrder) -> Result<(), CoreError> {
279 if self.orders.contains_key(&order.pending_id) {
280 return Err(CoreError::Pending(PendingError::DuplicatePendingId {
281 pending_id: order.pending_id.clone(),
282 }));
283 }
284 self.orders.insert(order.pending_id.clone(), order);
285 Ok(())
286 }
287
288 pub(crate) fn check_confirm(
292 &self,
293 pending_id: &str,
294 amount_cents: u64,
295 now_ts: u64,
296 ) -> Result<(), PendingError> {
297 let order = self
298 .orders
299 .get(pending_id)
300 .ok_or_else(|| PendingError::UnknownPending {
301 pending_id: pending_id.to_string(),
302 })?;
303 if order.approved_amount_cents != amount_cents {
306 return Err(PendingError::AmountMismatch {
307 pending_id: pending_id.to_string(),
308 approved_cents: order.approved_amount_cents,
309 given_cents: amount_cents,
310 });
311 }
312 if order.state != PendingState::Open {
313 return Err(PendingError::NotOpen {
314 pending_id: pending_id.to_string(),
315 state: order.state,
316 });
317 }
318 if now_ts >= order.expires_ts {
319 return Err(PendingError::Expired {
320 pending_id: pending_id.to_string(),
321 expires_ts: order.expires_ts,
322 now_ts,
323 });
324 }
325 Ok(())
326 }
327
328 pub(crate) fn apply_confirm(
330 &mut self,
331 pending_id: &str,
332 amount_cents: u64,
333 proof: &str,
334 now_ts: u64,
335 ) -> Result<(), CoreError> {
336 self.check_confirm(pending_id, amount_cents, now_ts)
337 .map_err(CoreError::Pending)?;
338 let order = self
339 .orders
340 .get_mut(pending_id)
341 .expect("check_confirm 已确认单存在");
342 order.state = PendingState::Confirmed;
343 order.proof = Some(proof.to_string());
344 order.confirmed_ts = Some(now_ts);
345 Ok(())
346 }
347
348 pub(crate) fn apply_complete(&mut self, pending_id: &str) -> Result<(), CoreError> {
350 let order = self.orders.get_mut(pending_id).ok_or_else(|| {
351 CoreError::Pending(PendingError::UnknownPending {
352 pending_id: pending_id.to_string(),
353 })
354 })?;
355 if order.state != PendingState::Confirmed {
356 return Err(CoreError::Pending(PendingError::NotConfirmed {
357 pending_id: pending_id.to_string(),
358 state: order.state,
359 }));
360 }
361 order.state = PendingState::Completed;
362 Ok(())
363 }
364
365 pub(crate) fn apply_void(&mut self, pending_id: &str, now_ts: u64) -> Result<(), CoreError> {
367 let order = self.orders.get_mut(pending_id).ok_or_else(|| {
368 CoreError::Pending(PendingError::UnknownPending {
369 pending_id: pending_id.to_string(),
370 })
371 })?;
372 if order.state != PendingState::Open {
373 return Err(CoreError::Pending(PendingError::NotOpen {
374 pending_id: pending_id.to_string(),
375 state: order.state,
376 }));
377 }
378 if now_ts < order.expires_ts {
379 return Err(CoreError::Pending(PendingError::NotYetExpired {
380 pending_id: pending_id.to_string(),
381 expires_ts: order.expires_ts,
382 now_ts,
383 }));
384 }
385 order.state = PendingState::Voided;
386 Ok(())
387 }
388}