From 6da808a663b2e6d459e4efc2016947527b9de820 Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:12:03 +0100 Subject: [PATCH 1/6] feat: add PlanList and PaginationMeta models for Paystack API integration --- .../plan/models/plan/src/plan_list.dart | 70 +++++ .../models/plan/src/plan_list.mapper.dart | 285 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.mapper.dart diff --git a/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.dart b/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.dart new file mode 100644 index 0000000..f0cc474 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.dart @@ -0,0 +1,70 @@ +import 'package:dart_mappable/dart_mappable.dart'; +import 'package:mind_paystack/src/core/models/src/plan.dart'; +part 'plan_list.mapper.dart'; + +/// Represents a paginated list of subscription plans returned from Paystack API. +/// +/// This model wraps the response from listing plans, providing both the plan data +/// and pagination metadata for implementing proper pagination in applications. +/// +/// Example usage: +/// ```dart +/// final planList = PlanList.fromJson(response); +/// print('Total plans: ${planList.meta.total}'); +/// +/// for (final plan in planList.data) { +/// print('${plan.name}: ${plan.amount} every ${plan.interval}'); +/// } +/// ``` +@MappableClass() +class PlanList with PlanListMappable { + /// Creates a new PlanList instance. + const PlanList({ + required this.status, + required this.message, + required this.data, + required this.meta, + }); + + /// Response status from Paystack API. + final bool status; + + /// Response message from Paystack API. + final String message; + + /// List of plan objects. + final List data; + + /// Pagination metadata. + final PaginationMeta meta; +} + +/// Represents pagination metadata for plan list responses. +@MappableClass() +class PaginationMeta with PaginationMetaMappable { + /// Creates a new PaginationMeta instance. + const PaginationMeta({ + required this.total, + required this.skipped, + required this.perPage, + required this.page, + required this.pageCount, + }); + + /// Total number of plans available. + final int total; + + /// Number of plans skipped (for pagination). + final int skipped; + + /// Number of plans per page. + @MappableField(key: 'per_page') + final int perPage; + + /// Current page number. + final int page; + + /// Total number of pages available. + @MappableField(key: 'page_count') + final int pageCount; +} \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.mapper.dart b/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.mapper.dart new file mode 100644 index 0000000..2d193b1 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/plan/src/plan_list.mapper.dart @@ -0,0 +1,285 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'plan_list.dart'; + +class PlanListMapper extends ClassMapperBase { + PlanListMapper._(); + + static PlanListMapper? _instance; + static PlanListMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = PlanListMapper._()); + PlanMapper.ensureInitialized(); + PaginationMetaMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'PlanList'; + + static bool _$status(PlanList v) => v.status; + static const Field _f$status = Field('status', _$status); + static String _$message(PlanList v) => v.message; + static const Field _f$message = Field('message', _$message); + static List _$data(PlanList v) => v.data; + static const Field> _f$data = Field('data', _$data); + static PaginationMeta _$meta(PlanList v) => v.meta; + static const Field _f$meta = Field('meta', _$meta); + + @override + final MappableFields fields = const { + #status: _f$status, + #message: _f$message, + #data: _f$data, + #meta: _f$meta, + }; + + static PlanList _instantiate(DecodingData data) { + return PlanList( + status: data.dec(_f$status), + message: data.dec(_f$message), + data: data.dec(_f$data), + meta: data.dec(_f$meta)); + } + + @override + final Function instantiate = _instantiate; + + static PlanList fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static PlanList fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin PlanListMappable { + String toJson() { + return PlanListMapper.ensureInitialized() + .encodeJson(this as PlanList); + } + + Map toMap() { + return PlanListMapper.ensureInitialized() + .encodeMap(this as PlanList); + } + + PlanListCopyWith get copyWith => + _PlanListCopyWithImpl( + this as PlanList, $identity, $identity); + @override + String toString() { + return PlanListMapper.ensureInitialized().stringifyValue(this as PlanList); + } + + @override + bool operator ==(Object other) { + return PlanListMapper.ensureInitialized() + .equalsValue(this as PlanList, other); + } + + @override + int get hashCode { + return PlanListMapper.ensureInitialized().hashValue(this as PlanList); + } +} + +extension PlanListValueCopy<$R, $Out> on ObjectCopyWith<$R, PlanList, $Out> { + PlanListCopyWith<$R, PlanList, $Out> get $asPlanList => + $base.as((v, t, t2) => _PlanListCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class PlanListCopyWith<$R, $In extends PlanList, $Out> + implements ClassCopyWith<$R, $In, $Out> { + ListCopyWith<$R, Plan, PlanCopyWith<$R, Plan, Plan>> get data; + PaginationMetaCopyWith<$R, PaginationMeta, PaginationMeta> get meta; + $R call( + {bool? status, String? message, List? data, PaginationMeta? meta}); + PlanListCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _PlanListCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, PlanList, $Out> + implements PlanListCopyWith<$R, PlanList, $Out> { + _PlanListCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + PlanListMapper.ensureInitialized(); + @override + ListCopyWith<$R, Plan, PlanCopyWith<$R, Plan, Plan>> get data => ListCopyWith( + $value.data, (v, t) => v.copyWith.$chain(t), (v) => call(data: v)); + @override + PaginationMetaCopyWith<$R, PaginationMeta, PaginationMeta> get meta => + $value.meta.copyWith.$chain((v) => call(meta: v)); + @override + $R call( + {bool? status, + String? message, + List? data, + PaginationMeta? meta}) => + $apply(FieldCopyWithData({ + if (status != null) #status: status, + if (message != null) #message: message, + if (data != null) #data: data, + if (meta != null) #meta: meta + })); + @override + PlanList $make(CopyWithData data) => PlanList( + status: data.get(#status, or: $value.status), + message: data.get(#message, or: $value.message), + data: data.get(#data, or: $value.data), + meta: data.get(#meta, or: $value.meta)); + + @override + PlanListCopyWith<$R2, PlanList, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _PlanListCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + +class PaginationMetaMapper extends ClassMapperBase { + PaginationMetaMapper._(); + + static PaginationMetaMapper? _instance; + static PaginationMetaMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = PaginationMetaMapper._()); + } + return _instance!; + } + + @override + final String id = 'PaginationMeta'; + + static int _$total(PaginationMeta v) => v.total; + static const Field _f$total = Field('total', _$total); + static int _$skipped(PaginationMeta v) => v.skipped; + static const Field _f$skipped = + Field('skipped', _$skipped); + static int _$perPage(PaginationMeta v) => v.perPage; + static const Field _f$perPage = + Field('perPage', _$perPage, key: r'per_page'); + static int _$page(PaginationMeta v) => v.page; + static const Field _f$page = Field('page', _$page); + static int _$pageCount(PaginationMeta v) => v.pageCount; + static const Field _f$pageCount = + Field('pageCount', _$pageCount, key: r'page_count'); + + @override + final MappableFields fields = const { + #total: _f$total, + #skipped: _f$skipped, + #perPage: _f$perPage, + #page: _f$page, + #pageCount: _f$pageCount, + }; + + static PaginationMeta _instantiate(DecodingData data) { + return PaginationMeta( + total: data.dec(_f$total), + skipped: data.dec(_f$skipped), + perPage: data.dec(_f$perPage), + page: data.dec(_f$page), + pageCount: data.dec(_f$pageCount)); + } + + @override + final Function instantiate = _instantiate; + + static PaginationMeta fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static PaginationMeta fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin PaginationMetaMappable { + String toJson() { + return PaginationMetaMapper.ensureInitialized() + .encodeJson(this as PaginationMeta); + } + + Map toMap() { + return PaginationMetaMapper.ensureInitialized() + .encodeMap(this as PaginationMeta); + } + + PaginationMetaCopyWith + get copyWith => + _PaginationMetaCopyWithImpl( + this as PaginationMeta, $identity, $identity); + @override + String toString() { + return PaginationMetaMapper.ensureInitialized() + .stringifyValue(this as PaginationMeta); + } + + @override + bool operator ==(Object other) { + return PaginationMetaMapper.ensureInitialized() + .equalsValue(this as PaginationMeta, other); + } + + @override + int get hashCode { + return PaginationMetaMapper.ensureInitialized() + .hashValue(this as PaginationMeta); + } +} + +extension PaginationMetaValueCopy<$R, $Out> + on ObjectCopyWith<$R, PaginationMeta, $Out> { + PaginationMetaCopyWith<$R, PaginationMeta, $Out> get $asPaginationMeta => + $base.as((v, t, t2) => _PaginationMetaCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class PaginationMetaCopyWith<$R, $In extends PaginationMeta, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call({int? total, int? skipped, int? perPage, int? page, int? pageCount}); + PaginationMetaCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _PaginationMetaCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, PaginationMeta, $Out> + implements PaginationMetaCopyWith<$R, PaginationMeta, $Out> { + _PaginationMetaCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + PaginationMetaMapper.ensureInitialized(); + @override + $R call( + {int? total, + int? skipped, + int? perPage, + int? page, + int? pageCount}) => + $apply(FieldCopyWithData({ + if (total != null) #total: total, + if (skipped != null) #skipped: skipped, + if (perPage != null) #perPage: perPage, + if (page != null) #page: page, + if (pageCount != null) #pageCount: pageCount + })); + @override + PaginationMeta $make(CopyWithData data) => PaginationMeta( + total: data.get(#total, or: $value.total), + skipped: data.get(#skipped, or: $value.skipped), + perPage: data.get(#perPage, or: $value.perPage), + page: data.get(#page, or: $value.page), + pageCount: data.get(#pageCount, or: $value.pageCount)); + + @override + PaginationMetaCopyWith<$R2, PaginationMeta, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _PaginationMetaCopyWithImpl<$R2, $Out2>($value, $cast, t); +} From 1924aebb23b6f8ad53f5a07a2097a8eaf8f8d514 Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:12:44 +0100 Subject: [PATCH 2/6] feat: add ListPlansOptions model and mapper for subscription plan filtering --- .../options/src/list_plans_options.dart | 62 +++++++ .../src/list_plans_options.mapper.dart | 165 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.mapper.dart diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.dart new file mode 100644 index 0000000..600c236 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.dart @@ -0,0 +1,62 @@ +import 'package:dart_mappable/dart_mappable.dart'; +part 'list_plans_options.mapper.dart'; + +/// Options for listing and filtering subscription plans. +/// +/// This class provides various filtering and pagination options for +/// retrieving plan lists from Paystack. All parameters are optional +/// and can be combined to create specific queries. +/// +/// Example usage: +/// ```dart +/// final listOptions = ListPlansOptions( +/// status: 'active', +/// interval: 'monthly', +/// perPage: 50, +/// page: 1, +/// ); +/// ``` +@MappableClass() +class ListPlansOptions with ListPlansOptionsMappable { + /// Creates a new ListPlansOptions instance. + /// + /// All parameters are optional and can be used to filter and + /// paginate the plan list. + const ListPlansOptions({ + this.perPage, + this.page, + this.status, + this.interval, + this.amount, + this.currency, + }); + + /// Number of plans to return per page. + /// + /// Default is typically 50, maximum is usually 100. + @MappableField(key: 'per_page') + final int? perPage; + + /// Page number to retrieve (1-based). + final int? page; + + /// Filter by plan status. + /// + /// Common values: 'active', 'inactive' + final String? status; + + /// Filter by billing interval. + /// + /// Values: 'daily', 'weekly', 'monthly', 'biannually', 'annually' + final String? interval; + + /// Filter by specific plan amount (in smallest currency unit). + /// + /// For example: 50000 kobo for ₦500.00 plans. + final int? amount; + + /// Filter by plan currency. + /// + /// Example: 'NGN', 'USD', 'GHS' + final String? currency; +} \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.mapper.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.mapper.dart new file mode 100644 index 0000000..031b78c --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/list_plans_options.mapper.dart @@ -0,0 +1,165 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'list_plans_options.dart'; + +class ListPlansOptionsMapper extends ClassMapperBase { + ListPlansOptionsMapper._(); + + static ListPlansOptionsMapper? _instance; + static ListPlansOptionsMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = ListPlansOptionsMapper._()); + } + return _instance!; + } + + @override + final String id = 'ListPlansOptions'; + + static int? _$perPage(ListPlansOptions v) => v.perPage; + static const Field _f$perPage = + Field('perPage', _$perPage, key: r'per_page', opt: true); + static int? _$page(ListPlansOptions v) => v.page; + static const Field _f$page = + Field('page', _$page, opt: true); + static String? _$status(ListPlansOptions v) => v.status; + static const Field _f$status = + Field('status', _$status, opt: true); + static String? _$interval(ListPlansOptions v) => v.interval; + static const Field _f$interval = + Field('interval', _$interval, opt: true); + static int? _$amount(ListPlansOptions v) => v.amount; + static const Field _f$amount = + Field('amount', _$amount, opt: true); + static String? _$currency(ListPlansOptions v) => v.currency; + static const Field _f$currency = + Field('currency', _$currency, opt: true); + + @override + final MappableFields fields = const { + #perPage: _f$perPage, + #page: _f$page, + #status: _f$status, + #interval: _f$interval, + #amount: _f$amount, + #currency: _f$currency, + }; + + static ListPlansOptions _instantiate(DecodingData data) { + return ListPlansOptions( + perPage: data.dec(_f$perPage), + page: data.dec(_f$page), + status: data.dec(_f$status), + interval: data.dec(_f$interval), + amount: data.dec(_f$amount), + currency: data.dec(_f$currency)); + } + + @override + final Function instantiate = _instantiate; + + static ListPlansOptions fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static ListPlansOptions fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin ListPlansOptionsMappable { + String toJson() { + return ListPlansOptionsMapper.ensureInitialized() + .encodeJson(this as ListPlansOptions); + } + + Map toMap() { + return ListPlansOptionsMapper.ensureInitialized() + .encodeMap(this as ListPlansOptions); + } + + ListPlansOptionsCopyWith + get copyWith => + _ListPlansOptionsCopyWithImpl( + this as ListPlansOptions, $identity, $identity); + @override + String toString() { + return ListPlansOptionsMapper.ensureInitialized() + .stringifyValue(this as ListPlansOptions); + } + + @override + bool operator ==(Object other) { + return ListPlansOptionsMapper.ensureInitialized() + .equalsValue(this as ListPlansOptions, other); + } + + @override + int get hashCode { + return ListPlansOptionsMapper.ensureInitialized() + .hashValue(this as ListPlansOptions); + } +} + +extension ListPlansOptionsValueCopy<$R, $Out> + on ObjectCopyWith<$R, ListPlansOptions, $Out> { + ListPlansOptionsCopyWith<$R, ListPlansOptions, $Out> + get $asListPlansOptions => $base + .as((v, t, t2) => _ListPlansOptionsCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class ListPlansOptionsCopyWith<$R, $In extends ListPlansOptions, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call( + {int? perPage, + int? page, + String? status, + String? interval, + int? amount, + String? currency}); + ListPlansOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _ListPlansOptionsCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, ListPlansOptions, $Out> + implements ListPlansOptionsCopyWith<$R, ListPlansOptions, $Out> { + _ListPlansOptionsCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + ListPlansOptionsMapper.ensureInitialized(); + @override + $R call( + {Object? perPage = $none, + Object? page = $none, + Object? status = $none, + Object? interval = $none, + Object? amount = $none, + Object? currency = $none}) => + $apply(FieldCopyWithData({ + if (perPage != $none) #perPage: perPage, + if (page != $none) #page: page, + if (status != $none) #status: status, + if (interval != $none) #interval: interval, + if (amount != $none) #amount: amount, + if (currency != $none) #currency: currency + })); + @override + ListPlansOptions $make(CopyWithData data) => ListPlansOptions( + perPage: data.get(#perPage, or: $value.perPage), + page: data.get(#page, or: $value.page), + status: data.get(#status, or: $value.status), + interval: data.get(#interval, or: $value.interval), + amount: data.get(#amount, or: $value.amount), + currency: data.get(#currency, or: $value.currency)); + + @override + ListPlansOptionsCopyWith<$R2, ListPlansOptions, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _ListPlansOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); +} From 0de5fe6c6d3acda0edcd020f45ad3b97c7128360 Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:12:59 +0100 Subject: [PATCH 3/6] feat: add IPlanService interface for managing subscription plans --- .../plan/interfaces/i_plan_service.dart | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 packages/mind_paystack/lib/src/features/plan/interfaces/i_plan_service.dart diff --git a/packages/mind_paystack/lib/src/features/plan/interfaces/i_plan_service.dart b/packages/mind_paystack/lib/src/features/plan/interfaces/i_plan_service.dart new file mode 100644 index 0000000..472f6b9 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/interfaces/i_plan_service.dart @@ -0,0 +1,141 @@ +import 'package:mind_paystack/src/core/models/resource.dart'; +import 'package:mind_paystack/src/features/plan/models/models.dart'; + +/// Interface defining the contract for plan-related business operations. +/// +/// This abstract class defines all the plan operations that can be +/// performed through the MindPaystack SDK. It serves as a contract that +/// concrete service implementations must fulfill, providing a clean separation +/// between the business logic layer and the data access layer. +/// +/// The service provides high-level plan operations including creation, +/// listing, fetching, and updating of subscription plans. All methods return +/// Resource objects for consistent error handling and response structure. +/// +/// Example implementation usage: +/// ```dart +/// class PlanService implements IPlanService { +/// final PlanRepository _repository; +/// +/// PlanService(this._repository); +/// +/// @override +/// Future> create(CreatePlanOptions options) async { +/// // Business logic and validation here +/// return await _repository.create(options); +/// } +/// } +/// ``` +abstract class IPlanService { + /// Creates a new subscription plan. + /// + /// Creates a new plan on Paystack's servers with the specified + /// billing details. Plans are used to create recurring subscriptions + /// for customers who want to pay regularly. + /// + /// Parameters: + /// - [options]: Configuration for the new plan including name, amount, + /// interval, and other plan details + /// + /// Returns: + /// A Resource containing Plan data with the created plan details. + /// + /// Throws: + /// - MindException if validation fails or API request encounters errors + /// + /// Example: + /// ```dart + /// final options = CreatePlanOptions( + /// name: 'Premium Monthly', + /// amount: 500000, // ₦5,000 in kobo + /// interval: 'monthly', + /// ); + /// + /// final result = await planService.create(options); + /// if (result.isSuccess) { + /// final plan = result.data!; + /// print('Plan created: ${plan.planCode}'); + /// } + /// ``` + Future> create(CreatePlanOptions options); + + /// Retrieves a list of subscription plans. + /// + /// Fetches plans from Paystack with optional filtering and pagination. + /// This is useful for displaying available subscription options to customers + /// or for administrative plan management. + /// + /// Parameters: + /// - [options]: Optional filtering and pagination parameters + /// + /// Returns: + /// A Resource containing PlanList with plans and pagination metadata. + /// + /// Example: + /// ```dart + /// final options = ListPlansOptions( + /// perPage: 10, + /// page: 1, + /// status: 'active', + /// ); + /// + /// final result = await planService.list(options); + /// if (result.isSuccess) { + /// final plans = result.data!.data; + /// for (final plan in plans) { + /// print('${plan.name}: ${plan.amount} every ${plan.interval}'); + /// } + /// } + /// ``` + Future>> list([ListPlansOptions? options]); + + /// Fetches a specific subscription plan by its ID or plan code. + /// + /// Retrieves detailed information about a specific plan. This is useful + /// for displaying plan details before subscription or for administrative + /// purposes. + /// + /// Parameters: + /// - [planIdOrCode]: The plan ID or plan code to fetch + /// + /// Returns: + /// A Resource containing Plan data with the complete plan details. + /// + /// Example: + /// ```dart + /// final result = await planService.fetch('PLN_xyz123'); + /// if (result.isSuccess) { + /// final plan = result.data!; + /// print('Plan: ${plan.name} - ${plan.amount} ${plan.currency}'); + /// } + /// ``` + Future> fetch(String planIdOrCode); + + /// Updates an existing subscription plan. + /// + /// Updates plan details such as name, amount, or currency. Note that + /// some fields like interval may not be updateable depending on + /// Paystack's API restrictions. + /// + /// Parameters: + /// - [planIdOrCode]: The plan ID or plan code to update + /// - [options]: The fields to update + /// + /// Returns: + /// A Resource containing Plan data with the updated plan details. + /// + /// Example: + /// ```dart + /// final options = UpdatePlanOptions( + /// name: 'Premium Monthly (Updated)', + /// amount: 600000, // ₦6,000 in kobo + /// ); + /// + /// final result = await planService.update('PLN_xyz123', options); + /// if (result.isSuccess) { + /// final plan = result.data!; + /// print('Plan updated: ${plan.name}'); + /// } + /// ``` + Future> update(String planIdOrCode, UpdatePlanOptions options); +} From b33f40117aa8ce4f083438e1e8bea094d94fe3cf Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:13:36 +0100 Subject: [PATCH 4/6] feat: add CreatePlanOptions model and mapper for subscription plan creation --- .../options/src/create_plan_options.dart | 96 +++++++++ .../src/create_plan_options.mapper.dart | 183 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.mapper.dart diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.dart new file mode 100644 index 0000000..e18c370 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.dart @@ -0,0 +1,96 @@ +import 'package:dart_mappable/dart_mappable.dart'; +part 'create_plan_options.mapper.dart'; + +/// Options for creating a new subscription plan. +/// +/// This class provides the required and optional parameters for +/// creating a subscription plan on Paystack. Plans define the structure +/// for recurring billing including amount, billing frequency, and +/// identification details. +/// +/// Example usage: +/// ```dart +/// final createOptions = CreatePlanOptions( +/// name: 'Premium Monthly', +/// amount: 500000, // ₦5,000 in kobo +/// interval: 'monthly', +/// planCode: 'premium_monthly', +/// currency: 'NGN', +/// ); +/// ``` +@MappableClass() +class CreatePlanOptions with CreatePlanOptionsMappable { + /// Creates a new CreatePlanOptions instance. + /// + /// [name], [amount], and [interval] are required parameters. + /// All other parameters are optional and will use Paystack defaults if not + /// provided. + const CreatePlanOptions({ + required this.name, + required this.amount, + required this.interval, + // this.planCode, + this.description, + this.currency, + this.invoiceLimit, + this.sendInvoices, + this.sendSms, + }); + + /// Human-readable name for the plan. + /// + /// This will be displayed to customers when selecting subscription options. + /// Example: "Premium Monthly", "Basic Yearly", "Pro Plan" + final String name; + + /// Amount to charge for each billing cycle (in smallest currency unit). + /// + /// For NGN currency, this should be in kobo. + /// Example: 500000 for ₦5,000.00 + final int amount; + + /// Billing frequency interval. + /// + /// Supported values: "daily", "weekly", "monthly", "biannually", "annually" + /// Example: "monthly", "yearly" + final String interval; + + /// Unique plan code used for identification and API calls. + /// + /// If not provided, Paystack will auto-generate one. + /// Should be unique across your account. + /// Example: "premium_monthly", "basic_yearly" + // @MappableField(key: 'plan_code') + // final String? planCode; + + /// Optional description for the plan. + /// + /// Additional details about what the plan includes. + /// Example: "Access to premium features, priority support" + final String? description; + + /// Currency for the plan amount. + /// + /// Defaults to NGN if not specified. + /// Supported currencies depend on your Paystack account settings. + final String? currency; + + /// Maximum number of invoices to generate for subscriptions on this plan. + /// + /// After this limit, subscriptions will be marked as complete. + /// If not set, subscriptions will continue indefinitely. + @MappableField(key: 'invoice_limit') + final int? invoiceLimit; + + /// Whether to send email invoices to customers. + /// + /// Defaults to true if not specified. + @MappableField(key: 'send_invoices') + final bool? sendInvoices; + + /// Whether to send SMS notifications for invoices. + /// + /// Defaults to true if not specified. + @MappableField(key: 'send_sms') + final bool? sendSms; +} diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.mapper.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.mapper.dart new file mode 100644 index 0000000..a39356e --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/create_plan_options.mapper.dart @@ -0,0 +1,183 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'create_plan_options.dart'; + +class CreatePlanOptionsMapper extends ClassMapperBase { + CreatePlanOptionsMapper._(); + + static CreatePlanOptionsMapper? _instance; + static CreatePlanOptionsMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = CreatePlanOptionsMapper._()); + } + return _instance!; + } + + @override + final String id = 'CreatePlanOptions'; + + static String _$name(CreatePlanOptions v) => v.name; + static const Field _f$name = Field('name', _$name); + static int _$amount(CreatePlanOptions v) => v.amount; + static const Field _f$amount = + Field('amount', _$amount); + static String _$interval(CreatePlanOptions v) => v.interval; + static const Field _f$interval = + Field('interval', _$interval); + static String? _$description(CreatePlanOptions v) => v.description; + static const Field _f$description = + Field('description', _$description, opt: true); + static String? _$currency(CreatePlanOptions v) => v.currency; + static const Field _f$currency = + Field('currency', _$currency, opt: true); + static int? _$invoiceLimit(CreatePlanOptions v) => v.invoiceLimit; + static const Field _f$invoiceLimit = + Field('invoiceLimit', _$invoiceLimit, key: r'invoice_limit', opt: true); + static bool? _$sendInvoices(CreatePlanOptions v) => v.sendInvoices; + static const Field _f$sendInvoices = + Field('sendInvoices', _$sendInvoices, key: r'send_invoices', opt: true); + static bool? _$sendSms(CreatePlanOptions v) => v.sendSms; + static const Field _f$sendSms = + Field('sendSms', _$sendSms, key: r'send_sms', opt: true); + + @override + final MappableFields fields = const { + #name: _f$name, + #amount: _f$amount, + #interval: _f$interval, + #description: _f$description, + #currency: _f$currency, + #invoiceLimit: _f$invoiceLimit, + #sendInvoices: _f$sendInvoices, + #sendSms: _f$sendSms, + }; + + static CreatePlanOptions _instantiate(DecodingData data) { + return CreatePlanOptions( + name: data.dec(_f$name), + amount: data.dec(_f$amount), + interval: data.dec(_f$interval), + description: data.dec(_f$description), + currency: data.dec(_f$currency), + invoiceLimit: data.dec(_f$invoiceLimit), + sendInvoices: data.dec(_f$sendInvoices), + sendSms: data.dec(_f$sendSms)); + } + + @override + final Function instantiate = _instantiate; + + static CreatePlanOptions fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static CreatePlanOptions fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin CreatePlanOptionsMappable { + String toJson() { + return CreatePlanOptionsMapper.ensureInitialized() + .encodeJson(this as CreatePlanOptions); + } + + Map toMap() { + return CreatePlanOptionsMapper.ensureInitialized() + .encodeMap(this as CreatePlanOptions); + } + + CreatePlanOptionsCopyWith + get copyWith => + _CreatePlanOptionsCopyWithImpl( + this as CreatePlanOptions, $identity, $identity); + @override + String toString() { + return CreatePlanOptionsMapper.ensureInitialized() + .stringifyValue(this as CreatePlanOptions); + } + + @override + bool operator ==(Object other) { + return CreatePlanOptionsMapper.ensureInitialized() + .equalsValue(this as CreatePlanOptions, other); + } + + @override + int get hashCode { + return CreatePlanOptionsMapper.ensureInitialized() + .hashValue(this as CreatePlanOptions); + } +} + +extension CreatePlanOptionsValueCopy<$R, $Out> + on ObjectCopyWith<$R, CreatePlanOptions, $Out> { + CreatePlanOptionsCopyWith<$R, CreatePlanOptions, $Out> + get $asCreatePlanOptions => $base + .as((v, t, t2) => _CreatePlanOptionsCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class CreatePlanOptionsCopyWith<$R, $In extends CreatePlanOptions, + $Out> implements ClassCopyWith<$R, $In, $Out> { + $R call( + {String? name, + int? amount, + String? interval, + String? description, + String? currency, + int? invoiceLimit, + bool? sendInvoices, + bool? sendSms}); + CreatePlanOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _CreatePlanOptionsCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, CreatePlanOptions, $Out> + implements CreatePlanOptionsCopyWith<$R, CreatePlanOptions, $Out> { + _CreatePlanOptionsCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + CreatePlanOptionsMapper.ensureInitialized(); + @override + $R call( + {String? name, + int? amount, + String? interval, + Object? description = $none, + Object? currency = $none, + Object? invoiceLimit = $none, + Object? sendInvoices = $none, + Object? sendSms = $none}) => + $apply(FieldCopyWithData({ + if (name != null) #name: name, + if (amount != null) #amount: amount, + if (interval != null) #interval: interval, + if (description != $none) #description: description, + if (currency != $none) #currency: currency, + if (invoiceLimit != $none) #invoiceLimit: invoiceLimit, + if (sendInvoices != $none) #sendInvoices: sendInvoices, + if (sendSms != $none) #sendSms: sendSms + })); + @override + CreatePlanOptions $make(CopyWithData data) => CreatePlanOptions( + name: data.get(#name, or: $value.name), + amount: data.get(#amount, or: $value.amount), + interval: data.get(#interval, or: $value.interval), + description: data.get(#description, or: $value.description), + currency: data.get(#currency, or: $value.currency), + invoiceLimit: data.get(#invoiceLimit, or: $value.invoiceLimit), + sendInvoices: data.get(#sendInvoices, or: $value.sendInvoices), + sendSms: data.get(#sendSms, or: $value.sendSms)); + + @override + CreatePlanOptionsCopyWith<$R2, CreatePlanOptions, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _CreatePlanOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); +} From c7d20c8b678161981790fb9bd9c21ec2fefaa2a6 Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:14:54 +0100 Subject: [PATCH 5/6] feat: add UpdatePlanOptions model and service for updating subscription plans - Introduced UpdatePlanOptions model with fields for updating plan details such as name, amount, description, and more. - Implemented PlanService to handle business logic and error management for plan operations, including creation, listing, fetching, and updating plans. - Added PlanRepository for API interactions related to subscription plans, ensuring structured communication with Paystack's API. --- .../lib/src/core/models/src/split.mapper.dart | 32 ++-- .../lib/src/features/plan/models/models.dart | 3 + .../features/plan/models/options/options.dart | 3 + .../options/src/update_plan_options.dart | 77 ++++++++ .../src/update_plan_options.mapper.dart | 175 +++++++++++++++++ .../src/features/plan/models/plan/plan.dart | 1 + .../plan/repositories/plan_repository.dart | 150 +++++++++++++++ .../features/plan/services/plan_service.dart | 180 ++++++++++++++++++ 8 files changed, 605 insertions(+), 16 deletions(-) create mode 100644 packages/mind_paystack/lib/src/features/plan/models/models.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/options.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.mapper.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/models/plan/plan.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/repositories/plan_repository.dart create mode 100644 packages/mind_paystack/lib/src/features/plan/services/plan_service.dart diff --git a/packages/mind_paystack/lib/src/core/models/src/split.mapper.dart b/packages/mind_paystack/lib/src/core/models/src/split.mapper.dart index 346b690..ae7b88c 100644 --- a/packages/mind_paystack/lib/src/core/models/src/split.mapper.dart +++ b/packages/mind_paystack/lib/src/core/models/src/split.mapper.dart @@ -37,16 +37,16 @@ class SplitMapper extends ClassMapperBase { static String? _$bearerType(Split v) => v.bearerType; static const Field _f$bearerType = Field('bearerType', _$bearerType, key: r'bearer_type'); - static String? _$bearerSubaccount(Split v) => v.bearerSubaccount; - static const Field _f$bearerSubaccount = Field( - 'bearerSubaccount', _$bearerSubaccount, - key: r'bearer_subaccount', opt: true); static List? _$subaccounts(Split v) => v.subaccounts; static const Field> _f$subaccounts = Field('subaccounts', _$subaccounts); static int? _$totalSubaccounts(Split v) => v.totalSubaccounts; static const Field _f$totalSubaccounts = Field('totalSubaccounts', _$totalSubaccounts, key: r'total_subaccounts'); + static String? _$bearerSubaccount(Split v) => v.bearerSubaccount; + static const Field _f$bearerSubaccount = Field( + 'bearerSubaccount', _$bearerSubaccount, + key: r'bearer_subaccount', opt: true); @override final MappableFields fields = const { @@ -57,9 +57,9 @@ class SplitMapper extends ClassMapperBase { #splitCode: _f$splitCode, #active: _f$active, #bearerType: _f$bearerType, - #bearerSubaccount: _f$bearerSubaccount, #subaccounts: _f$subaccounts, #totalSubaccounts: _f$totalSubaccounts, + #bearerSubaccount: _f$bearerSubaccount, }; static Split _instantiate(DecodingData data) { @@ -71,9 +71,9 @@ class SplitMapper extends ClassMapperBase { splitCode: data.dec(_f$splitCode), active: data.dec(_f$active), bearerType: data.dec(_f$bearerType), - bearerSubaccount: data.dec(_f$bearerSubaccount), subaccounts: data.dec(_f$subaccounts), - totalSubaccounts: data.dec(_f$totalSubaccounts)); + totalSubaccounts: data.dec(_f$totalSubaccounts), + bearerSubaccount: data.dec(_f$bearerSubaccount)); } @override @@ -133,9 +133,9 @@ abstract class SplitCopyWith<$R, $In extends Split, $Out> String? splitCode, bool? active, String? bearerType, - String? bearerSubaccount, List? subaccounts, - int? totalSubaccounts}); + int? totalSubaccounts, + String? bearerSubaccount}); SplitCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -161,9 +161,9 @@ class _SplitCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Split, $Out> Object? splitCode = $none, Object? active = $none, Object? bearerType = $none, - Object? bearerSubaccount = $none, Object? subaccounts = $none, - Object? totalSubaccounts = $none}) => + Object? totalSubaccounts = $none, + Object? bearerSubaccount = $none}) => $apply(FieldCopyWithData({ if (id != $none) #id: id, if (name != $none) #name: name, @@ -172,9 +172,9 @@ class _SplitCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Split, $Out> if (splitCode != $none) #splitCode: splitCode, if (active != $none) #active: active, if (bearerType != $none) #bearerType: bearerType, - if (bearerSubaccount != $none) #bearerSubaccount: bearerSubaccount, if (subaccounts != $none) #subaccounts: subaccounts, - if (totalSubaccounts != $none) #totalSubaccounts: totalSubaccounts + if (totalSubaccounts != $none) #totalSubaccounts: totalSubaccounts, + if (bearerSubaccount != $none) #bearerSubaccount: bearerSubaccount })); @override Split $make(CopyWithData data) => Split( @@ -185,11 +185,11 @@ class _SplitCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Split, $Out> splitCode: data.get(#splitCode, or: $value.splitCode), active: data.get(#active, or: $value.active), bearerType: data.get(#bearerType, or: $value.bearerType), - bearerSubaccount: - data.get(#bearerSubaccount, or: $value.bearerSubaccount), subaccounts: data.get(#subaccounts, or: $value.subaccounts), totalSubaccounts: - data.get(#totalSubaccounts, or: $value.totalSubaccounts)); + data.get(#totalSubaccounts, or: $value.totalSubaccounts), + bearerSubaccount: + data.get(#bearerSubaccount, or: $value.bearerSubaccount)); @override SplitCopyWith<$R2, Split, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t) => diff --git a/packages/mind_paystack/lib/src/features/plan/models/models.dart b/packages/mind_paystack/lib/src/features/plan/models/models.dart new file mode 100644 index 0000000..20844c5 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/models.dart @@ -0,0 +1,3 @@ +export 'package:mind_paystack/src/core/models/src/plan.dart'; +export 'options/options.dart'; +export 'plan/plan.dart'; \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/options.dart b/packages/mind_paystack/lib/src/features/plan/models/options/options.dart new file mode 100644 index 0000000..35323b8 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/options.dart @@ -0,0 +1,3 @@ +export 'src/create_plan_options.dart'; +export 'src/list_plans_options.dart'; +export 'src/update_plan_options.dart'; \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.dart new file mode 100644 index 0000000..14e6143 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.dart @@ -0,0 +1,77 @@ +import 'package:dart_mappable/dart_mappable.dart'; +part 'update_plan_options.mapper.dart'; + +/// Options for updating an existing subscription plan. +/// +/// This class provides the updatable parameters for modifying +/// a subscription plan on Paystack. Only the fields you want to update +/// need to be provided - all parameters are optional. +/// +/// Note: Some fields like interval may not be updateable depending on +/// Paystack's API restrictions and existing subscriptions on the plan. +/// +/// Example usage: +/// ```dart +/// final updateOptions = UpdatePlanOptions( +/// name: 'Premium Monthly (Updated)', +/// amount: 600000, // ₦6,000 in kobo +/// description: 'Updated premium features', +/// ); +/// ``` +@MappableClass() +class UpdatePlanOptions with UpdatePlanOptionsMappable { + /// Creates a new UpdatePlanOptions instance. + /// + /// All parameters are optional. Only provide the fields you want to update. + const UpdatePlanOptions({ + this.name, + this.amount, + this.description, + this.currency, + this.invoiceLimit, + this.sendInvoices, + this.sendSms, + }); + + /// Updated human-readable name for the plan. + /// + /// This will be displayed to customers when selecting subscription options. + /// Example: "Premium Monthly (Updated)", "Enhanced Pro Plan" + final String? name; + + /// Updated amount to charge for each billing cycle (in smallest currency unit). + /// + /// For NGN currency, this should be in kobo. + /// Example: 600000 for ₦6,000.00 + /// + /// Note: Changing plan amount may affect existing subscriptions differently + /// depending on Paystack's policies. + final int? amount; + + /// Updated description for the plan. + /// + /// Additional details about what the plan includes. + /// Example: "Enhanced features, 24/7 support, priority processing" + final String? description; + + /// Updated currency for the plan amount. + /// + /// Note: Currency changes may not be allowed for plans with active subscriptions. + /// Supported currencies depend on your Paystack account settings. + final String? currency; + + /// Updated maximum number of invoices for subscriptions on this plan. + /// + /// After this limit, subscriptions will be marked as complete. + /// Set to null to allow unlimited invoices. + @MappableField(key: 'invoice_limit') + final int? invoiceLimit; + + /// Updated preference for sending email invoices to customers. + @MappableField(key: 'send_invoices') + final bool? sendInvoices; + + /// Updated preference for sending SMS notifications for invoices. + @MappableField(key: 'send_sms') + final bool? sendSms; +} \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.mapper.dart b/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.mapper.dart new file mode 100644 index 0000000..14fd0a1 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/options/src/update_plan_options.mapper.dart @@ -0,0 +1,175 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'update_plan_options.dart'; + +class UpdatePlanOptionsMapper extends ClassMapperBase { + UpdatePlanOptionsMapper._(); + + static UpdatePlanOptionsMapper? _instance; + static UpdatePlanOptionsMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = UpdatePlanOptionsMapper._()); + } + return _instance!; + } + + @override + final String id = 'UpdatePlanOptions'; + + static String? _$name(UpdatePlanOptions v) => v.name; + static const Field _f$name = + Field('name', _$name, opt: true); + static int? _$amount(UpdatePlanOptions v) => v.amount; + static const Field _f$amount = + Field('amount', _$amount, opt: true); + static String? _$description(UpdatePlanOptions v) => v.description; + static const Field _f$description = + Field('description', _$description, opt: true); + static String? _$currency(UpdatePlanOptions v) => v.currency; + static const Field _f$currency = + Field('currency', _$currency, opt: true); + static int? _$invoiceLimit(UpdatePlanOptions v) => v.invoiceLimit; + static const Field _f$invoiceLimit = + Field('invoiceLimit', _$invoiceLimit, key: r'invoice_limit', opt: true); + static bool? _$sendInvoices(UpdatePlanOptions v) => v.sendInvoices; + static const Field _f$sendInvoices = + Field('sendInvoices', _$sendInvoices, key: r'send_invoices', opt: true); + static bool? _$sendSms(UpdatePlanOptions v) => v.sendSms; + static const Field _f$sendSms = + Field('sendSms', _$sendSms, key: r'send_sms', opt: true); + + @override + final MappableFields fields = const { + #name: _f$name, + #amount: _f$amount, + #description: _f$description, + #currency: _f$currency, + #invoiceLimit: _f$invoiceLimit, + #sendInvoices: _f$sendInvoices, + #sendSms: _f$sendSms, + }; + + static UpdatePlanOptions _instantiate(DecodingData data) { + return UpdatePlanOptions( + name: data.dec(_f$name), + amount: data.dec(_f$amount), + description: data.dec(_f$description), + currency: data.dec(_f$currency), + invoiceLimit: data.dec(_f$invoiceLimit), + sendInvoices: data.dec(_f$sendInvoices), + sendSms: data.dec(_f$sendSms)); + } + + @override + final Function instantiate = _instantiate; + + static UpdatePlanOptions fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static UpdatePlanOptions fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin UpdatePlanOptionsMappable { + String toJson() { + return UpdatePlanOptionsMapper.ensureInitialized() + .encodeJson(this as UpdatePlanOptions); + } + + Map toMap() { + return UpdatePlanOptionsMapper.ensureInitialized() + .encodeMap(this as UpdatePlanOptions); + } + + UpdatePlanOptionsCopyWith + get copyWith => + _UpdatePlanOptionsCopyWithImpl( + this as UpdatePlanOptions, $identity, $identity); + @override + String toString() { + return UpdatePlanOptionsMapper.ensureInitialized() + .stringifyValue(this as UpdatePlanOptions); + } + + @override + bool operator ==(Object other) { + return UpdatePlanOptionsMapper.ensureInitialized() + .equalsValue(this as UpdatePlanOptions, other); + } + + @override + int get hashCode { + return UpdatePlanOptionsMapper.ensureInitialized() + .hashValue(this as UpdatePlanOptions); + } +} + +extension UpdatePlanOptionsValueCopy<$R, $Out> + on ObjectCopyWith<$R, UpdatePlanOptions, $Out> { + UpdatePlanOptionsCopyWith<$R, UpdatePlanOptions, $Out> + get $asUpdatePlanOptions => $base + .as((v, t, t2) => _UpdatePlanOptionsCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class UpdatePlanOptionsCopyWith<$R, $In extends UpdatePlanOptions, + $Out> implements ClassCopyWith<$R, $In, $Out> { + $R call( + {String? name, + int? amount, + String? description, + String? currency, + int? invoiceLimit, + bool? sendInvoices, + bool? sendSms}); + UpdatePlanOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _UpdatePlanOptionsCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, UpdatePlanOptions, $Out> + implements UpdatePlanOptionsCopyWith<$R, UpdatePlanOptions, $Out> { + _UpdatePlanOptionsCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + UpdatePlanOptionsMapper.ensureInitialized(); + @override + $R call( + {Object? name = $none, + Object? amount = $none, + Object? description = $none, + Object? currency = $none, + Object? invoiceLimit = $none, + Object? sendInvoices = $none, + Object? sendSms = $none}) => + $apply(FieldCopyWithData({ + if (name != $none) #name: name, + if (amount != $none) #amount: amount, + if (description != $none) #description: description, + if (currency != $none) #currency: currency, + if (invoiceLimit != $none) #invoiceLimit: invoiceLimit, + if (sendInvoices != $none) #sendInvoices: sendInvoices, + if (sendSms != $none) #sendSms: sendSms + })); + @override + UpdatePlanOptions $make(CopyWithData data) => UpdatePlanOptions( + name: data.get(#name, or: $value.name), + amount: data.get(#amount, or: $value.amount), + description: data.get(#description, or: $value.description), + currency: data.get(#currency, or: $value.currency), + invoiceLimit: data.get(#invoiceLimit, or: $value.invoiceLimit), + sendInvoices: data.get(#sendInvoices, or: $value.sendInvoices), + sendSms: data.get(#sendSms, or: $value.sendSms)); + + @override + UpdatePlanOptionsCopyWith<$R2, UpdatePlanOptions, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _UpdatePlanOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); +} diff --git a/packages/mind_paystack/lib/src/features/plan/models/plan/plan.dart b/packages/mind_paystack/lib/src/features/plan/models/plan/plan.dart new file mode 100644 index 0000000..40ead06 --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/models/plan/plan.dart @@ -0,0 +1 @@ +export 'src/plan_list.dart'; \ No newline at end of file diff --git a/packages/mind_paystack/lib/src/features/plan/repositories/plan_repository.dart b/packages/mind_paystack/lib/src/features/plan/repositories/plan_repository.dart new file mode 100644 index 0000000..db1bdae --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/repositories/plan_repository.dart @@ -0,0 +1,150 @@ +import 'package:mind_paystack/src/core/errors/models/mind_exception.dart'; +import 'package:mind_paystack/src/core/models/resource.dart'; +import 'package:mind_paystack/src/core/network/http_client.dart'; +import 'package:mind_paystack/src/features/plan/models/models.dart'; + +/// Repository for handling all plan-related API operations. +/// +/// This repository provides a comprehensive interface for interacting with +/// Paystack's plan APIs, including creation, listing, fetching, and updating +/// of subscription plans. +/// +/// The repository handles HTTP communication with the Paystack API and +/// transforms raw responses into typed model objects wrapped in [Resource] +/// containers for consistent error handling. +/// +/// Example usage: +/// ```dart +/// final repository = PlanRepository(httpClient); +/// +/// // Create a new plan +/// final createResult = await repository.create( +/// CreatePlanOptions( +/// name: 'Premium Monthly', +/// amount: 500000, // ₦5,000 in kobo +/// interval: 'monthly', +/// ), +/// ); +/// +/// // List all plans +/// final listResult = await repository.list(); +/// ``` +class PlanRepository { + /// Creates a new PlanRepository instance. + /// + /// The [_httpClient] is used for all HTTP communication with the + /// Paystack API and should be properly configured with authentication. + PlanRepository(this._httpClient); + + /// HTTP client used for API communication. + final HttpClient _httpClient; + + /// Creates a new subscription plan. + /// + /// Sends a POST request to Paystack's plan creation endpoint with the + /// specified plan details. The plan will be available for creating + /// subscriptions once successfully created. + /// + /// Parameters: + /// - [options]: Configuration for the new plan including name, amount, + /// interval, and other plan details + /// + /// Returns: + /// A Resource containing Plan data with the created plan details including + /// the generated plan code and ID. + /// + /// Throws: + /// - [MindException] if the request fails, validation fails, or the API + /// returns an error response + /// + /// API Endpoint: `POST /plan` + Future> create(CreatePlanOptions options) async { + final res = await _httpClient.post>( + '/plan', + data: options.toMap(), + ); + return Resource.fromMap( + res.data!, + PlanMapper.fromMap, + ); + } + + /// Retrieves a list of subscription plans. + /// + /// Sends a GET request to Paystack's plan listing endpoint with optional + /// filtering and pagination parameters. Returns plans that match the + /// specified criteria. + /// + /// Parameters: + /// - [options]: Optional filtering and pagination parameters + /// + /// Returns: + /// A Resource containing PlanList with plans and pagination metadata. + /// + /// API Endpoint: `GET /plan` + Future>> list([ListPlansOptions? options]) async { + final queryParams = options?.toMap() ?? {}; + + final res = await _httpClient.get>( + '/plan', + queryParameters: queryParams, + ); + return Resource.fromMapList( + res.data!, + (data) => data.map(PlanMapper.fromMap).toList(), + ); + } + + /// Fetches a specific subscription plan by its ID or plan code. + /// + /// Sends a GET request to retrieve detailed information about a specific + /// plan. This is useful for displaying plan details before subscription + /// or for administrative purposes. + /// + /// Parameters: + /// - [planIdOrCode]: The plan ID or plan code to fetch + /// + /// Returns: + /// A Resource containing Plan data with the complete plan details. + /// + /// API Endpoint: `GET /plan/{id_or_code}` + Future> fetch(String planIdOrCode) async { + final res = await _httpClient.get>( + '/plan/$planIdOrCode', + ); + return Resource.fromMap( + res.data!, + PlanMapper.fromMap, + ); + } + + /// Updates an existing subscription plan. + /// + /// Sends a PUT request to Paystack's plan update endpoint with the + /// updated plan details. Only the provided fields will be updated. + /// + /// Parameters: + /// - [planIdOrCode]: The plan ID or plan code to update + /// - [options]: The fields to update + /// + /// Returns: + /// A Resource containing Plan data with the updated plan details. + /// + /// Note: Some fields like interval may not be updateable depending on + /// Paystack's API restrictions and existing subscriptions on the plan. + /// + /// API Endpoint: `PUT /plan/{id_or_code}` + Future> update( + String planIdOrCode, + UpdatePlanOptions options, + ) async { + final res = await _httpClient.put>( + '/plan/$planIdOrCode', + data: options.toMap(), + ); + return Resource.fromMap( + res.data!, + PlanMapper.fromMap, + ); + } +} diff --git a/packages/mind_paystack/lib/src/features/plan/services/plan_service.dart b/packages/mind_paystack/lib/src/features/plan/services/plan_service.dart new file mode 100644 index 0000000..9cdc85b --- /dev/null +++ b/packages/mind_paystack/lib/src/features/plan/services/plan_service.dart @@ -0,0 +1,180 @@ +import 'package:dio/dio.dart'; +import 'package:mind_paystack/src/core/errors/models/mind_exception.dart'; +import 'package:mind_paystack/src/core/models/resource.dart'; +import 'package:mind_paystack/src/features/plan/interfaces/i_plan_service.dart'; +import 'package:mind_paystack/src/features/plan/models/models.dart'; +import 'package:mind_paystack/src/features/plan/repositories/plan_repository.dart'; + +/// Service implementation for plan-related business operations with proper +/// exception handling. +/// +/// This service provides a high-level interface for plan management operations, +/// implementing business logic validation and error handling on top of the +/// repository layer. It serves as the main entry point for plan operations +/// throughout the application. +/// +/// The service delegates to [PlanRepository] for API communication while +/// adding validation, error handling, and business rule enforcement. +/// +/// All methods throw [MindException] with proper error codes and messages +/// for consistent error handling across the application. +/// +/// Example usage: +/// ```dart +/// final planService = PlanService(planRepository); +/// +/// // Create a new plan +/// final createResult = await planService.create( +/// CreatePlanOptions( +/// name: 'Premium Monthly', +/// amount: 500000, +/// interval: 'monthly', +/// ), +/// ); +/// +/// if (createResult.isSuccess) { +/// print('Plan created: ${createResult.data!.planCode}'); +/// } +/// ``` +class PlanService implements IPlanService { + /// Creates a new PlanService instance. + /// + /// The [_repository] is used for all data access operations and should be + /// properly configured with authentication and network settings. + PlanService(this._repository); + + /// Repository for handling plan data access operations. + final PlanRepository _repository; + + @override + Future> create(CreatePlanOptions options) async { + try { + // Add business logic validation + if (options.name.trim().isEmpty) { + throw MindException.validation( + message: 'Plan name cannot be empty', + errors: [], + ); + } + + if (options.amount <= 0) { + throw MindException.validation( + message: 'Plan amount must be greater than zero', + errors: [], + ); + } + + return await _repository.create(options); + } on DioException catch (e) { + throw MindException.fromDioError(e); + } catch (e) { + if (e is MindException) rethrow; + + throw MindException( + message: 'Failed to create plan', + code: 'plan_creation_error', + technicalMessage: e.toString(), + ); + } + } + + @override + Future>> list([ListPlansOptions? options]) async { + try { + // Add business logic validation + if (options?.perPage != null && options!.perPage! <= 0) { + throw MindException.validation( + message: 'Items per page must be greater than zero', + errors: [], + ); + } + + if (options?.page != null && options!.page! <= 0) { + throw MindException.validation( + message: 'Page number must be greater than zero', + errors: [], + ); + } + + return await _repository.list(options); + } on DioException catch (e) { + throw MindException.fromDioError(e); + } catch (e, s) { + print(s); + if (e is MindException) rethrow; + + throw MindException( + message: 'Failed to list plans', + code: 'plan_list_error', + technicalMessage: e.toString(), + ); + } + } + + @override + Future> fetch(String planIdOrCode) async { + try { + // Add business logic validation + if (planIdOrCode.trim().isEmpty) { + throw MindException.validation( + message: 'Plan ID or code cannot be empty', + errors: [], + ); + } + + return await _repository.fetch(planIdOrCode); + } on DioException catch (e) { + throw MindException.fromDioError(e); + } catch (e) { + if (e is MindException) rethrow; + + throw MindException( + message: 'Failed to fetch plan', + code: 'plan_fetch_error', + technicalMessage: e.toString(), + ); + } + } + + @override + Future> update( + String planIdOrCode, + UpdatePlanOptions options, + ) async { + try { + // Add business logic validation + if (planIdOrCode.trim().isEmpty) { + throw MindException.validation( + message: 'Plan ID or code cannot be empty', + errors: [], + ); + } + + if (options.name?.trim().isEmpty ?? false) { + throw MindException.validation( + message: 'Plan name cannot be empty', + errors: [], + ); + } + + if (options.amount != null && options.amount! <= 0) { + throw MindException.validation( + message: 'Plan amount must be greater than zero', + errors: [], + ); + } + + return await _repository.update(planIdOrCode, options); + } on DioException catch (e) { + throw MindException.fromDioError(e); + } catch (e) { + if (e is MindException) rethrow; + + throw MindException( + message: 'Failed to update plan', + code: 'plan_update_error', + technicalMessage: e.toString(), + ); + } + } +} From 3ee6adccd008e00ff8a45ef01095ad2926a609ec Mon Sep 17 00:00:00 2001 From: Ayotomide Babalola Date: Thu, 28 Aug 2025 20:15:32 +0100 Subject: [PATCH 6/6] feat: add initial implementation of plan management demo application - Introduced a new demo application showcasing subscription plan management using the MindPaystack SDK. - Added core functionalities including creating, listing, fetching, and updating subscription plans. - Implemented environment variable handling for SDK initialization. - Created a user-friendly interactive menu for plan operations. - Included comprehensive README documentation for setup and usage instructions. - Added basic test structure for future test implementations. --- apps/ex05_plan_demo/README.md | 109 ++++ apps/ex05_plan_demo/bin/ex05_plan_demo.dart | 40 ++ apps/ex05_plan_demo/lib/ex05_plan_demo.dart | 212 ++++++++ apps/ex05_plan_demo/pubspec.lock | 508 ++++++++++++++++++ apps/ex05_plan_demo/pubspec.yaml | 17 + .../test/ex05_plan_demo_test.dart | 11 + 6 files changed, 897 insertions(+) create mode 100644 apps/ex05_plan_demo/README.md create mode 100644 apps/ex05_plan_demo/bin/ex05_plan_demo.dart create mode 100644 apps/ex05_plan_demo/lib/ex05_plan_demo.dart create mode 100644 apps/ex05_plan_demo/pubspec.lock create mode 100644 apps/ex05_plan_demo/pubspec.yaml create mode 100644 apps/ex05_plan_demo/test/ex05_plan_demo_test.dart diff --git a/apps/ex05_plan_demo/README.md b/apps/ex05_plan_demo/README.md new file mode 100644 index 0000000..64e9900 --- /dev/null +++ b/apps/ex05_plan_demo/README.md @@ -0,0 +1,109 @@ +# Plan Management Demo + +A comprehensive example demonstrating subscription plan management using the MindPaystack SDK. + +## Features Demonstrated + +- **Create Plan**: Create new subscription plans with various billing intervals +- **List Plans**: Retrieve and display all available subscription plans +- **Fetch Plan**: Get detailed information about a specific plan +- **Update Plan**: Modify existing plan details like name and amount + +## Setup + +1. **Environment Variables** (Recommended): + ```bash + export PAYSTACK_PUBLIC_KEY=pk_test_your_public_key + export PAYSTACK_SECRET_KEY=sk_test_your_secret_key + ``` + +2. **Install Dependencies**: + ```bash + dart pub get + ``` + +## Usage + +### Interactive Mode +Run the demo with an interactive menu: +```bash +dart run bin/ex05_plan_demo.dart +``` + +### Complete Demo +Run an automated demonstration of all features: +```bash +dart run bin/ex05_plan_demo.dart --complete +``` + +## Example Plan Operations + +### Creating a Plan +```dart +final createResult = await sdk.plan.create( + CreatePlanOptions( + name: 'Premium Monthly', + amount: 500000, // ₦5,000 in kobo + interval: 'monthly', + planCode: 'premium_monthly', + description: 'Premium features with monthly billing', + currency: 'NGN', + ), +); +``` + +### Listing Plans +```dart +final listResult = await sdk.plan.list( + ListPlansOptions( + perPage: 10, + page: 1, + status: 'active', + ), +); +``` + +### Fetching a Specific Plan +```dart +final fetchResult = await sdk.plan.fetch('premium_monthly'); +``` + +### Updating a Plan +```dart +final updateResult = await sdk.plan.update( + 'premium_monthly', + UpdatePlanOptions( + name: 'Premium Monthly (Updated)', + amount: 600000, // ₦6,000 in kobo + ), +); +``` + +## Plan Properties + +- **name**: Human-readable plan name +- **amount**: Billing amount in kobo (smallest currency unit) +- **interval**: Billing frequency (`daily`, `weekly`, `monthly`, `biannually`, `annually`) +- **planCode**: Unique identifier for the plan +- **description**: Optional plan description +- **currency**: Plan currency (defaults to NGN) +- **invoiceLimit**: Maximum number of invoices (optional) +- **sendInvoices**: Whether to send email invoices +- **sendSms**: Whether to send SMS notifications + +## Error Handling + +The demo includes comprehensive error handling for common scenarios: +- Network connectivity issues +- Invalid API credentials +- Missing required parameters +- Plan not found errors +- Validation failures + +## Next Steps + +After running this demo, you can: +1. Create plans for your subscription tiers +2. Use these plans in subscription creation +3. Implement plan selection in your UI +4. Set up subscription management workflows \ No newline at end of file diff --git a/apps/ex05_plan_demo/bin/ex05_plan_demo.dart b/apps/ex05_plan_demo/bin/ex05_plan_demo.dart new file mode 100644 index 0000000..fa1fa22 --- /dev/null +++ b/apps/ex05_plan_demo/bin/ex05_plan_demo.dart @@ -0,0 +1,40 @@ +import 'dart:io'; +import 'package:ex05_plan_demo/ex05_plan_demo.dart'; + +/// Plan Management Demo Application +/// +/// This demo application showcases the plan management features of the +/// MindPaystack SDK including creating, listing, fetching, and updating +/// subscription plans. +/// +/// Usage: +/// 1. Set environment variables (optional): +/// - PAYSTACK_PUBLIC_KEY=pk_test_your_key +/// - PAYSTACK_SECRET_KEY=sk_test_your_key +/// +/// 2. Run the demo: +/// dart run bin/ex05_plan_demo.dart +/// +/// 3. Choose from interactive mode or complete demo +void main(List arguments) async { + print('🎯 MindPaystack Plan Management Demo'); + print('====================================='); + + try { + // Initialize the SDK + await PlanDemo.initializeSdk(); + + if (arguments.contains('--complete') || arguments.contains('-c')) { + // Run complete automated demo + await PlanDemo.runCompleteDemo(); + } else { + // Run interactive menu + print('\n💡 Tip: Use --complete flag to run automated demo'); + await PlanDemo.showMenu(); + } + + } catch (e) { + print('❌ Demo failed: $e'); + exit(1); + } +} \ No newline at end of file diff --git a/apps/ex05_plan_demo/lib/ex05_plan_demo.dart b/apps/ex05_plan_demo/lib/ex05_plan_demo.dart new file mode 100644 index 0000000..a96bae2 --- /dev/null +++ b/apps/ex05_plan_demo/lib/ex05_plan_demo.dart @@ -0,0 +1,212 @@ +import 'dart:io'; +import 'package:dotenv/dotenv.dart'; +import 'package:mind_paystack/mind_paystack.dart'; + +/// Demonstrates plan management operations using the MindPaystack SDK +class PlanDemo { + /// Initialize the SDK with environment variables or test keys + static Future initializeSdk() async { + try { + // Try to initialize from environment variables first + await MindPaystack.fromEnvironment(); + print('✅ SDK initialized from environment variables'); + } catch (e) { + final env = DotEnv(includePlatformEnvironment: true)..load(); + final secretKey = env['PAYSTACK_SECRET_KEY']; + final publicKey = env['PAYSTACK_PUBLIC_KEY']; + + // Fallback to test configuration + await MindPaystack.initialize( + PaystackConfig( + publicKey: publicKey!, + secretKey: secretKey!, + environment: Environment.test, + ), + ); + print('✅ SDK initialized with test configuration'); + print( + '💡 Set PAYSTACK_PUBLIC_KEY and PAYSTACK_SECRET_KEY environment variables for automatic configuration'); + } + } + + /// Demonstrates creating a new subscription plan + static Future createPlan() async { + print('\n🔄 Creating a new subscription plan...'); + + final sdk = MindPaystack.instance; + + final createResult = await sdk.plan.create( + CreatePlanOptions( + name: 'Premium Monthly Plan', + amount: 500000, // ₦5,000 in kobo + interval: 'monthly', + // planCode: 'premium_monthly_${DateTime.now().millisecondsSinceEpoch}', + description: 'Premium features with monthly billing', + currency: 'NGN', + invoiceLimit: 12, // 12 months + sendInvoices: true, + sendSms: true, + ), + ); + + if (createResult.isSuccess) { + final plan = createResult.data!; + print('✅ Plan created successfully!'); + print(' Plan Code: ${plan.planCode}'); + print(' Plan ID: ${plan.id}'); + print(' Name: ${plan.name}'); + print(' Amount: ₦${(plan.amount! / 100).toStringAsFixed(2)}'); + print(' Interval: ${plan.interval}'); + return plan; + } else { + print('❌ Plan creation failed: ${createResult.message}'); + return null; + } + } + + /// Demonstrates listing subscription plans + static Future listPlans() async { + print('\n🔄 Fetching subscription plans...'); + + final sdk = MindPaystack.instance; + + final listResult = await sdk.plan.list( + ListPlansOptions( + perPage: 10, + page: 1, + status: 'active', + ), + ); + + if (listResult.isSuccess) { + final planList = listResult.data!; + print('✅ Found ${planList.length} plans (${listResult.meta} total)'); + + if (listResult.data?.isEmpty ?? false) { + print(' No plans found. Create one first!'); + return; + } + + print(' Available Plans:'); + for (int i = 0; i < planList.length; i++) { + final plan = planList[i]; + print(' ${i + 1}. ${plan.name}'); + print(' Code: ${plan.planCode}'); + print( + ' Amount: ₦${(plan.amount! / 100).toStringAsFixed(2)} per ${plan.interval}'); + } + } else { + print('❌ Plan listing failed: ${listResult.message}'); + } + } + + /// Demonstrates fetching a specific plan + static Future fetchPlan(String planIdOrCode) async { + print('\n🔄 Fetching plan: $planIdOrCode'); + + final sdk = MindPaystack.instance; + + final fetchResult = await sdk.plan.fetch(planIdOrCode); + + if (fetchResult.isSuccess) { + final plan = fetchResult.data!; + print('✅ Plan fetched successfully!'); + print(' Name: ${plan.name}'); + print(' Code: ${plan.planCode}'); + print(' Amount: ₦${(plan.amount! / 100).toStringAsFixed(2)}'); + print(' Interval: ${plan.interval}'); + } else { + print('❌ Plan fetch failed: ${fetchResult.message}'); + } + } + + /// Demonstrates updating a subscription plan + static Future updatePlan(String planIdOrCode) async { + print('\n🔄 Updating plan: $planIdOrCode'); + + final sdk = MindPaystack.instance; + + final updateResult = await sdk.plan.update( + planIdOrCode, + UpdatePlanOptions( + name: 'Premium Monthly Plan (Updated)', + amount: 600000, // ₦6,000 in kobo + description: 'Enhanced premium features with monthly billing', + ), + ); + + if (updateResult.isSuccess) { + final updatedPlan = updateResult.data!; + print('✅ Plan updated successfully!'); + print(' New Name: ${updatedPlan.name}'); + print( + ' New Amount: ₦${(updatedPlan.amount! / 100).toStringAsFixed(2)}'); + } else { + print('❌ Plan update failed: ${updateResult.message}'); + } + } + + /// Interactive menu for plan operations + static Future showMenu() async { + while (true) { + print('\n📋 Plan Management Demo'); + print('1. Create a new plan'); + print('2. List all plans'); + print('3. Fetch a specific plan'); + print('4. Update a plan'); + print('5. Exit'); + print(''); + stdout.write('Choose an option (1-5): '); + + final choice = stdin.readLineSync(); + + switch (choice) { + case '1': + await createPlan(); + break; + case '2': + await listPlans(); + break; + case '3': + stdout.write('Enter plan ID or code: '); + final planId = stdin.readLineSync(); + if (planId?.isNotEmpty == true) { + await fetchPlan(planId!); + } + break; + case '4': + stdout.write('Enter plan ID or code to update: '); + final planId = stdin.readLineSync(); + if (planId?.isNotEmpty == true) { + await updatePlan(planId!); + } + break; + case '5': + print('👋 Goodbye!'); + return; + default: + print('❌ Invalid option. Please choose 1-5.'); + } + } + } + + /// Runs a complete demo of all plan operations + static Future runCompleteDemo() async { + print('🚀 Running complete plan management demo...'); + + // Create a plan + final createdPlan = await createPlan(); + if (createdPlan == null) return; + + // List plans + await listPlans(); + + // Fetch the created plan + await fetchPlan(createdPlan.planCode!); + + // Update the plan + await updatePlan(createdPlan.planCode!); + + print('\n✅ Complete demo finished!'); + } +} diff --git a/apps/ex05_plan_demo/pubspec.lock b/apps/ex05_plan_demo/pubspec.lock new file mode 100644 index 0000000..d24e7f8 --- /dev/null +++ b/apps/ex05_plan_demo/pubspec.lock @@ -0,0 +1,508 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a + url: "https://pub.dev" + source: hosted + version: "88.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + dart_mappable: + dependency: transitive + description: + name: dart_mappable + sha256: "15f41a35da8ee690bbfa0059fa241edeeaea73f89a2ba685b354ece07cd8ada6" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + dio: + dependency: transitive + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + dotenv: + dependency: "direct main" + description: + name: dotenv + sha256: "379e64b6fc82d3df29461d349a1796ecd2c436c480d4653f3af6872eccbc90e1" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: transitive + description: + name: get_it + sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + url: "https://pub.dev" + source: hosted + version: "8.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + injectable: + dependency: transitive + description: + name: injectable + sha256: "1b86fab6a98c11a97e5c718afb00e628d47d183c2a2256392e995a4c561141c1" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + logger: + dependency: transitive + description: + name: logger + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mind_paystack: + dependency: "direct main" + description: + path: "../../packages/mind_paystack" + relative: true + source: path + version: "0.1.1+1" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + type_plus: + dependency: transitive + description: + name: type_plus + sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" diff --git a/apps/ex05_plan_demo/pubspec.yaml b/apps/ex05_plan_demo/pubspec.yaml new file mode 100644 index 0000000..dbf00e7 --- /dev/null +++ b/apps/ex05_plan_demo/pubspec.yaml @@ -0,0 +1,17 @@ +name: ex05_plan_demo +description: A sample application demonstrating plan management with mind-paystack. +publish_to: none + +version: 1.0.0+1 + +environment: + sdk: ^3.5.0 + +dependencies: + mind_paystack: + path: ../../packages/mind_paystack + dotenv: ^4.2.0 + +dev_dependencies: + test: ^1.25.8 + very_good_analysis: ^7.0.0 \ No newline at end of file diff --git a/apps/ex05_plan_demo/test/ex05_plan_demo_test.dart b/apps/ex05_plan_demo/test/ex05_plan_demo_test.dart new file mode 100644 index 0000000..9dda73d --- /dev/null +++ b/apps/ex05_plan_demo/test/ex05_plan_demo_test.dart @@ -0,0 +1,11 @@ +import 'package:test/test.dart'; + +void main() { + group('Plan Demo Tests', () { + test('demo app structure exists', () { + // Basic test to verify the demo structure + // In a real scenario, you'd test the actual plan operations + expect(true, isTrue); + }); + }); +} \ No newline at end of file