From a179b984f12b461757a9156d17ed10972412208f Mon Sep 17 00:00:00 2001 From: yunusabdul38 Date: Tue, 10 Jun 2025 11:25:26 +0100 Subject: [PATCH] feat: add subscription management with cancel and renew functionality --- src/chainlib/ChainLib.cairo | 107 +++++++++++++++++++++++++++++++++ src/interfaces/IChainLib.cairo | 2 + tests/test_ChainLib.cairo | 73 +++++++++++++++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 752951b..9bc077d 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -58,6 +58,14 @@ pub mod ChainLib { Art, } + #[derive(Copy, Drop, Serde, starknet::Store, PartialEq, Debug)] + pub enum SubscriptionStatus { + Active, + #[default] + Inactive, + Cancelled, + } + #[derive(Copy, Drop, Serde, starknet::Store, Debug)] pub struct ContentMetadata { pub content_id: felt252, @@ -79,6 +87,7 @@ pub mod ChainLib { pub is_active: bool, pub last_payment_date: u64, pub subscription_type: PlanType, + pub status: SubscriptionStatus, } #[derive(Drop, Serde, starknet::Store, Clone, PartialEq)] @@ -241,6 +250,8 @@ pub mod ChainLib { DelegationExpired: DelegationExpired, ContentPurchased: ContentPurchased, PurchaseStatusUpdated: PurchaseStatusUpdated, + SubscriptionCancelled: SubscriptionCancelled, + SubscriptionRenewed: SubscriptionRenewed, } #[derive(Drop, starknet::Event)] @@ -260,6 +271,18 @@ pub mod ChainLib { pub amount: u256, } + #[derive(Drop, starknet::Event)] + struct SubscriptionRenewed { + user: ContractAddress, + subscription_id: u256, + new_end_time: u64, + } + + #[derive(Drop, starknet::Event)] + struct SubscriptionCancelled { + user: ContractAddress, + subscription_id: u256, + } #[derive(Drop, starknet::Event)] pub struct AccessVerified { pub user_id: u256, @@ -756,6 +779,7 @@ pub mod ChainLib { is_active: true, last_payment_date: current_time, subscription_type: subscription_plan.subscription_type, + status: subscription_plan.status, }; // Store the subscription @@ -1429,6 +1453,7 @@ pub mod ChainLib { is_active: true, last_payment_date: current_time, subscription_type: subscription_type, + status: SubscriptionStatus::Active, }; self.subscriptions.write(user_id, new_subscription.clone()); @@ -1859,5 +1884,87 @@ pub mod ChainLib { true } + + fn cancel_subscription(ref self: ContractState, user_id: u256) -> bool { + let caller = get_caller_address(); + + // Verify the user exists + let user = self.users.read(user_id); + assert(user.id == user_id, 'User does not exist'); + + let subscription_plan: Subscription = self.subscriptions.read(user_id); + + // update user_id subscription to cancelled + let update_subscription = Subscription { + id: subscription_plan.id, + subscriber: subscription_plan.subscriber, + plan_id: subscription_plan.plan_id, + amount: subscription_plan.amount, + start_date: subscription_plan.start_date, + end_date: subscription_plan.end_date, + is_active: false, + last_payment_date: subscription_plan.last_payment_date, + subscription_type: subscription_plan.subscription_type, + status: SubscriptionStatus::Cancelled, + }; + + // Store the subscription + self.subscriptions.write(user_id, update_subscription.clone()); + + self.subscription_record.entry(user_id).append().write(update_subscription); + + let current_count = self.subscription_count.read(user_id); + + self.emit(SubscriptionCancelled { user: caller, subscription_id: user_id }); + + true + } + + fn renew_subscription(ref self: ContractState, user_id: u256) -> bool { + let caller = get_caller_address(); + + // Verify the user exists + let user = self.users.read(user_id); + assert(user.id == user_id, 'User does not exist'); + + // let subscription_id = self.subscription_id.read(); + let subscription_plan: Subscription = self.subscriptions.read(user_id); + + let current_time = get_block_timestamp(); + + // Default subscription period is 30 days (in seconds) + let subscription_period: u64 = 30 * 24 * 60 * 60; + let end_date = current_time + subscription_period; + + // update user_id subscription to renew the previous subscription + let update_subscription = Subscription { + id: subscription_plan.id, + subscriber: subscription_plan.subscriber, + plan_id: subscription_plan.plan_id, + amount: subscription_plan.amount, + start_date: subscription_plan.start_date, + end_date: end_date, + is_active: true, + last_payment_date: subscription_plan.last_payment_date, + subscription_type: subscription_plan.subscription_type, + status: SubscriptionStatus::Active, + }; + + // Store the subscription + self.subscriptions.write(user_id, update_subscription.clone()); + + self.subscription_record.entry(user_id).append().write(update_subscription); + + let current_count = self.subscription_count.read(user_id); + + self + .emit( + SubscriptionRenewed { + user: caller, subscription_id: user_id, new_end_time: end_date, + }, + ); + + true + } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index af38808..c0a487b 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -211,4 +211,6 @@ pub trait IChainLib { fn get_content_purchases(ref self: TContractState, content_id: felt252) -> Array; fn get_user_subscription_record(ref self: TContractState, user_id: u256) -> Array; + fn cancel_subscription(ref self: TContractState, user_id: u256) -> bool; + fn renew_subscription(ref self: TContractState, user_id: u256) -> bool; } diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index f558f02..e368833 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1,7 +1,7 @@ // Import the contract modules use chain_lib::base::types::{PurchaseStatus, Rank, Role, Status}; use chain_lib::chainlib::ChainLib; -use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, PlanType}; +use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, PlanType, SubscriptionStatus}; use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; use snforge_std::{ CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare, @@ -331,6 +331,7 @@ fn test_create_subscription() { let subscription = dispatcher.get_user_subscription(account_id); assert(subscription.id == 1, 'Subscription ID should be 1'); assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Active, 'Plan type should be YEARLY'); let subscription_record = dispatcher.get_user_subscription_record(account_id); assert(subscription_record.len() == 1, 'record should have length 1'); } @@ -748,3 +749,73 @@ fn test_update_nonexistent_purchase() { let nonexistent_purchase_id = 42_u256; let _ = dispatcher.update_purchase_status(nonexistent_purchase_id, PurchaseStatus::Completed); } + +#[test] +fn test_cancel_subscription() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + // Test input values + let username: felt252 = 'John'; + let role: Role = Role::READER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + // Call register + let account_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + dispatcher.create_subscription(account_id, 500, 1); + let subscription = dispatcher.get_user_subscription(account_id); + assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Active, 'Plan type should be YEARLY'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); + + dispatcher.cancel_subscription(account_id); + let subscription = dispatcher.get_user_subscription(account_id); + assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Cancelled, 'Plan status should be cancelled'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); +} + +#[test] +fn test_renew_subscription() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + // Test input values + let username: felt252 = 'John'; + let role: Role = Role::READER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + // Call register + let account_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + dispatcher.create_subscription(account_id, 500, 1); + let subscription = dispatcher.get_user_subscription(account_id); + assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Active, 'Plan type should be YEARLY'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); + + dispatcher.cancel_subscription(account_id); + let subscription = dispatcher.get_user_subscription(account_id); + assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Cancelled, 'Plan status should be cancelled'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); + + dispatcher.renew_subscription(account_id); + let subscription = dispatcher.get_user_subscription(account_id); + assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + assert(subscription.status == SubscriptionStatus::Active, 'Plan status should be Active'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); +}