|
| 1 | +use crate::admin::AdminStorage; |
| 2 | +use crate::errors::QuickLendXError; |
| 3 | +use crate::invoice::{Dispute, DisputeStatus, InvoiceStatus, InvoiceStorage}; |
| 4 | +use crate::protocol_limits::{ |
| 5 | + MAX_DISPUTE_EVIDENCE_LENGTH, MAX_DISPUTE_REASON_LENGTH, MAX_DISPUTE_RESOLUTION_LENGTH, |
| 6 | +}; |
| 7 | +use soroban_sdk::{symbol_short, Address, BytesN, Env, String, Vec}; |
| 8 | + |
| 9 | +fn dispute_index_key() -> soroban_sdk::Symbol { |
| 10 | + symbol_short!("dispute") |
| 11 | +} |
| 12 | + |
| 13 | +fn get_dispute_index(env: &Env) -> Vec<BytesN<32>> { |
| 14 | + env.storage() |
| 15 | + .instance() |
| 16 | + .get(&dispute_index_key()) |
| 17 | + .unwrap_or_else(|| Vec::new(env)) |
| 18 | +} |
| 19 | + |
| 20 | +fn add_to_dispute_index(env: &Env, invoice_id: &BytesN<32>) { |
| 21 | + let mut ids = get_dispute_index(env); |
| 22 | + if !ids.iter().any(|id| id == *invoice_id) { |
| 23 | + ids.push_back(invoice_id.clone()); |
| 24 | + env.storage().instance().set(&dispute_index_key(), &ids); |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +fn zero_address(env: &Env) -> Address { |
| 29 | + Address::from_str(env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF") |
| 30 | +} |
| 31 | + |
| 32 | +#[allow(dead_code)] |
| 33 | +pub fn create_dispute( |
| 34 | + env: &Env, |
| 35 | + invoice_id: &BytesN<32>, |
| 36 | + creator: &Address, |
| 37 | + reason: &String, |
| 38 | + evidence: &String, |
| 39 | +) -> Result<(), QuickLendXError> { |
| 40 | + creator.require_auth(); |
| 41 | + |
| 42 | + let mut invoice = |
| 43 | + InvoiceStorage::get_invoice(env, invoice_id).ok_or(QuickLendXError::InvoiceNotFound)?; |
| 44 | + |
| 45 | + if invoice.dispute_status != DisputeStatus::None { |
| 46 | + return Err(QuickLendXError::DisputeAlreadyExists); |
| 47 | + } |
| 48 | + |
| 49 | + match invoice.status { |
| 50 | + InvoiceStatus::Pending |
| 51 | + | InvoiceStatus::Verified |
| 52 | + | InvoiceStatus::Funded |
| 53 | + | InvoiceStatus::Paid => {} |
| 54 | + _ => return Err(QuickLendXError::InvalidStatus), |
| 55 | + } |
| 56 | + |
| 57 | + let is_business = *creator == invoice.business; |
| 58 | + let is_investor = invoice |
| 59 | + .investor |
| 60 | + .as_ref() |
| 61 | + .map_or(false, |investor| *creator == *investor); |
| 62 | + if !is_business && !is_investor { |
| 63 | + return Err(QuickLendXError::DisputeNotAuthorized); |
| 64 | + } |
| 65 | + |
| 66 | + if reason.len() == 0 || reason.len() > MAX_DISPUTE_REASON_LENGTH { |
| 67 | + return Err(QuickLendXError::InvalidDisputeReason); |
| 68 | + } |
| 69 | + if evidence.len() == 0 || evidence.len() > MAX_DISPUTE_EVIDENCE_LENGTH { |
| 70 | + return Err(QuickLendXError::InvalidDisputeEvidence); |
| 71 | + } |
| 72 | + |
| 73 | + invoice.dispute_status = DisputeStatus::Disputed; |
| 74 | + invoice.dispute = Dispute { |
| 75 | + created_by: creator.clone(), |
| 76 | + created_at: env.ledger().timestamp(), |
| 77 | + reason: reason.clone(), |
| 78 | + evidence: evidence.clone(), |
| 79 | + resolution: String::from_str(env, ""), |
| 80 | + resolved_by: zero_address(env), |
| 81 | + resolved_at: 0, |
| 82 | + }; |
| 83 | + |
| 84 | + InvoiceStorage::update_invoice(env, &invoice); |
| 85 | + add_to_dispute_index(env, invoice_id); |
| 86 | + Ok(()) |
| 87 | +} |
| 88 | + |
| 89 | +#[allow(dead_code)] |
| 90 | +pub fn put_dispute_under_review( |
| 91 | + env: &Env, |
| 92 | + admin: &Address, |
| 93 | + invoice_id: &BytesN<32>, |
| 94 | +) -> Result<(), QuickLendXError> { |
| 95 | + AdminStorage::require_admin(env, admin)?; |
| 96 | + let mut invoice = |
| 97 | + InvoiceStorage::get_invoice(env, invoice_id).ok_or(QuickLendXError::InvoiceNotFound)?; |
| 98 | + |
| 99 | + if invoice.dispute_status == DisputeStatus::None { |
| 100 | + return Err(QuickLendXError::DisputeNotFound); |
| 101 | + } |
| 102 | + if invoice.dispute_status != DisputeStatus::Disputed { |
| 103 | + return Err(QuickLendXError::InvalidStatus); |
| 104 | + } |
| 105 | + |
| 106 | + invoice.dispute_status = DisputeStatus::UnderReview; |
| 107 | + InvoiceStorage::update_invoice(env, &invoice); |
| 108 | + Ok(()) |
| 109 | +} |
| 110 | + |
| 111 | +#[allow(dead_code)] |
| 112 | +pub fn resolve_dispute( |
| 113 | + env: &Env, |
| 114 | + admin: &Address, |
| 115 | + invoice_id: &BytesN<32>, |
| 116 | + resolution: &String, |
| 117 | +) -> Result<(), QuickLendXError> { |
| 118 | + AdminStorage::require_admin(env, admin)?; |
| 119 | + let mut invoice = |
| 120 | + InvoiceStorage::get_invoice(env, invoice_id).ok_or(QuickLendXError::InvoiceNotFound)?; |
| 121 | + |
| 122 | + if invoice.dispute_status == DisputeStatus::None { |
| 123 | + return Err(QuickLendXError::DisputeNotFound); |
| 124 | + } |
| 125 | + if invoice.dispute_status != DisputeStatus::UnderReview { |
| 126 | + return Err(QuickLendXError::DisputeNotUnderReview); |
| 127 | + } |
| 128 | + if resolution.len() == 0 || resolution.len() > MAX_DISPUTE_RESOLUTION_LENGTH { |
| 129 | + return Err(QuickLendXError::InvalidDisputeReason); |
| 130 | + } |
| 131 | + |
| 132 | + invoice.dispute_status = DisputeStatus::Resolved; |
| 133 | + invoice.dispute.resolution = resolution.clone(); |
| 134 | + invoice.dispute.resolved_by = admin.clone(); |
| 135 | + invoice.dispute.resolved_at = env.ledger().timestamp(); |
| 136 | + InvoiceStorage::update_invoice(env, &invoice); |
| 137 | + Ok(()) |
| 138 | +} |
| 139 | + |
| 140 | +#[allow(dead_code)] |
| 141 | +pub fn get_dispute_details(env: &Env, invoice_id: &BytesN<32>) -> Option<Dispute> { |
| 142 | + let invoice = InvoiceStorage::get_invoice(env, invoice_id)?; |
| 143 | + if invoice.dispute_status == DisputeStatus::None { |
| 144 | + None |
| 145 | + } else { |
| 146 | + Some(invoice.dispute) |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +#[allow(dead_code)] |
| 151 | +pub fn get_invoices_with_disputes(env: &Env) -> Vec<BytesN<32>> { |
| 152 | + get_dispute_index(env) |
| 153 | +} |
| 154 | + |
| 155 | +#[allow(dead_code)] |
| 156 | +pub fn get_invoices_by_dispute_status( |
| 157 | + env: &Env, |
| 158 | + status: &DisputeStatus, |
| 159 | +) -> Vec<BytesN<32>> { |
| 160 | + let mut result = Vec::new(env); |
| 161 | + for invoice_id in get_dispute_index(env).iter() { |
| 162 | + if let Some(invoice) = InvoiceStorage::get_invoice(env, &invoice_id) { |
| 163 | + if invoice.dispute_status == *status { |
| 164 | + result.push_back(invoice_id); |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + result |
| 169 | +} |
1 | 170 | //! Invoice disputes are represented on [`crate::invoice::Invoice`] and handled by contract |
2 | 171 | //! entry points in `lib.rs`. This module is reserved for future dispute-specific helpers. |
0 commit comments