From cee13fa781ee0574270816a08d3dfca17d9115f3 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:17:16 +0100 Subject: [PATCH 1/6] feat(types): add discount-config type --- contracts/sharpy/src/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/sharpy/src/types.rs b/contracts/sharpy/src/types.rs index e044fe3..2e87ec3 100644 --- a/contracts/sharpy/src/types.rs +++ b/contracts/sharpy/src/types.rs @@ -265,3 +265,13 @@ pub struct InvoiceMetadata { /// Last update timestamp. pub updated_at: u64, } + +/// Discount configuration per invoice — basis points off total. +#[contracttype] +#[derive(Clone, Debug)] +pub struct DiscountConfig { + /// Discount in basis points (0-10000, 1000=10%). + pub discount_bps: u32, + /// Timestamp set. + pub updated_at: u64, +} From 4baba3e7c7a1113e2db8d109658bb660305f542c Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:17:16 +0100 Subject: [PATCH 2/6] feat(storage): add discount-config storage key and import --- contracts/sharpy/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index 5688ea8..bb72475 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -42,6 +42,7 @@ fn account_balance_key(account: &Address, token: &Address) -> (Symbol, Address, } fn invoice_notes_key(id: u64) -> (Symbol, u64) { (symbol_short!("notes"), id) } fn invoice_tags_key(id: u64) -> (Symbol, u64) { (symbol_short!("itags"), id) } +fn discount_key(id: u64) -> (Symbol, u64) { (symbol_short!("disc"), id) } fn invoice_metadata_key(id: u64) -> (Symbol, u64) { (symbol_short!("imeta"), id) } fn invoice_memo_ext_key(id: u64) -> (Symbol, u64) { (symbol_short!("imemo"), id) } From c4530d61c7acbf2e730b63dccc4399e40d67932c Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:17:32 +0100 Subject: [PATCH 3/6] feat(events): add discount-config event --- contracts/sharpy/src/events.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/sharpy/src/events.rs b/contracts/sharpy/src/events.rs index 11d8c15..2b46d9c 100644 --- a/contracts/sharpy/src/events.rs +++ b/contracts/sharpy/src/events.rs @@ -272,3 +272,10 @@ pub struct InvoiceMetadataUpdatedEvent { pub invoice_id: u64, pub updater: Addre pub fn invoice_metadata_updated(env: &Env, invoice_id: u64, updater: &Address) { env.events().publish((symbol_short!("imeta"),), InvoiceMetadataUpdatedEvent { invoice_id, updater: updater.clone() }); } + +#[contracttype] +#[derive(Clone)] +pub struct DiscountUpdatedEvent { pub invoice_id: u64, pub discount_bps: u32 } +pub fn discount_updated(env: &Env, invoice_id: u64, discount_bps: u32) { + env.events().publish((symbol_short!("disc"),), DiscountUpdatedEvent { invoice_id, discount_bps }); +} From a89d8f191ee816a11e2b3cacc7617f818ce94836 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:17:37 +0100 Subject: [PATCH 4/6] feat(contract): add discount-config core logic --- contracts/sharpy/src/lib.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index bb72475..86629d9 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -22,7 +22,7 @@ mod test; use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Bytes, Env, Map, String, Symbol, Vec}; use types::{ AuditEntry, CreateInvoiceParams, DisputeState, Invoice, InvoiceNotes, InvoiceOptions, - InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, SplitRule, + InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, DiscountConfig, SplitRule, SubscriptionParams, }; @@ -1065,6 +1065,21 @@ impl SharpyContract { pub fn get_invoice_metadata(env: Env, invoice_id: u64) -> Option { env.storage().persistent().get(&invoice_metadata_key(invoice_id)) } + + pub fn set_discount(env: Env, caller: Address, invoice_id: u64, discount_bps: u32) { + require_not_paused(&env); caller.require_auth(); + let invoice = load_invoice(&env, invoice_id); + assert!(invoice.creator == caller, "only creator can set discount"); + assert!(discount_bps <= 10000, "discount exceeds 100%"); + let cfg = DiscountConfig { discount_bps, updated_at: env.ledger().timestamp() }; + env.storage().persistent().set(&discount_key(invoice_id), &cfg); + env.storage().persistent().extend_ttl(&discount_key(invoice_id), 100_000, 6_307_200); + append_audit(&env, invoice_id, symbol_short!("disc"), &caller); + events::discount_updated(&env, invoice_id, discount_bps); + } + pub fn get_discount(env: Env, invoice_id: u64) -> Option { + env.storage().persistent().get(&discount_key(invoice_id)) + } } /// Validates that a token address is not the zero address. From c438f4cd67b5d9fd381638bccade8607922c1455 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:19:06 +0100 Subject: [PATCH 5/6] test: add discount-config tests (3 tests) --- contracts/sharpy/src/test.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 905db37..927aa0b 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -3680,3 +3680,28 @@ mod test_invoice_metadata { } } + +#[cfg(test)] +mod test_discount { + use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; + use crate::SharpyContractClient; + fn setup() -> (Env, SharpyContractClient<'static>) { let env=Env::default(); env.mock_all_auths(); let cid=env.register(crate::SharpyContract, ()); let c=SharpyContractClient::new(&env,&cid); let a=Address::generate(&env); let t=Address::generate(&env); c.initialize(&a,&t); (env,c) } + fn no_rules(env: &Env) -> crate::types::InvoiceOptions { crate::types::InvoiceOptions{escrow_enabled:false, escrow_release_delay:None, split_rules:Vec::new(env), auto_resolve_rules:Vec::new(env), arbitrator:None} } + #[test] fn test_discount_set_get() { + let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400; + let id=client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &no_rules(&env)); + assert!(client.get_discount(&id).is_none()); + client.set_discount(&creator, &id, &1000u32); + assert_eq!(client.get_discount(&id).unwrap().discount_bps, 1000u32); + } + #[test] #[should_panic(expected="discount exceeds 100%")] fn test_discount_too_high() { + let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400; + let id=client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &no_rules(&env)); + client.set_discount(&creator, &id, &10001u32); + } + #[test] #[should_panic(expected="only creator can set discount")] fn test_discount_non_creator() { + let (env, client)=setup(); let creator=Address::generate(&env); let s=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400; + let id=client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &no_rules(&env)); + client.set_discount(&s, &id, &500u32); + } +} From 1be0b19399206e7f877965e8f7de5c5a5f919bba Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Wed, 2 Sep 2026 03:19:23 +0100 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20update=20README=20badge=20150?= =?UTF-8?q?=E2=86=92153=20and=20changelog=20for=20discount-config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + README.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74fb4ab..59445a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the Sharpy smart contract are documented here. ## [Unreleased] +- feat: discount config — set/get_discount — feat/discount-config - feat: invoice metadata — set/get_invoice_metadata — Adds InvoiceMetadata (key-value map) via set/get with creato - feat: deadline extension — extend_deadline for creators — Adds extend_deadline(caller, id, new_deadline) allowing crea - feat: batch refund — refund_batch for multiple invoices — Adds refund_batch(caller, ids) to refund multiple deadline-p diff --git a/README.md b/README.md index 36394ad..1b437dd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Soroban](https://img.shields.io/badge/Soroban-Protocol%2027-6C63FF?logo=stellar) ![Rust](https://img.shields.io/badge/Rust-stable-orange?logo=rust) -![Tests](https://img.shields.io/badge/tests-150%20passing-00D4AA) +![Tests](https://img.shields.io/badge/tests-153%20passing-00D4AA) ![License](https://img.shields.io/badge/license-MIT-green) ![Version](https://img.shields.io/badge/version-0.2.0-6C63FF) [![Demo](https://img.shields.io/badge/Demo-Watch%20on%20Loom-00D4AA?logo=loom)](https://www.loom.com/share/09aa4a78e0c944dcab866a7036fde24d) @@ -81,6 +81,7 @@ graph TD - **Freeze control** — `freeze_invoice()`/`unfreeze_invoice()` admin blocks/re-enables `pay` (frozen field) - **Invoice notes** — `set_invoice_notes()`/`get_invoice_notes()` free-text `InvoiceNotes { text, updated_at }` - **Invoice tags** +- **Discount config** — `set/get_discount` `DiscountConfig { discount_bps, updated_at }` - **Invoice metadata** — `set/get_invoice_metadata` `InvoiceMetadata { entries, updated_at }` - **Deadline extension** — `extend_deadline(caller, id, new_deadline)` creator can push deadline forward - **Batch refund** — `refund_batch(caller, ids)` refund up to 10 deadline-passed invoices in one tx