Skip to content

Commit 3d30f79

Browse files
authored
Merge pull request #165 from Abdulazeem-code/feature/144-fee-discount-system
feat: Implement tiered fee discounts for merchants based on transacti…
2 parents bd38edb + c970262 commit 3d30f79

File tree

10 files changed

+277
-26
lines changed

10 files changed

+277
-26
lines changed

contracts/shade/src/components/admin.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,54 @@ pub fn get_fee(env: &Env, token: &Address) -> i128 {
131131
.unwrap_or(0)
132132
}
133133

134-
pub fn calculate_fee(env: &Env, token: &Address, amount: i128) -> i128 {
135-
let fee_bps: i128 = get_fee(env, token);
136-
if fee_bps == 0 {
134+
pub fn calculate_fee(env: &Env, merchant: &Address, token: &Address, amount: i128) -> i128 {
135+
let base_fee = get_fee(env, token);
136+
if base_fee == 0 {
137137
return 0;
138138
}
139-
(amount * fee_bps) / 10_000i128
139+
140+
let volume = get_merchant_volume(env, merchant);
141+
let discount_bps = discount_bps_for_volume(volume);
142+
143+
if discount_bps > 0 {
144+
events::publish_fee_discount_applied_event(
145+
env,
146+
merchant.clone(),
147+
volume,
148+
discount_bps,
149+
env.ledger().timestamp(),
150+
);
151+
}
152+
153+
let fee = amount * base_fee / 10_000;
154+
fee - (fee * discount_bps / 10_000)
155+
}
156+
157+
pub fn get_merchant_volume(env: &Env, merchant: &Address) -> i128 {
158+
env.storage()
159+
.persistent()
160+
.get(&DataKey::MerchantVolume(merchant.clone()))
161+
.unwrap_or(0)
162+
}
163+
164+
pub fn add_merchant_volume(env: &Env, merchant: &Address, amount: i128) {
165+
let current = get_merchant_volume(env, merchant);
166+
env.storage().persistent().set(
167+
&DataKey::MerchantVolume(merchant.clone()),
168+
&(current + amount),
169+
);
170+
}
171+
172+
fn discount_bps_for_volume(volume: i128) -> i128 {
173+
if volume >= 100_000 {
174+
50
175+
} else if volume >= 50_000 {
176+
25
177+
} else if volume >= 10_000 {
178+
10
179+
} else {
180+
0
181+
}
140182
}
141183

142184
pub fn propose_fee(env: &Env, admin: &Address, token: &Address, fee: i128) {

contracts/shade/src/components/invoice.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,11 +532,11 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount:
532532
panic_with_error!(env, ContractError::TokenNotAccepted);
533533
}
534534

535-
let fee_amount = admin::calculate_fee(env, &invoice.token, amount);
535+
let merchant_account_id = merchant::get_merchant_account(env, invoice.merchant_id);
536+
let fee_amount = admin::calculate_fee(env, &merchant_account_id, &invoice.token, amount);
536537
let merchant_amount = amount - fee_amount;
537538

538539
let token_client = token::TokenClient::new(env, &invoice.token);
539-
let merchant_account_id = merchant::get_merchant_account(env, invoice.merchant_id);
540540

541541
token_client.transfer(payer, &merchant_account_id, &merchant_amount);
542542
if fee_amount > 0 {

contracts/shade/src/components/subscription.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub fn create_subscription_plan(
4747
panic_with_error!(env, ContractError::TokenNotAccepted);
4848
}
4949

50-
let fee_amount = admin::calculate_fee(env, &token, amount);
50+
let fee_amount = admin::calculate_fee(env, &merchant, &token, amount);
5151
if amount < fee_amount {
5252
panic_with_error!(env, ContractError::InvalidAmount);
5353
}
@@ -141,7 +141,7 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) {
141141
panic_with_error!(env, ContractError::ChargeTooEarly);
142142
}
143143

144-
let fee = admin::calculate_fee(env, &plan.token, plan.amount);
144+
let fee = admin::calculate_fee(env, &plan.merchant, &plan.token, plan.amount);
145145
let merchant_amount = plan.amount - fee;
146146

147147
let token_client = token::TokenClient::new(env, &plan.token);

contracts/shade/src/events.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,30 @@ pub fn publish_account_restricted_event(
356356
.publish(env);
357357
}
358358

359+
#[contractevent]
360+
pub struct FeeDiscountAppliedEvent {
361+
pub merchant: Address,
362+
pub volume: i128,
363+
pub discount_bps: i128,
364+
pub timestamp: u64,
365+
}
366+
367+
pub fn publish_fee_discount_applied_event(
368+
env: &Env,
369+
merchant: Address,
370+
volume: i128,
371+
discount_bps: i128,
372+
timestamp: u64,
373+
) {
374+
FeeDiscountAppliedEvent {
375+
merchant,
376+
volume,
377+
discount_bps,
378+
timestamp,
379+
}
380+
.publish(env);
381+
}
382+
359383
// Kept merchant_amount from your branch AND merchant_account from main — both are useful.
360384
#[contractevent]
361385
pub struct InvoicePaidEvent {

contracts/shade/src/interface.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ pub trait ShadeTrait {
7373
merchant_address: Address,
7474
status: bool,
7575
);
76+
fn calculate_fee(env: Env, merchant: Address, token: Address, amount: i128) -> i128;
77+
fn get_merchant_volume(env: Env, merchant: Address) -> i128;
7678
fn set_merchant_account(env: Env, merchant: Address, account: Address);
7779
fn get_merchant_account(env: Env, merchant_id: u64) -> Address;
7880
fn pay_invoice(env: Env, payer: Address, invoice_id: u64);

contracts/shade/src/shade.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ impl ShadeTrait for Shade {
238238
merchant_component::restrict_merchant_account(&env, &caller, &merchant_address, status);
239239
}
240240

241+
fn calculate_fee(env: Env, merchant: Address, token: Address, amount: i128) -> i128 {
242+
admin_component::calculate_fee(&env, &merchant, &token, amount)
243+
}
244+
245+
fn get_merchant_volume(env: Env, merchant: Address) -> i128 {
246+
admin_component::get_merchant_volume(&env, &merchant)
247+
}
248+
241249
fn set_merchant_account(env: Env, merchant: Address, account: Address) {
242250
merchant_component::set_merchant_account(&env, &merchant, &account);
243251
}

contracts/shade/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod test_access_control;
44
pub mod test_account_factory;
55
pub mod test_admin_payment;
66
pub mod test_admin_transfer;
7+
pub mod test_fee_discount;
78
// pub mod test_batch_token_whitelist;
89
pub mod test_calculate_fee;
910
pub mod test_date_range_filter;

contracts/shade/src/tests/test_calculate_fee.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@ fn setup(env: &Env) -> (Address, ShadeClient<'_>, Address) {
2929
#[test]
3030
fn test_calculate_fee_returns_zero_when_no_fee_set() {
3131
let env = Env::default();
32-
let (_admin, client, token) = setup(&env);
32+
let (admin, client, token) = setup(&env);
3333
let contract_id = client.address.clone();
3434

3535
env.as_contract(&contract_id, || {
36-
assert_eq!(admin_component::calculate_fee(&env, &token, 0), 0);
37-
assert_eq!(admin_component::calculate_fee(&env, &token, 1_000), 0);
38-
assert_eq!(admin_component::calculate_fee(&env, &token, 1_000_000), 0);
36+
assert_eq!(admin_component::calculate_fee(&env, &admin, &token, 0), 0);
37+
assert_eq!(
38+
admin_component::calculate_fee(&env, &admin, &token, 1_000),
39+
0
40+
);
41+
assert_eq!(
42+
admin_component::calculate_fee(&env, &admin, &token, 1_000_000),
43+
0
44+
);
3945
});
4046
}
4147

@@ -49,7 +55,7 @@ fn test_calculate_fee_zero_amount() {
4955
client.set_fee(&admin, &token, &500); // 5%
5056

5157
env.as_contract(&contract_id, || {
52-
assert_eq!(admin_component::calculate_fee(&env, &token, 0), 0);
58+
assert_eq!(admin_component::calculate_fee(&env, &admin, &token, 0), 0);
5359
});
5460
}
5561

@@ -63,7 +69,10 @@ fn test_calculate_fee_5_percent() {
6369
client.set_fee(&admin, &token, &500);
6470

6571
env.as_contract(&contract_id, || {
66-
assert_eq!(admin_component::calculate_fee(&env, &token, 1_000), 50);
72+
assert_eq!(
73+
admin_component::calculate_fee(&env, &admin, &token, 1_000),
74+
50
75+
);
6776
});
6877
}
6978

@@ -77,7 +86,10 @@ fn test_calculate_fee_1_percent() {
7786
client.set_fee(&admin, &token, &100);
7887

7988
env.as_contract(&contract_id, || {
80-
assert_eq!(admin_component::calculate_fee(&env, &token, 10_000), 100);
89+
assert_eq!(
90+
admin_component::calculate_fee(&env, &admin, &token, 10_000),
91+
100
92+
);
8193
});
8294
}
8395

@@ -91,7 +103,10 @@ fn test_calculate_fee_10_percent() {
91103
client.set_fee(&admin, &token, &1_000);
92104

93105
env.as_contract(&contract_id, || {
94-
assert_eq!(admin_component::calculate_fee(&env, &token, 5_000), 500);
106+
assert_eq!(
107+
admin_component::calculate_fee(&env, &admin, &token, 5_000),
108+
500
109+
);
95110
});
96111
}
97112

@@ -105,7 +120,10 @@ fn test_calculate_fee_100_percent() {
105120
client.set_fee(&admin, &token, &10_000);
106121

107122
env.as_contract(&contract_id, || {
108-
assert_eq!(admin_component::calculate_fee(&env, &token, 1_000), 1_000);
123+
assert_eq!(
124+
admin_component::calculate_fee(&env, &admin, &token, 1_000),
125+
1_000
126+
);
109127
});
110128
}
111129

@@ -121,10 +139,16 @@ fn test_calculate_fee_truncates_fractional_result() {
121139

122140
env.as_contract(&contract_id, || {
123141
// Too small to produce a whole unit
124-
assert_eq!(admin_component::calculate_fee(&env, &token, 1), 0);
125-
assert_eq!(admin_component::calculate_fee(&env, &token, 9_999), 0);
142+
assert_eq!(admin_component::calculate_fee(&env, &admin, &token, 1), 0);
143+
assert_eq!(
144+
admin_component::calculate_fee(&env, &admin, &token, 9_999),
145+
0
146+
);
126147
// Exactly at the boundary: 10_000 * 1 / 10_000 = 1
127-
assert_eq!(admin_component::calculate_fee(&env, &token, 10_000), 1);
148+
assert_eq!(
149+
admin_component::calculate_fee(&env, &admin, &token, 10_000),
150+
1
151+
);
128152
});
129153
}
130154

@@ -138,13 +162,19 @@ fn test_calculate_fee_reflects_updated_fee() {
138162
client.set_fee(&admin, &token, &200); // 2%
139163

140164
env.as_contract(&contract_id, || {
141-
assert_eq!(admin_component::calculate_fee(&env, &token, 10_000), 200);
165+
assert_eq!(
166+
admin_component::calculate_fee(&env, &admin, &token, 10_000),
167+
200
168+
);
142169
});
143170

144171
client.set_fee(&admin, &token, &500); // updated to 5%
145172

146173
env.as_contract(&contract_id, || {
147-
assert_eq!(admin_component::calculate_fee(&env, &token, 10_000), 500);
174+
assert_eq!(
175+
admin_component::calculate_fee(&env, &admin, &token, 10_000),
176+
500
177+
);
148178
});
149179
}
150180

@@ -160,7 +190,7 @@ fn test_calculate_fee_large_amount() {
160190

161191
env.as_contract(&contract_id, || {
162192
assert_eq!(
163-
admin_component::calculate_fee(&env, &token, 1_000_000_000),
193+
admin_component::calculate_fee(&env, &admin, &token, 1_000_000_000),
164194
25_000_000
165195
);
166196
});
@@ -184,7 +214,13 @@ fn test_calculate_fee_per_token_independence() {
184214
client.set_fee(&admin, &token_b, &700); // 7%
185215

186216
env.as_contract(&contract_id, || {
187-
assert_eq!(admin_component::calculate_fee(&env, &token_a, 10_000), 300);
188-
assert_eq!(admin_component::calculate_fee(&env, &token_b, 10_000), 700);
217+
assert_eq!(
218+
admin_component::calculate_fee(&env, &admin, &token_a, 10_000),
219+
300
220+
);
221+
assert_eq!(
222+
admin_component::calculate_fee(&env, &admin, &token_b, 10_000),
223+
700
224+
);
189225
});
190226
}

0 commit comments

Comments
 (0)