Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ Update this file after every completed contract change, fix, or architectural de

## Completed

### Issue #7 — Vendor Approval Flow
- Added `VendorStatus` enum (`Pending`, `Approved`, `Suspended`, `Rejected`) to `types.rs`
- Replaced `active: bool` with `status: VendorStatus` in `VendorInfo`
- `register_vendor()` now sets `status = VendorStatus::Pending` instead of immediately active
- Added `approve_vendor()` (admin-only, requires Pending → Approved)
- Added `suspend_vendor()` (admin-only, any status → Suspended)
- `is_active()` returns `true` only for `Approved` vendors — automatically prevents unapproved/suspended vendors from receiving loans in `creditline-contract`
- Legacy functions (`activate_vendor`, `deactivate_vendor`, `set_vendor_status`) updated to map to new enum
- New error: `VendorNotPending = 10` in `vendor-registry-contract`
- Updated `publish_vendor_status` event to emit `VendorStatus` instead of `bool`
- All vendor-registry tests updated; 7 new tests added (approval flow, non-pending rejection, suspension, re-approval, reentrancy guards for approve/suspend)
- Creditline tests updated to approve vendors after registration
- No changes needed to `creditline-contract/src/lib.rs` — `validate_vendor()` already uses `is_active()` which now checks for `Approved`

### Workspace Cleanup
- Removed dead code: `lp-contract` (superseded by `liquidity-pool-contract`)
- Removed empty placeholder: `adapter-trustless-contract`
Expand Down Expand Up @@ -82,6 +96,13 @@ Update this file after every completed contract change, fix, or architectural de

- None currently.

## Recently Fixed

### Issue #7 — Follow-up: Missing `approve_vendor` in `RealIntegrationCtx::register_vendor`
- Discovered second `register_vendor` helper in `RealIntegrationCtx` (integration test struct, ~line 2390) that only called `register_vendor` without `approve_vendor`
- All integration tests using `RealIntegrationCtx` created loans with `Pending` vendors → `validate_vendor` → `is_active` returned `false` → `VendorNotActive` (#3)
- Added `self.vendor_registry.approve_vendor(&self.admin, vendor)` after registration in `RealIntegrationCtx::register_vendor`

---

## Next Up (In Order)
Expand Down
27 changes: 27 additions & 0 deletions contracts/creditline-contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ impl TestCtx {
}

/// Register a vendor in the vendor registry (idempotent - won't fail if already registered)
/// and approve it so loans can be created.
fn register_vendor(&self, vendor: &Address, name: &str) {
use soroban_sdk::{IntoVal, Symbol};
let vendor_name = SorobanString::from_str(&self.env, name);
Expand All @@ -165,6 +166,12 @@ impl TestCtx {
&Symbol::new(&self.env, "register_vendor"),
(&self.admin, vendor, vendor_name).into_val(&self.env),
);
// Approve the vendor so create_loan can proceed
let _ = self.env.try_invoke_contract::<(), soroban_sdk::Error>(
&self.vendor_registry_id,
&Symbol::new(&self.env, "approve_vendor"),
(&self.admin, vendor).into_val(&self.env),
);
}

/// Create a loan with sensible defaults: total=1000, guarantee=200, 1 installment.
Expand Down Expand Up @@ -887,6 +894,12 @@ fn test_mark_defaulted_success() {
&Symbol::new(&env, "register_vendor"),
(&admin, &vendor, vendor_name).into_val(&env),
);
// Approve the vendor
let _: Result<(), vendor_registry_contract::VendorRegistryError> = env.invoke_contract(
&vendor_registry_id,
&Symbol::new(&env, "approve_vendor"),
(&admin, &vendor).into_val(&env),
);

client.initialize(
&admin,
Expand Down Expand Up @@ -962,6 +975,12 @@ fn test_mark_defaulted_too_early_fails() {
&Symbol::new(&env, "register_vendor"),
(&admin, &vendor, vendor_name).into_val(&env),
);
// Approve the vendor
let _: Result<(), vendor_registry_contract::VendorRegistryError> = env.invoke_contract(
&vendor_registry_id,
&Symbol::new(&env, "approve_vendor"),
(&admin, &vendor).into_val(&env),
);

client.initialize(
&admin,
Expand Down Expand Up @@ -1151,6 +1170,12 @@ fn test_create_loan_rejected_when_reputation_below_threshold() {
&Symbol::new(&env, "register_vendor"),
(&admin, &vendor, vendor_name).into_val(&env),
);
// Approve the vendor
let _: Result<(), vendor_registry_contract::VendorRegistryError> = env.invoke_contract(
&vendor_registry_id,
&Symbol::new(&env, "approve_vendor"),
(&admin, &vendor).into_val(&env),
);

client.initialize(
&admin,
Expand Down Expand Up @@ -2366,6 +2391,8 @@ impl RealIntegrationCtx {
let vendor_name = SorobanString::from_str(&self.env, name);
self.vendor_registry
.register_vendor(&self.admin, vendor, &vendor_name);
self.vendor_registry
.approve_vendor(&self.admin, vendor);
}

fn single_installment(
Expand Down
1 change: 1 addition & 0 deletions contracts/vendor-registry-contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ pub enum Error {
Overflow = 7,
ReentrancyDetected = 8,
Underflow = 9,
VendorNotPending = 10,
}
5 changes: 3 additions & 2 deletions contracts/vendor-registry-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::types::VendorStatus;
use soroban_sdk::{Address, Env, String, Symbol};

pub fn publish_vendor_registered(env: &Env, vendor: Address, name: String) {
let topics = (Symbol::new(env, "MERCHTREG"), vendor);
env.events().publish(topics, name);
}

pub fn publish_vendor_status(env: &Env, vendor: Address, active: bool) {
pub fn publish_vendor_status(env: &Env, vendor: Address, status: VendorStatus) {
let topics = (Symbol::new(env, "MERCHTSTATUS"), vendor);
env.events().publish(topics, active);
env.events().publish(topics, status);
}

pub fn emit_contract_upgraded(env: &Env, old_version: u32, new_version: u32) {
Expand Down
74 changes: 61 additions & 13 deletions contracts/vendor-registry-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod tests;

use errors::Error;
use soroban_sdk::{contract, contractimpl, Address, Env, String};
use types::VendorInfo;
use types::{VendorInfo, VendorStatus};

// Export Error type for external use
pub use errors::Error as VendorRegistryError;
Expand Down Expand Up @@ -71,7 +71,7 @@ impl VendorRegistryContract {
let info = VendorInfo {
name: name.clone(),
registration_date: env.ledger().timestamp(),
active: true,
status: VendorStatus::Pending,
total_sales: 0,
};

Expand All @@ -84,7 +84,50 @@ impl VendorRegistryContract {
Ok(())
}

/// Deactivates an existing vendor
/// Approves a pending vendor so they can receive loans
pub fn approve_vendor(env: Env, admin: Address, vendor: Address) -> Result<(), Error> {
if !storage::has_admin(&env) {
return Err(Error::NotInitialized);
}

access::require_admin(&env, &admin)?;

Self::check_non_reentrant(&env)?;

let mut info = storage::get_vendor(&env, &vendor)?;
if info.status != VendorStatus::Pending {
return Err(Error::VendorNotPending);
}
info.status = VendorStatus::Approved;
storage::set_vendor(&env, &vendor, &info);
events::publish_vendor_status(&env, vendor, VendorStatus::Approved);

Self::exit_non_reentrant(&env);

Ok(())
}

/// Suspends an approved vendor, preventing new loans
pub fn suspend_vendor(env: Env, admin: Address, vendor: Address) -> Result<(), Error> {
if !storage::has_admin(&env) {
return Err(Error::NotInitialized);
}

access::require_admin(&env, &admin)?;

Self::check_non_reentrant(&env)?;

let mut info = storage::get_vendor(&env, &vendor)?;
info.status = VendorStatus::Suspended;
storage::set_vendor(&env, &vendor, &info);
events::publish_vendor_status(&env, vendor, VendorStatus::Suspended);

Self::exit_non_reentrant(&env);

Ok(())
}

/// Deactivates an existing vendor (legacy — maps to Suspended)
pub fn deactivate_vendor(env: Env, admin: Address, vendor: Address) -> Result<(), Error> {
if !storage::has_admin(&env) {
return Err(Error::NotInitialized);
Expand All @@ -95,16 +138,16 @@ impl VendorRegistryContract {
Self::check_non_reentrant(&env)?;

let mut info = storage::get_vendor(&env, &vendor)?;
info.active = false;
info.status = VendorStatus::Suspended;
storage::set_vendor(&env, &vendor, &info);
events::publish_vendor_status(&env, vendor, false);
events::publish_vendor_status(&env, vendor, VendorStatus::Suspended);

Self::exit_non_reentrant(&env);

Ok(())
}

/// Activates an existing vendor
/// Activates an existing vendor (legacy — maps to Approved)
pub fn activate_vendor(env: Env, admin: Address, vendor: Address) -> Result<(), Error> {
if !storage::has_admin(&env) {
return Err(Error::NotInitialized);
Expand All @@ -115,17 +158,17 @@ impl VendorRegistryContract {
Self::check_non_reentrant(&env)?;

let mut info = storage::get_vendor(&env, &vendor)?;
info.active = true;
info.status = VendorStatus::Approved;
storage::set_vendor(&env, &vendor, &info);
events::publish_vendor_status(&env, vendor, true);
events::publish_vendor_status(&env, vendor, VendorStatus::Approved);

Self::exit_non_reentrant(&env);

Ok(())
}

/// Sets a vendor's active status (admin only).
/// Pass `active = true` to activate, `active = false` to deactivate.
/// Sets a vendor's active status (admin only, legacy).
/// Pass `active = true` to approve, `active = false` to suspend.
pub fn set_vendor_status(
env: Env,
admin: Address,
Expand All @@ -141,9 +184,14 @@ impl VendorRegistryContract {
Self::check_non_reentrant(&env)?;

let mut info = storage::get_vendor(&env, &vendor)?;
info.active = active;
let new_status = if active {
VendorStatus::Approved
} else {
VendorStatus::Suspended
};
info.status = new_status.clone();
storage::set_vendor(&env, &vendor, &info);
events::publish_vendor_status(&env, vendor, active);
events::publish_vendor_status(&env, vendor, new_status);

Self::exit_non_reentrant(&env);

Expand All @@ -152,7 +200,7 @@ impl VendorRegistryContract {

pub fn is_active(env: Env, vendor: Address) -> bool {
storage::get_vendor(&env, &vendor)
.map(|info| info.active)
.map(|info| info.status == VendorStatus::Approved)
.unwrap_or(false)
}

Expand Down
Loading
Loading