Skip to content

Commit 1fcfd74

Browse files
Caleb PetersonCaleb Peterson
authored andcommitted
Merge and integrate refund mechanism with subscription pausing
1 parent ed7a641 commit 1fcfd74

13 files changed

Lines changed: 14421 additions & 6 deletions

contracts/src/lib.rs

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Ve
66
#[contracttype]
77
#[derive(Clone, Debug, PartialEq)]
88
pub enum Interval {
9-
Weekly, // 604800s
10-
Monthly, // 2592000s (30 days)
11-
Quarterly, // 7776000s (90 days)
12-
Yearly, // 31536000s (365 days)
9+
Weekly, // 604800s
10+
Monthly, // 2592000s (30 days)
11+
Quarterly, // 7776000s (90 days)
12+
Yearly, // 31536000s (365 days)
1313
}
1414

1515
const MAX_PAUSE_DURATION: u64 = 2_592_000; // 30 days
@@ -63,6 +63,7 @@ pub struct Subscription {
6363
pub total_paid: i128,
6464
pub paused_at: u64,
6565
pub pause_duration: u64,
66+
pub refund_requested_amount: i128,
6667
}
6768

6869
#[contracttype]
@@ -215,6 +216,7 @@ impl SubTrackrContract {
215216
total_paid: 0,
216217
paused_at: 0,
217218
pause_duration: 0,
219+
refund_requested_amount: 0,
218220
};
219221

220222
env.storage()
@@ -379,6 +381,8 @@ impl SubTrackrContract {
379381
.get(&DataKey::Subscription(subscription_id))
380382
.expect("Subscription not found");
381383

384+
sub.subscriber.require_auth();
385+
382386
// Handle auto-resume if needed
383387
if Self::check_and_resume_internal(&env, &mut sub) {
384388
env.storage()
@@ -414,6 +418,108 @@ impl SubTrackrContract {
414418
.set(&DataKey::Subscription(subscription_id), &sub);
415419
}
416420

421+
/// Request a refund for a subscription (can only be called by the subscriber)
422+
pub fn request_refund(env: Env, subscription_id: u64, amount: i128) {
423+
let mut sub: Subscription = env
424+
.storage()
425+
.persistent()
426+
.get(&DataKey::Subscription(subscription_id))
427+
.expect("Subscription not found");
428+
429+
sub.subscriber.require_auth();
430+
431+
assert!(amount > 0, "Refund amount must be positive");
432+
assert!(
433+
amount <= sub.total_paid,
434+
"Refund amount cannot exceed total paid"
435+
);
436+
437+
sub.refund_requested_amount = amount;
438+
439+
env.storage()
440+
.persistent()
441+
.set(&DataKey::Subscription(subscription_id), &sub);
442+
443+
// Publish event
444+
env.events().publish(
445+
(String::from_str(&env, "refund_requested"), subscription_id),
446+
(sub.subscriber.clone(), amount),
447+
);
448+
}
449+
450+
/// Approve a refund (can only be called by the admin)
451+
pub fn approve_refund(env: Env, subscription_id: u64) {
452+
let mut sub: Subscription = env
453+
.storage()
454+
.persistent()
455+
.get(&DataKey::Subscription(subscription_id))
456+
.expect("Subscription not found");
457+
458+
let admin: Address = env
459+
.storage()
460+
.instance()
461+
.get(&DataKey::Admin)
462+
.expect("Admin not set");
463+
admin.require_auth();
464+
465+
let amount = sub.refund_requested_amount;
466+
assert!(amount > 0, "No pending refund request");
467+
468+
let _plan: Plan = env
469+
.storage()
470+
.persistent()
471+
.get(&DataKey::Plan(sub.plan_id))
472+
.expect("Plan not found");
473+
474+
// TODO: Execute actual token transfer from merchant back to subscriber
475+
// token::Client::new(&env, &plan.token).transfer(
476+
// &plan.merchant, &sub.subscriber, &amount
477+
// );
478+
479+
sub.total_paid -= amount;
480+
sub.refund_requested_amount = 0;
481+
482+
env.storage()
483+
.persistent()
484+
.set(&DataKey::Subscription(subscription_id), &sub);
485+
486+
// Publish event
487+
env.events().publish(
488+
(String::from_str(&env, "refund_approved"), subscription_id),
489+
(sub.subscriber.clone(), amount),
490+
);
491+
}
492+
493+
/// Reject a refund (can only be called by the admin)
494+
pub fn reject_refund(env: Env, subscription_id: u64) {
495+
let mut sub: Subscription = env
496+
.storage()
497+
.persistent()
498+
.get(&DataKey::Subscription(subscription_id))
499+
.expect("Subscription not found");
500+
501+
let admin: Address = env
502+
.storage()
503+
.instance()
504+
.get(&DataKey::Admin)
505+
.expect("Admin not set");
506+
admin.require_auth();
507+
508+
assert!(sub.refund_requested_amount > 0, "No pending refund request");
509+
510+
sub.refund_requested_amount = 0;
511+
512+
env.storage()
513+
.persistent()
514+
.set(&DataKey::Subscription(subscription_id), &sub);
515+
516+
// Publish event
517+
env.events().publish(
518+
(String::from_str(&env, "refund_rejected"), subscription_id),
519+
sub.subscriber.clone(),
520+
);
521+
}
522+
417523
// ── Queries ──
418524

419525
/// Get plan details
@@ -653,7 +759,10 @@ mod test {
653759
client.resume_subscription(&subscriber, &sub_id);
654760
let resumed = client.get_subscription(&sub_id);
655761
assert_eq!(resumed.status, SubscriptionStatus::Active);
656-
assert_eq!(resumed.next_charge_at, env.ledger().timestamp() + Interval::Monthly.seconds());
762+
assert_eq!(
763+
resumed.next_charge_at,
764+
env.ledger().timestamp() + Interval::Monthly.seconds()
765+
);
657766
assert!(resumed.next_charge_at > initial.next_charge_at);
658767
}
659768

@@ -696,8 +805,33 @@ mod test {
696805
li.timestamp += Interval::Monthly.seconds();
697806
});
698807
client.charge_subscription(&sub_id);
699-
808+
700809
let charged = client.get_subscription(&sub_id);
701810
assert_eq!(charged.total_paid, 500);
702811
}
812+
813+
#[test]
814+
fn test_refund_flow() {
815+
let env = Env::default();
816+
let (client, _admin, _merchant, subscriber, _token) = setup(&env);
817+
let sub_id = client.subscribe(&subscriber, &1);
818+
819+
// Charge the subscription at month 1
820+
env.ledger().set_timestamp(86_400 * 31);
821+
client.charge_subscription(&sub_id);
822+
823+
let sub = client.get_subscription(&sub_id);
824+
assert_eq!(sub.total_paid, 500);
825+
826+
// Request refund
827+
client.request_refund(&sub_id, &200);
828+
let sub = client.get_subscription(&sub_id);
829+
assert_eq!(sub.refund_requested_amount, 200);
830+
831+
// Approve refund
832+
client.approve_refund(&sub_id);
833+
let sub = client.get_subscription(&sub_id);
834+
assert_eq!(sub.total_paid, 300);
835+
assert_eq!(sub.refund_requested_amount, 0);
836+
}
703837
}

0 commit comments

Comments
 (0)