use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::error::CoreError;
use crate::intent::SpendIntent;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PayMode {
#[default]
PendingPay,
AutoDebit,
Manual,
}
impl PayMode {
pub fn label(self) -> &'static str {
match self {
PayMode::PendingPay => "人在环待支付",
PayMode::AutoDebit => "免密代扣",
PayMode::Manual => "纯闸",
}
}
pub fn opens_pending(self) -> bool {
matches!(self, PayMode::PendingPay)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PendingState {
Open,
Confirmed,
Completed,
Voided,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PendingOutcome {
Completed,
ExpiredVoid,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingOrder {
pub pending_id: String,
pub delegation_id: String,
pub intent: SpendIntent,
pub approved_amount_cents: u64,
pub created_ts: u64,
pub expires_ts: u64,
pub state: PendingState,
pub proof: Option<String>,
pub confirmed_ts: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingReceipt {
pub pending_id: String,
pub approved_amount_cents: u64,
pub expires_ts: u64,
pub wal_line: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PendingError {
UnknownPending { pending_id: String },
NotOpen {
pending_id: String,
state: PendingState,
},
AmountMismatch {
pending_id: String,
approved_cents: u64,
given_cents: u64,
},
Expired {
pending_id: String,
expires_ts: u64,
now_ts: u64,
},
InvalidTtl { ttl_secs: u64 },
EmptyProof,
NotConfirmed {
pending_id: String,
state: PendingState,
},
NotYetExpired {
pending_id: String,
expires_ts: u64,
now_ts: u64,
},
DuplicatePendingId { pending_id: String },
}
impl fmt::Display for PendingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PendingError::UnknownPending { pending_id } => {
write!(f, "待支付单不存在: {pending_id}")
}
PendingError::NotOpen { pending_id, state } => write!(
f,
"待支付单 {pending_id} 不在待支付状态(当前 {state:?}),不能再次确认(幂等)"
),
PendingError::AmountMismatch {
pending_id,
approved_cents,
given_cents,
} => write!(
f,
"待支付单 {pending_id} 金额不一致:审批 {approved_cents} 分,确认 {given_cents} 分(防夹带,拒)"
),
PendingError::Expired {
pending_id,
expires_ts,
now_ts,
} => write!(
f,
"待支付单 {pending_id} 已过期(过期时刻 {expires_ts},当前 {now_ts}),确认被拒"
),
PendingError::InvalidTtl { ttl_secs } => {
write!(f, "待支付 TTL 非法: {ttl_secs} 秒(必须 > 0)")
}
PendingError::EmptyProof => {
write!(f, "支付凭证为空:确认必须带交易号,回放才可对账")
}
PendingError::NotConfirmed { pending_id, state } => write!(
f,
"待支付单 {pending_id} 未处于已确认状态(当前 {state:?}),不能记完成"
),
PendingError::NotYetExpired {
pending_id,
expires_ts,
now_ts,
} => write!(
f,
"待支付单 {pending_id} 还没到期(过期时刻 {expires_ts},当前 {now_ts}),不能作废"
),
PendingError::DuplicatePendingId { pending_id } => {
write!(f, "待支付单号重复: {pending_id}(单号必须唯一)")
}
}
}
}
impl std::error::Error for PendingError {}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingLedger {
orders: BTreeMap<String, PendingOrder>,
}
impl PendingLedger {
pub fn new() -> Self {
Self {
orders: BTreeMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.orders.is_empty()
}
pub fn len(&self) -> usize {
self.orders.len()
}
pub fn get(&self, pending_id: &str) -> Option<&PendingOrder> {
self.orders.get(pending_id)
}
pub fn contains_key(&self, pending_id: &str) -> bool {
self.orders.contains_key(pending_id)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &PendingOrder)> {
self.orders.iter()
}
pub fn contains_intent(&self, delegation_id: &str, nonce: u64) -> bool {
self.orders
.values()
.any(|o| o.delegation_id == delegation_id && o.intent.nonce == nonce)
}
pub(crate) fn apply_open(&mut self, order: PendingOrder) -> Result<(), CoreError> {
if self.orders.contains_key(&order.pending_id) {
return Err(CoreError::Pending(PendingError::DuplicatePendingId {
pending_id: order.pending_id.clone(),
}));
}
self.orders.insert(order.pending_id.clone(), order);
Ok(())
}
pub(crate) fn check_confirm(
&self,
pending_id: &str,
amount_cents: u64,
now_ts: u64,
) -> Result<(), PendingError> {
let order = self
.orders
.get(pending_id)
.ok_or_else(|| PendingError::UnknownPending {
pending_id: pending_id.to_string(),
})?;
if order.approved_amount_cents != amount_cents {
return Err(PendingError::AmountMismatch {
pending_id: pending_id.to_string(),
approved_cents: order.approved_amount_cents,
given_cents: amount_cents,
});
}
if order.state != PendingState::Open {
return Err(PendingError::NotOpen {
pending_id: pending_id.to_string(),
state: order.state,
});
}
if now_ts >= order.expires_ts {
return Err(PendingError::Expired {
pending_id: pending_id.to_string(),
expires_ts: order.expires_ts,
now_ts,
});
}
Ok(())
}
pub(crate) fn apply_confirm(
&mut self,
pending_id: &str,
amount_cents: u64,
proof: &str,
now_ts: u64,
) -> Result<(), CoreError> {
self.check_confirm(pending_id, amount_cents, now_ts)
.map_err(CoreError::Pending)?;
let order = self
.orders
.get_mut(pending_id)
.expect("check_confirm 已确认单存在");
order.state = PendingState::Confirmed;
order.proof = Some(proof.to_string());
order.confirmed_ts = Some(now_ts);
Ok(())
}
pub(crate) fn apply_complete(&mut self, pending_id: &str) -> Result<(), CoreError> {
let order = self.orders.get_mut(pending_id).ok_or_else(|| {
CoreError::Pending(PendingError::UnknownPending {
pending_id: pending_id.to_string(),
})
})?;
if order.state != PendingState::Confirmed {
return Err(CoreError::Pending(PendingError::NotConfirmed {
pending_id: pending_id.to_string(),
state: order.state,
}));
}
order.state = PendingState::Completed;
Ok(())
}
pub(crate) fn apply_void(&mut self, pending_id: &str, now_ts: u64) -> Result<(), CoreError> {
let order = self.orders.get_mut(pending_id).ok_or_else(|| {
CoreError::Pending(PendingError::UnknownPending {
pending_id: pending_id.to_string(),
})
})?;
if order.state != PendingState::Open {
return Err(CoreError::Pending(PendingError::NotOpen {
pending_id: pending_id.to_string(),
state: order.state,
}));
}
if now_ts < order.expires_ts {
return Err(CoreError::Pending(PendingError::NotYetExpired {
pending_id: pending_id.to_string(),
expires_ts: order.expires_ts,
now_ts,
}));
}
order.state = PendingState::Voided;
Ok(())
}
}