From f8ff097d322e8d96b7456aec0d2f915c33cbbc7f Mon Sep 17 00:00:00 2001 From: Vibeofkd Date: Mon, 19 Oct 2026 00:24:02 +0100 Subject: [PATCH] issues resolved --- .../chainsetttle/src/test_new_milestone.rs | 172 ++++++ contracts/chainsetttle/src/test_observer.rs | 212 +++++++ .../src/test_partial_cancellation.rs | 554 ++++++++++++++++++ 3 files changed, 938 insertions(+) diff --git a/contracts/chainsetttle/src/test_new_milestone.rs b/contracts/chainsetttle/src/test_new_milestone.rs index 5ac93ff..347628e 100644 --- a/contracts/chainsetttle/src/test_new_milestone.rs +++ b/contracts/chainsetttle/src/test_new_milestone.rs @@ -2,6 +2,178 @@ extern crate std; +use super::*; +use crate::test_common::{build_milestones, default_options, setup, single_buyer_vec}; +use soroban_sdk::{testutils::Ledger as _, vec, Env, String, Symbol}; + +fn sid(env: &Env, s: &str) -> String { + String::from_str(env, s) +} + +fn ipfs(env: &Env) -> Symbol { + Symbol::new(env, "ipfs") +} + +// ============================================================ +// Issue #42 — Minimum shipment value +// ============================================================ + +#[test] +fn test_min_shipment_value_default_zero_allows_any_amount() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + // Default min = 0 (disabled) — small shipment must succeed + client.create_shipment( + &sid(&t.env, "min-default"), + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1, + &vec![ + &t.env, + Milestone { + name: sid(&t.env, "M"), + payment_percent: 100, + proof_hash: sid(&t.env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }, + ], + &default_options(&t.env), + ); + assert_eq!(client.get_min_shipment_value(), 0); +} + +#[test] +#[should_panic(expected = "MinShipmentValueNotMet")] +fn test_min_shipment_value_below_floor_panics() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + client.set_min_shipment_value(&t.buyer, &1_000); + // total_amount = 500 < 1_000 → must panic + client.create_shipment( + &sid(&t.env, "below-floor"), + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &500, + &vec![ + &t.env, + Milestone { + name: sid(&t.env, "M"), + payment_percent: 100, + proof_hash: sid(&t.env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }, + ], + &default_options(&t.env), + ); +} + +// ============================================================ +// Issue #299 — Shipment-level fee override +// ============================================================ + +#[test] +fn test_shipment_fee_override_cleared_reverts_to_global() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "fee-clear"); + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + client.set_shipment_fee_override(&t.buyer, &ship_id, &50u32); + client.clear_shipment_fee_override(&t.buyer, &ship_id); + assert_eq!(client.get_shipment_fee_override(&ship_id), None); +} + +// ============================================================ +// Issue #300 — Long-hold escrow rebate +// ============================================================ + +#[test] +fn test_long_hold_rebate_getter_after_set() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + client.set_long_hold_rebate(&t.buyer, &1000u32, &500u32); + assert_eq!(client.get_long_hold_rebate(), (1000u32, 500u32)); +} + +// ============================================================ +// Issue #298 — Governance timelock +// ============================================================ + +#[test] +#[should_panic(expected = "TimelockNotExpired")] +fn test_execute_param_change_before_delay_panics() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + client.set_timelock_duration(&t.buyer, &100u32); + let param = Symbol::new(&t.env, "max_shipment_value"); + client.propose_param_change(&t.buyer, ¶m, &500_000); + // Do NOT advance ledger — must panic + client.execute_param_change(&t.buyer, ¶m); +} + +#[test] +#[should_panic(expected = "NoPendingParamChange")] +fn test_cancel_param_change_removes_pending() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + client.set_timelock_duration(&t.buyer, &50u32); + let param = Symbol::new(&t.env, "max_shipment_value"); + client.propose_param_change(&t.buyer, ¶m, &1_000_000); + client.cancel_param_change(&t.buyer, ¶m); + // Advance past timelock and try to execute — must panic (no pending change) + t.env.ledger().with_mut(|l| l.sequence_number += 51); + client.execute_param_change(&t.buyer, ¶m); +} + +#[test] +fn test_timelock_zero_executes_immediately() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + // Default timelock = 0 → effective_ledger = current → can execute immediately + let param = Symbol::new(&t.env, "max_shipment_value"); + client.propose_param_change(&t.buyer, ¶m, &777_777); + client.execute_param_change(&t.buyer, ¶m); + assert_eq!(client.get_max_shipment_value(), 777_777); +} + +#[test] +fn test_circuit_breaker_limit_param_change() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let param = Symbol::new(&t.env, "circuit_breaker_limit"); + client.propose_param_change(&t.buyer, ¶m, &5_000_000); + client.execute_param_change(&t.buyer, ¶m); + // No panic = success; circuit breaker limit updated +} +#![cfg(test)] + +extern crate std; + use super::*; use crate::test_common::{create_standard_shipment, setup, submit_proof_for_milestone, TestSetup}; use soroban_sdk::testutils::{Address as _, Events as _}; diff --git a/contracts/chainsetttle/src/test_observer.rs b/contracts/chainsetttle/src/test_observer.rs index 5000ad3..c1d2c1f 100644 --- a/contracts/chainsetttle/src/test_observer.rs +++ b/contracts/chainsetttle/src/test_observer.rs @@ -2,6 +2,218 @@ extern crate std; +use super::*; +use crate::test_common::{build_milestones, default_options, setup, single_buyer_vec}; +use soroban_sdk::{vec, BytesN, Env, String, Symbol}; + +fn sid(env: &Env, id: &str) -> String { + String::from_str(env, id) +} + +fn ipfs(env: &Env) -> Symbol { + Symbol::new(env, "ipfs") +} + +// ============================================================ +// #113 – Fee tier tests +// ============================================================ + +#[test] +fn test_fee_tier_no_tiers_uses_default_fee() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + client.set_fee_config(&t.buyer, &100u32, &t.treasury); + // No tiers: get_fee_tier returns FeeConfig.fee_bps + assert_eq!(client.get_fee_tier(&t.buyer), 100); +} + +// ============================================================ +// #112 – Invoice hash tests +// ============================================================ + +#[test] +fn test_no_invoice_hash_returns_none() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "nohash"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + + assert!(client.get_invoice_hash(&ship_id, &0u32).is_none()); +} + +#[test] +#[should_panic(expected = "invoice hash already set and is immutable")] +fn test_invoice_hash_immutable_after_first_submission() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "invimm"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + client.submit_proof( + &t.supplier, + &ship_id, + &0u32, + &sid(&t.env, "h0"), + &ipfs(&t.env), + ); + let hash = BytesN::from_array(&t.env, &[0xAAu8; 32]); + client.attach_invoice_hash(&t.supplier, &ship_id, &0u32, &hash); + // Second attach must panic + client.attach_invoice_hash(&t.supplier, &ship_id, &0u32, &hash); +} + +// ============================================================ +// #111 – Amendment log tests +// ============================================================ + +// Helper: milestones where index 0 can validly change percent (100% single milestone) +fn single_milestone(env: &Env, name: &str) -> soroban_sdk::Vec { + vec![ + env, + Milestone { + name: String::from_str(env, name), + payment_percent: 100, + proof_hash: String::from_str(env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }, + ] +} + +#[test] +fn test_amendment_log_empty_on_unilateral_proposal() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "amlog2"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + + // Only buyer proposes → no acceptance + client.propose_amendment(&t.buyer, &ship_id, &0u32, &25u32, &sid(&t.env, "V2")); + assert_eq!(client.get_amendment_log(&ship_id, &0u32).len(), 0); +} + +// ============================================================ +// #110 – Extension request tests +// ============================================================ + +#[test] +fn test_extension_request_count_includes_approved_requests() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "extapproved"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + client.set_max_extension_requests(&t.buyer, &2u32); + + client.request_extension(&t.supplier, &ship_id, &0u32, &100u32); + client.approve_extension(&t.buyer, &ship_id, &0u32); + client.request_extension(&t.supplier, &ship_id, &0u32, &200u32); + client.approve_extension(&t.buyer, &ship_id, &0u32); + + assert!(client.get_milestone_deadline(&ship_id, &0u32) > 0); +} + +#[test] +#[should_panic(expected = "max extension requests reached")] +fn test_extension_request_over_limit_rejected() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "extlimit"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + client.set_max_extension_requests(&t.buyer, &1u32); + + client.request_extension(&t.supplier, &ship_id, &0u32, &100u32); + client.deny_extension(&t.buyer, &ship_id, &0u32); + + // second request should be rejected because the milestone has already reached cap + client.request_extension(&t.supplier, &ship_id, &0u32, &200u32); +} + +#[test] +#[should_panic(expected = "extension request already pending")] +fn test_double_extension_request_rejected() { + let t = setup(); + let client = ChainSettleContractClient::new(&t.env, &t.contract_id); + let ship_id = sid(&t.env, "extdbl"); + + client.create_shipment( + &ship_id, + &single_buyer_vec(&t.env, &t.buyer), + &t.supplier, + &t.logistics, + &t.arbiter, + &t.token_id, + &1_000_000, + &build_milestones(&t.env), + &default_options(&t.env), + ); + + client.request_extension(&t.supplier, &ship_id, &0u32, &100u32); + // Second request while first is pending must panic + client.request_extension(&t.supplier, &ship_id, &0u32, &200u32); +} + +#![cfg(test)] + +extern crate std; + use super::*; use soroban_sdk::{testutils::Address as _, Address, String, vec}; use crate::test_common::*; diff --git a/contracts/chainsetttle/src/test_partial_cancellation.rs b/contracts/chainsetttle/src/test_partial_cancellation.rs index 7e2a37b..f470d14 100644 --- a/contracts/chainsetttle/src/test_partial_cancellation.rs +++ b/contracts/chainsetttle/src/test_partial_cancellation.rs @@ -7,6 +7,560 @@ // `total_amount - released_amount - total_advanced_amount` to the buyer, leaving // funds already paid out to the supplier untouched. These tests pin down that // behaviour for the zero/partial/fully-confirmed scenarios called out in the issue. +#![cfg(test)] + +use soroban_sdk::{testutils::Address as _, token, Address, Env, String, Symbol, Vec}; + +use crate::{ + ChainSettleContract, ChainSettleContractClient, Milestone, MilestoneMode, MilestoneStatus, + Resolution, ShipmentOptions, ShipmentStatus, +}; + +// ============================================================ +// TEST SETUP & HELPERS +// ============================================================ + +struct OracleTestSetup { + env: Env, + contract_id: Address, + buyer: Address, + supplier: Address, + logistics: Address, + arbiter: Address, + admin: Address, + token_id: Address, +} + +fn oracle_setup() -> OracleTestSetup { + let env = Env::default(); + env.mock_all_auths(); + + let buyer = Address::generate(&env); + let supplier = Address::generate(&env); + let logistics = Address::generate(&env); + let arbiter = Address::generate(&env); + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + + // Deploy mock token + let token_id = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_client = token::StellarAssetClient::new(&env, &token_id); + + // Mint tokens + token_client.mint(&buyer, &(100_000 * 10i128.pow(18))); + token_client.mint(&supplier, &(10_000 * 10i128.pow(18))); + + // Deploy ChainSettle contract + let contract_id = env.register(ChainSettleContract, ()); + + // Initialize ChainSettle + ChainSettleContractClient::new(&env, &contract_id).init(&admin); + + OracleTestSetup { + env, + contract_id, + buyer, + supplier, + logistics, + arbiter, + admin, + token_id, + } +} + +fn build_single_milestone(env: &Env) -> Vec { + let mut v = Vec::new(&env); + v.push_back(Milestone { + name: String::from_str(&env, "Delivery"), + payment_percent: 100, + proof_hash: String::from_str(&env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }); + v +} + +fn build_three_milestones(env: &Env) -> Vec { + let mut v = Vec::new(&env); + v.push_back(Milestone { + name: String::from_str(&env, "Dispatch"), + payment_percent: 33, + proof_hash: String::from_str(&env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }); + v.push_back(Milestone { + name: String::from_str(&env, "Transit"), + payment_percent: 33, + proof_hash: String::from_str(&env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }); + v.push_back(Milestone { + name: String::from_str(&env, "Delivery"), + payment_percent: 34, + proof_hash: String::from_str(&env, ""), + status: MilestoneStatus::Pending, + release_after_ledger: 0, + proof_submitted_ledger: None, + dispute_opened_ledger: None, + deadline_ledger: 0, + penalty_bps_per_ledger: 0, + }); + v +} + +fn default_options(env: &Env) -> ShipmentOptions { + ShipmentOptions { + response_deadline: 0, + penalty_bps: 0, + milestone_mode: MilestoneMode::Parallel, + holdback_ledgers: 0, + dispute_cooldown_ledgers: 0, + late_penalty_bps_per_ledger: 0, + auto_confirm_ledgers: 0, + dispute_bond_amount: 0, + dispute_bond_bps: 0, + arbiter_fee_bps: 0, + logistics_fee_bps: 0, + supplier_collateral: 0, + expires_at_ledger: None, + + metadata_hash: None, + referrer: None, + buyer_cancel_fee_bps: 0, + early_bonus_pool: 0, + review_window_ledgers: None, + + milestone_splits: Vec::new(env), + deadlines: Vec::new(env), + dispute_timeout_seconds: 0, + default_resolution: Resolution::Buyer, + backup_arbiter: None, + confirmation_cooldown_ledgers: None, + arbiter_panel: Vec::new(env), + jurisdiction: None, + grace_period_ledgers: 0, + quality_grades: Vec::new(env), + milestone_quantities: Vec::new(env), + retainage_bps: 0, + warranty_bps: 0, + warranty_ledgers: 0, + inspector: None, + inspected_milestones: Vec::new(env), + proof_submitters: Vec::new(env), + require_dual_attestation: false, + fund_from_vault: false, + milestone_suppliers: Vec::new(env), + refund_recipient: None, + } +} + +// ============================================================ +// TESTS: ORACLE INTEGRATION PATTERNS +// ============================================================ + +#[test] +fn test_oracle_pattern_proof_verified_auto_confirm() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_single_milestone(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-001"); + let total_amount = 1_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + // Verify shipment created + let shipment = client.get_shipment(&shipment_id); + assert_eq!(shipment.status, ShipmentStatus::Active); + assert_eq!(shipment.total_amount, total_amount); + assert_eq!(shipment.milestones.len(), 1); + + // Simulate oracle verification: submit proof + let proof_hash = String::from_str(&setup.env, "QmVerifiedProof123"); + client.submit_proof( + &setup.supplier, + &shipment_id, + &0u32, + &proof_hash, + &Symbol::new(&setup.env, "ipfs"), + ); + + // Verify milestone in ProofSubmitted state + let shipment = client.get_shipment(&shipment_id); + let milestone = shipment.milestones.get(0).unwrap(); + assert_eq!(milestone.status, MilestoneStatus::ProofSubmitted); + assert_eq!(milestone.proof_hash, proof_hash); + + // Buyer confirms (oracle pre-verified, buyer trusts oracle) + let mut indices = Vec::new(&setup.env); + indices.push_back(0u32); + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &indices); + + // Verify milestone confirmed and payment released + let shipment = client.get_shipment(&shipment_id); + assert_eq!(shipment.status, ShipmentStatus::Completed); + let milestone = shipment.milestones.get(0).unwrap(); + assert_eq!(milestone.status, MilestoneStatus::Confirmed); + + let token_client = token::Client::new(&setup.env, &setup.token_id); + let supplier_final = token_client.balance(&setup.supplier); + assert!(supplier_final > 0, "Supplier received payment"); +} + +#[test] +fn test_oracle_pattern_proof_rejected_raises_dispute() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_single_milestone(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-rejected"); + let total_amount = 1_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + // Submit proof (not verified by oracle) + let proof_hash = String::from_str(&setup.env, "QmRejectedProof"); + client.submit_proof( + &setup.supplier, + &shipment_id, + &0u32, + &proof_hash, + &Symbol::new(&setup.env, "ipfs"), + ); + + // Milestone is now ProofSubmitted + let shipment = client.get_shipment(&shipment_id); + assert_eq!( + shipment.milestones.get(0).unwrap().status, + MilestoneStatus::ProofSubmitted + ); + + // Buyer disputes (oracle rejected the proof) + client.raise_dispute(&setup.buyer, &shipment_id, &0u32); + + // Verify milestone is disputed + let shipment = client.get_shipment(&shipment_id); + assert_eq!( + shipment.milestones.get(0).unwrap().status, + MilestoneStatus::Disputed + ); +} + +#[test] +fn test_oracle_pattern_multiple_proofs_cross_contract() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_three_milestones(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-multi"); + let total_amount = 3_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + // Submit all proofs sequentially + for i in 0..3u32 { + let proof_hash = String::from_str(&setup.env, "QmProof"); + client.submit_proof( + &setup.supplier, + &shipment_id, + &i, + &proof_hash, + &Symbol::new(&setup.env, "ipfs"), + ); + } + + // Verify all milestones are ProofSubmitted + let shipment = client.get_shipment(&shipment_id); + for i in 0..3 { + assert_eq!( + shipment.milestones.get(i).unwrap().status, + MilestoneStatus::ProofSubmitted + ); + } + + // Buyer confirms all milestones (all oracle-verified) + let mut indices = Vec::new(&setup.env); + indices.push_back(0u32); + indices.push_back(1u32); + indices.push_back(2u32); + + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &indices); + + // Verify shipment completed + let shipment = client.get_shipment(&shipment_id); + assert_eq!(shipment.status, ShipmentStatus::Completed); + assert_eq!(shipment.released_amount, total_amount); +} + +#[test] +fn test_oracle_pattern_partial_approval_mixed_proofs() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_three_milestones(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-partial"); + let total_amount = 3_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + // Submit proofs for all 3 milestones + for i in 0..3u32 { + let proof_hash = String::from_str(&setup.env, "QmProof"); + client.submit_proof( + &setup.supplier, + &shipment_id, + &i, + &proof_hash, + &Symbol::new(&setup.env, "ipfs"), + ); + } + + // Buyer confirms only milestones 0 and 2 (oracle rejected milestone 1) + let mut indices = Vec::new(&setup.env); + indices.push_back(0u32); + indices.push_back(2u32); + + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &indices); + + // Verify partial confirmation + let shipment = client.get_shipment(&shipment_id); + assert_eq!( + shipment.milestones.get(0).unwrap().status, + MilestoneStatus::Confirmed + ); + assert_eq!( + shipment.milestones.get(1).unwrap().status, + MilestoneStatus::ProofSubmitted + ); + assert_eq!( + shipment.milestones.get(2).unwrap().status, + MilestoneStatus::Confirmed + ); + assert_eq!(shipment.status, ShipmentStatus::Active); +} + +#[test] +fn test_oracle_pattern_dispute_after_rejection() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_three_milestones(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-dispute"); + let total_amount = 3_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + // Submit proof for milestone 0 + client.submit_proof( + &setup.supplier, + &shipment_id, + &0u32, + &String::from_str(&setup.env, "QmProof0"), + &Symbol::new(&setup.env, "ipfs"), + ); + + // Buyer raises dispute (oracle rejected) + client.raise_dispute(&setup.buyer, &shipment_id, &0u32); + + // Verify dispute status + let shipment = client.get_shipment(&shipment_id); + assert_eq!( + shipment.milestones.get(0).unwrap().status, + MilestoneStatus::Disputed + ); + + // Arbiter resolves: approves the proof despite oracle rejection + client.resolve_dispute(&setup.arbiter, &shipment_id, &0u32, &true, &None); + + // Verify dispute resolved and milestone confirmed + let shipment = client.get_shipment(&shipment_id); + assert_eq!( + shipment.milestones.get(0).unwrap().status, + MilestoneStatus::Resolved + ); +} + +#[test] +fn test_oracle_pattern_shipment_lifecycle_oracle_verified() { + let setup = oracle_setup(); + let client = ChainSettleContractClient::new(&setup.env, &setup.contract_id); + + let milestones = build_three_milestones(&setup.env); + let shipment_id = String::from_str(&setup.env, "shipment-oracle-full-lifecycle"); + let total_amount = 3_000 * 10i128.pow(18); + + let mut buyers = Vec::new(&setup.env); + buyers.push_back(setup.buyer.clone()); + + let options = default_options(&setup.env); + + // Create shipment + client.create_shipment( + &shipment_id, + &buyers, + &setup.supplier, + &setup.logistics, + &setup.arbiter, + &setup.token_id, + &total_amount, + &milestones, + &options, + ); + + let token_client = token::Client::new(&setup.env, &setup.token_id); + + // Milestone 0: Dispatch + client.submit_proof( + &setup.supplier, + &shipment_id, + &0u32, + &String::from_str(&setup.env, "QmDispatch"), + &Symbol::new(&setup.env, "ipfs"), + ); + + let mut idx = Vec::new(&setup.env); + idx.push_back(0u32); + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &idx); + + let balance = token_client.balance(&setup.supplier); + assert!( + balance > 0, + "Supplier should receive payment for milestone 0" + ); + + // Milestone 1: Transit + client.submit_proof( + &setup.logistics, + &shipment_id, + &1u32, + &String::from_str(&setup.env, "QmTransit"), + &Symbol::new(&setup.env, "ipfs"), + ); + + let mut idx = Vec::new(&setup.env); + idx.push_back(1u32); + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &idx); + + // Milestone 2: Delivery + client.submit_proof( + &setup.supplier, + &shipment_id, + &2u32, + &String::from_str(&setup.env, "QmDelivery"), + &Symbol::new(&setup.env, "ipfs"), + ); + + let mut idx = Vec::new(&setup.env); + idx.push_back(2u32); + client.batch_confirm_milestones(&setup.buyer, &shipment_id, &idx); + + // Verify complete lifecycle + let shipment = client.get_shipment(&shipment_id); + assert_eq!(shipment.status, ShipmentStatus::Completed); + assert_eq!(shipment.released_amount, total_amount); + for i in 0..3 { + assert_eq!( + shipment.milestones.get(i).unwrap().status, + MilestoneStatus::Confirmed + ); + } +} extern crate std;