Skip to content
Open
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
42 changes: 40 additions & 2 deletions contracts/shade/src/components/invoice.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
pub fn finalize_invoice(env: &Env, merchant_address: &Address, invoice_id: u64) {
merchant_address.require_auth();
let mut invoice: Invoice = env
.storage()
.persistent()
.get(&DataKey::Invoice(invoice_id))
.unwrap_or_else(|| panic_with_error!(env, ContractError::InvoiceNotFound));
if invoice.merchant_id != env
.storage()
.persistent()
.get::<_, u64>(&DataKey::MerchantId(merchant_address.clone()))
.unwrap()
{
panic_with_error!(env, ContractError::NotAuthorized);
}
if invoice.status != InvoiceStatus::Draft {
panic_with_error!(env, ContractError::InvalidInvoiceStatus);
}
invoice.status = InvoiceStatus::Pending;
env.storage().persistent().set(&DataKey::Invoice(invoice_id), &invoice);

events::publish_invoice_finalized_event(
env,
invoice_id,
merchant_address.clone(),
env.ledger().timestamp(),
);
}
use crate::components::{access_control, admin, merchant, signature_util};
use crate::errors::ContractError;
use crate::events;
Expand Down Expand Up @@ -42,7 +70,7 @@ pub fn create_invoice(
description: description.clone(),
amount,
token: token.clone(),
status: InvoiceStatus::Pending,
status: InvoiceStatus::Draft,
merchant_id,
payer: None,
date_created: env.ledger().timestamp(),
Expand Down Expand Up @@ -130,7 +158,7 @@ pub fn create_invoice_signed(
description: description.clone(),
amount,
token: token.clone(),
status: InvoiceStatus::Pending,
status: InvoiceStatus::Draft,
merchant_id,
payer: None,
date_created: env.ledger().timestamp(),
Expand Down Expand Up @@ -211,6 +239,10 @@ pub fn get_invoices(env: &Env, filter: InvoiceFilter) -> Vec<Invoice> {
.get::<_, Invoice>(&DataKey::Invoice(i))
{
let mut matches = true;
// Hide Draft invoices unless explicitly filtering for Draft
if filter.status.is_none() && invoice.status == InvoiceStatus::Draft {
continue;
}
if let Some(status) = filter.status {
if invoice.status as u32 != status {
matches = false;
Expand Down Expand Up @@ -324,6 +356,9 @@ pub fn refund_invoice_partial(env: &Env, invoice_id: u64, amount: i128) {

pub fn pay_invoice(env: &Env, payer: &Address, invoice_id: u64) -> i128 {
let invoice = get_invoice(env, invoice_id);
if invoice.status == InvoiceStatus::Draft {
panic_with_error!(env, ContractError::InvalidInvoiceStatus);
}
if invoice.status != InvoiceStatus::Pending && invoice.status != InvoiceStatus::PartiallyPaid {
panic_with_error!(env, ContractError::InvalidInvoiceStatus);
}
Expand All @@ -342,6 +377,9 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount:
}

let mut invoice = get_invoice(env, invoice_id);
if invoice.status == InvoiceStatus::Draft {
panic_with_error!(env, ContractError::InvalidInvoiceStatus);
}

if let Some(expires_at) = invoice.expires_at {
if env.ledger().timestamp() >= expires_at {
Expand Down
21 changes: 21 additions & 0 deletions contracts/shade/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,27 @@ pub fn publish_invoice_created_event(
.publish(env);
}

#[contractevent]
pub struct InvoiceFinalizedEvent {
pub invoice_id: u64,
pub merchant: Address,
pub timestamp: u64,
}

pub fn publish_invoice_finalized_event(
env: &Env,
invoice_id: u64,
merchant: Address,
timestamp: u64,
) {
InvoiceFinalizedEvent {
invoice_id,
merchant,
timestamp,
}
.publish(env);
}

#[contractevent]
pub struct InvoiceRefundedEvent {
pub invoice_id: u64,
Expand Down
1 change: 1 addition & 0 deletions contracts/shade/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub trait ShadeTrait {
signature: BytesN<64>,
) -> u64;
fn get_invoice(env: Env, invoice_id: u64) -> Invoice;
fn finalize_invoice(env: Env, merchant: Address, invoice_id: u64);
fn refund_invoice(env: Env, merchant: Address, invoice_id: u64);
fn set_merchant_key(env: Env, merchant: Address, key: BytesN<32>);
fn get_merchant_key(env: Env, merchant: Address) -> BytesN<32>;
Expand Down
5 changes: 5 additions & 0 deletions contracts/shade/src/shade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ impl ShadeTrait for Shade {
invoice_component::get_invoice(&env, invoice_id)
}

fn finalize_invoice(env: Env, merchant: Address, invoice_id: u64) {
pausable_component::assert_not_paused(&env);
invoice_component::finalize_invoice(&env, &merchant, invoice_id);
}

fn refund_invoice(env: Env, merchant: Address, invoice_id: u64) {
pausable_component::assert_not_paused(&env);
invoice_component::refund_invoice(&env, &merchant, invoice_id);
Expand Down
90 changes: 77 additions & 13 deletions contracts/shade/src/tests/test_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,75 @@ fn test_create_and_get_invoice_success() {
let invoice_id = client.create_invoice(&merchant, &description, &amount, &token, &None);
assert_eq!(invoice_id, 1);

let invoice = client.get_invoice(&invoice_id);
assert_eq!(invoice.status, InvoiceStatus::Draft);

client.finalize_invoice(&merchant, &invoice_id);

assert_latest_invoice_event(&env, &contract_id, invoice_id, &merchant, amount, &token);

let invoice = client.get_invoice(&invoice_id);

assert_eq!(invoice.id, 1);
// Merchant ID is 1 because it's the first merchant registered in setup_test?
// No, setup_test doesn't register merchants. The register_merchant call on line 89 does.
assert_eq!(invoice.merchant_id, 1);
assert_eq!(invoice.amount, amount);
assert_eq!(invoice.token, token);
assert_eq!(invoice.description, description);
assert_eq!(invoice.status, InvoiceStatus::Pending);
}

#[test]
fn test_get_invoices_excludes_draft() {
let (env, client, _contract_id, _admin) = setup_test();

let merchant = Address::generate(&env);
client.register_merchant(&merchant);

let token = Address::generate(&env);
let description = String::from_str(&env, "Test Invoice");
let amount: i128 = 1000;

client.create_invoice(&merchant, &description, &amount, &token, &None);

let filter = crate::types::InvoiceFilter {
status: None,
merchant: None,
min_amount: None,
max_amount: None,
start_date: None,
end_date: None,
};

let invoices = client.get_invoices(&filter);
assert_eq!(invoices.len(), 0);

client.finalize_invoice(&merchant, &1);
let invoices = client.get_invoices(&filter);
assert_eq!(invoices.len(), 1);
}

#[should_panic(expected = "HostError: Error(Contract, #16)")]
#[test]
fn test_pay_draft_invoice_fails() {
let (env, client, _contract_id, _admin, token) = setup_test_with_payment();

let merchant = Address::generate(&env);
client.register_merchant(&merchant);
let merchant_account = Address::generate(&env);
client.set_merchant_account(&merchant, &merchant_account);

let description = String::from_str(&env, "Test Invoice");
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

let customer = Address::generate(&env);
let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token);
token_client.mint(&customer, &1000);

client.pay_invoice(&customer, &invoice_id);
}

#[test]
fn test_create_multiple_invoices() {
let (env, client, _contract_id, _admin) = setup_test();
Expand Down Expand Up @@ -200,6 +257,7 @@ fn test_refund_invoice_success_within_window() {
token_admin.mint(&merchant_account_id, &amount);

env.ledger().set_timestamp(1_000);
client.finalize_invoice(&merchant, &invoice_id);
mark_invoice_paid(
&env,
&shade_contract_id,
Expand Down Expand Up @@ -244,6 +302,7 @@ fn test_refund_invoice_fails_after_refund_window() {
merchant_account.initialize(&merchant, &shade_contract_id, &1_u64);

env.ledger().set_timestamp(604_801);
client.finalize_invoice(&merchant, &invoice_id);
mark_invoice_paid(
&env,
&shade_contract_id,
Expand Down Expand Up @@ -271,9 +330,16 @@ fn test_void_invoice_success() {
let description = String::from_str(&env, "Test Invoice");
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

// Verify invoice is Pending
// Verify invoice is Draft
let invoice_before = client.get_invoice(&invoice_id);
assert_eq!(invoice_before.status, InvoiceStatus::Pending);
assert_eq!(invoice_before.status, InvoiceStatus::Draft);

// Finalize
client.finalize_invoice(&merchant, &invoice_id);

// Verify invoice is Pending before voiding
let invoice_before_pending = client.get_invoice(&invoice_id);
assert_eq!(invoice_before_pending.status, InvoiceStatus::Pending);

// Void the invoice
client.void_invoice(&merchant, &invoice_id);
Expand Down Expand Up @@ -307,17 +373,7 @@ fn test_refund_invoice_fails_for_non_owner() {
merchant_account.initialize(&merchant, &shade_contract_id, &1_u64);

env.ledger().set_timestamp(100);
mark_invoice_paid(
&env,
&shade_contract_id,
&merchant,
invoice_id,
&payer,
90,
&merchant_account_id,
&client,
);

client.finalize_invoice(&merchant, &invoice_id);
client.refund_invoice(&other_merchant, &invoice_id);
}

Expand All @@ -336,6 +392,7 @@ fn test_void_invoice_non_owner() {
// Try to void with different merchant (should panic with NotAuthorized)
let other_merchant = Address::generate(&env);
client.register_merchant(&other_merchant);
client.finalize_invoice(&merchant, &invoice_id);
client.void_invoice(&other_merchant, &invoice_id);
}

Expand Down Expand Up @@ -403,6 +460,7 @@ fn test_pay_cancelled_invoice() {
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

// Void the invoice
client.finalize_invoice(&merchant, &invoice_id);
client.void_invoice(&merchant, &invoice_id);

// Try to pay cancelled invoice (should panic with InvalidInvoiceStatus)
Expand Down Expand Up @@ -439,6 +497,7 @@ fn test_amend_invoice_amount_success() {
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

// Amend the amount
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(&merchant, &invoice_id, &Some(2000), &None);

// Verify amount was updated
Expand All @@ -461,6 +520,7 @@ fn test_amend_invoice_description_success() {

// Amend the description
let new_description = String::from_str(&env, "Updated Description");
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(
&merchant,
&invoice_id,
Expand Down Expand Up @@ -488,6 +548,7 @@ fn test_amend_invoice_both_fields_success() {

// Amend both amount and description
let new_description = String::from_str(&env, "Updated");
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(
&merchant,
&invoice_id,
Expand Down Expand Up @@ -563,6 +624,7 @@ fn test_amend_invoice_non_owner_fails() {
// Try to amend with different merchant (should panic with NotAuthorized)
let other_merchant = Address::generate(&env);
client.register_merchant(&other_merchant);
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(&other_merchant, &invoice_id, &Some(2000), &None);
}

Expand All @@ -579,6 +641,7 @@ fn test_amend_invoice_invalid_amount_fails() {
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

// Try to amend with invalid amount (should panic with InvalidAmount)
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(&merchant, &invoice_id, &Some(0), &None);
}

Expand All @@ -595,6 +658,7 @@ fn test_amend_invoice_negative_amount_fails() {
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

// Try to amend with negative amount (should panic with InvalidAmount)
client.finalize_invoice(&merchant, &invoice_id);
client.amend_invoice(&merchant, &invoice_id, &Some(-100), &None);
}

Expand Down
23 changes: 21 additions & 2 deletions contracts/shade/src/tests/test_invoice_void.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ fn test_void_invoice_success() {
let amount: i128 = 1000;
let invoice_id = client.create_invoice(&merchant, &description, &amount, &token, &None);

// Verify invoice is Pending before voiding
let invoice_before = client.get_invoice(&invoice_id);
assert_eq!(invoice_before.status, InvoiceStatus::Pending);
assert_eq!(invoice_before.status, InvoiceStatus::Draft);

// Finalize the invoice
client.finalize_invoice(&merchant, &invoice_id);

// Verify invoice is Pending before voiding
let invoice_before_pending = client.get_invoice(&invoice_id);
assert_eq!(invoice_before_pending.status, InvoiceStatus::Pending);

// Void the invoice
client.void_invoice(&merchant, &invoice_id);
Expand All @@ -60,6 +66,7 @@ fn test_void_invoice_unauthorized_random_address() {

// Try to void with random address (should panic with NotAuthorized)
let random_address = Address::generate(&env);
client.finalize_invoice(&merchant, &invoice_id);
client.void_invoice(&random_address, &invoice_id);
}

Expand All @@ -82,6 +89,7 @@ fn test_void_invoice_unauthorized_different_merchant() {
let invoice_id = client.create_invoice(&merchant1, &description, &1000, &token, &None);

// Try to void with different merchant (should panic with NotAuthorized)
client.finalize_invoice(&merchant1, &invoice_id);
client.void_invoice(&merchant2, &invoice_id);
}

Expand Down Expand Up @@ -118,6 +126,8 @@ fn test_void_invoice_already_paid() {
let description = String::from_str(&env, "Test Invoice");
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

client.finalize_invoice(&merchant, &invoice_id);

// Pay the invoice
let customer = Address::generate(&env);
let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token);
Expand Down Expand Up @@ -161,6 +171,8 @@ fn test_pay_voided_invoice() {
let description = String::from_str(&env, "Test Invoice");
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

client.finalize_invoice(&merchant, &invoice_id);

// Void the invoice
client.void_invoice(&merchant, &invoice_id);

Expand Down Expand Up @@ -240,6 +252,8 @@ fn test_void_refunded_invoice() {
let description = String::from_str(&env, "Refundable Invoice");
let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None);

client.finalize_invoice(&merchant, &invoice_id);

let customer = Address::generate(&env);
let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token);
token_client.mint(&customer, &1000);
Expand Down Expand Up @@ -268,6 +282,11 @@ fn test_void_invoice_state_isolation() {
let invoice_id_2 = client.create_invoice(&merchant, &description, &2000, &token, &None);
let invoice_id_3 = client.create_invoice(&merchant, &description, &3000, &token, &None);

// Finalize non-cancelled ones if we want to check their Pending status
client.finalize_invoice(&merchant, &invoice_id_1);
client.finalize_invoice(&merchant, &invoice_id_2);
client.finalize_invoice(&merchant, &invoice_id_3);

// Void only the second invoice
client.void_invoice(&merchant, &invoice_id_2);

Expand Down
Loading
Loading