@@ -6,10 +6,10 @@ use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Ve
66#[ contracttype]
77#[ derive( Clone , Debug , PartialEq ) ]
88pub 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
1515const 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