Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Fundraising Campaigns (GSoC) #2635

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Count lines of code in each file
run: chmod +x ./.github/workflows/countline.py
- name: Running count lines
run: ./.github/workflows/countline.py --exclude_directories test/ --exclude_files lib/custom_painters/talawa_logo.dart lib/custom_painters/language_icon.dart lib/custom_painters/whatsapp_logo.dart lib/utils/queries.dart lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart lib/view_model/pre_auth_view_models/select_organization_view_model.dart lib/views/after_auth_screens/profile/profile_page.dart lib/view_model/main_screen_view_model.dart lib/views/after_auth_screens/events/create_event_page.dart lib/views/after_auth_screens/org_info_screen.dart lib/views/after_auth_screens/events/manage_volunteer_group.dart lib/views/after_auth_screens/events/create_agenda_item_page.dart lib/views/after_auth_screens/events/edit_agenda_item_page.dart lib/utils/event_queries.dart
run: ./.github/workflows/countline.py --exclude_directories test/ --exclude_files lib/custom_painters/talawa_logo.dart lib/custom_painters/language_icon.dart lib/custom_painters/whatsapp_logo.dart lib/utils/queries.dart lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart lib/view_model/pre_auth_view_models/select_organization_view_model.dart lib/views/after_auth_screens/profile/profile_page.dart lib/view_model/main_screen_view_model.dart lib/views/after_auth_screens/events/create_event_page.dart lib/views/after_auth_screens/org_info_screen.dart lib/views/after_auth_screens/events/manage_volunteer_group.dart lib/views/after_auth_screens/events/create_agenda_item_page.dart lib/views/after_auth_screens/events/edit_agenda_item_page.dart lib/utils/event_queries.dart lib/views/after_auth_screens/funds/fundraising_campaigns_screen.dart lib/views/after_auth_screens/funds/fund_pledges_screen.dart lib/widgets/update_pledge_dialogue_box.dart
- name: setup python
uses: actions/setup-python@v5
- name: Check for presence of ignore directives corresponding to custom lints
Expand Down
7 changes: 7 additions & 0 deletions lib/locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import 'package:talawa/services/comment_service.dart';
import 'package:talawa/services/database_mutation_functions.dart';
import 'package:talawa/services/event_service.dart';
import 'package:talawa/services/fund_service.dart';
import 'package:talawa/services/graphql_config.dart';
import 'package:talawa/services/image_service.dart';
import 'package:talawa/services/navigation_service.dart';
Expand All @@ -33,6 +34,7 @@
import 'package:talawa/view_model/after_auth_view_models/event_view_models/explore_events_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/event_view_models/manage_volunteer_group_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/feed_view_models/organization_feed_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/funds_view_models/fund_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/profile_view_models/edit_profile_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart';
import 'package:talawa/view_model/after_auth_view_models/settings_view_models/app_setting_view_model.dart';
Expand Down Expand Up @@ -90,6 +92,9 @@
///GetIt for SessionManager.
final sessionManager = locator<SessionManager>();

///GetIt for FundService.
final fundServcie = locator<FundService>();

Check warning on line 96 in lib/locator.dart

View check run for this annotation

Codecov / codecov/patch

lib/locator.dart#L96

Added line #L96 was not covered by tests

Comment on lines +95 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in variable name.

There's a typo in the variable name fundServcie which should be fundService.

Apply this fix:

-final fundServcie = locator<FundService>();
+final fundService = locator<FundService>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
///GetIt for FundService.
final fundServcie = locator<FundService>();
///GetIt for FundService.
final fundService = locator<FundService>();

///GetIt for ActonHandlerService.
final actionHandlerService = locator<ActionHandlerService>();

Expand Down Expand Up @@ -122,6 +127,7 @@

//Services
locator.registerLazySingleton(() => PostService());
locator.registerLazySingleton(() => FundService());
locator.registerLazySingleton(() => EventService());
locator.registerLazySingleton(() => CommentService());
locator.registerLazySingleton(() => OrganizationService());
Expand Down Expand Up @@ -151,6 +157,7 @@
locator.registerFactory(() => OrganizationFeedViewModel());
locator.registerFactory(() => SetUrlViewModel());
locator.registerFactory(() => LoginViewModel());
locator.registerFactory(() => FundViewModel());
locator.registerFactory(() => ManageVolunteerGroupViewModel());
locator.registerFactory(() => EditAgendaItemViewModel());
locator.registerFactory(() => SelectOrganizationViewModel());
Expand Down
91 changes: 91 additions & 0 deletions lib/models/funds/fund.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:talawa/models/funds/fund_campaign.dart';
import 'package:talawa/models/user/user_info.dart';

/// The `Fund` class represents a fund in the application.
class Fund {
/// Constructs a `Fund` instance.
///
/// [id] is the unique identifier of the fund.
/// [organizationId] is the identifier of the organization to which the fund belongs.
/// [name] is the name of the fund.
/// [taxDeductible] indicates whether the fund is tax-deductible.
/// [isDefault] indicates whether the fund is the default fund.
/// [isArchived] indicates whether the fund is archived.
/// [creatorId] is the identifier of the user who created the fund.
/// [campaigns] is a list of campaign identifiers associated with the fund.
/// [createdAt] is the timestamp of when the fund was created.
/// [updatedAt] is the timestamp of when the fund was last updated.
Fund({
this.id,
this.organizationId,
this.name,
this.taxDeductible,
this.isDefault,
this.isArchived,
this.creator,
this.campaigns,
this.createdAt,
this.updatedAt,
});
Comment on lines +6 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix constructor documentation and consider required fields

  1. The constructor documentation mentions creatorId but the parameter is named creator
  2. Consider making essential fields like id, name, and organizationId non-nullable to ensure data integrity

Apply this diff to fix the documentation and make essential fields required:

-  /// [creatorId] is the identifier of the user who created the fund.
+  /// [creator] is the user who created the fund.
   Fund({
-    this.id,
-    this.organizationId,
-    this.name,
+    required this.id,
+    required this.organizationId,
+    required this.name,
     this.taxDeductible,
     this.isDefault,
     this.isArchived,
     this.creator,
     this.campaigns,
     this.createdAt,
     this.updatedAt,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Constructs a `Fund` instance.
///
/// [id] is the unique identifier of the fund.
/// [organizationId] is the identifier of the organization to which the fund belongs.
/// [name] is the name of the fund.
/// [taxDeductible] indicates whether the fund is tax-deductible.
/// [isDefault] indicates whether the fund is the default fund.
/// [isArchived] indicates whether the fund is archived.
/// [creatorId] is the identifier of the user who created the fund.
/// [campaigns] is a list of campaign identifiers associated with the fund.
/// [createdAt] is the timestamp of when the fund was created.
/// [updatedAt] is the timestamp of when the fund was last updated.
Fund({
this.id,
this.organizationId,
this.name,
this.taxDeductible,
this.isDefault,
this.isArchived,
this.creator,
this.campaigns,
this.createdAt,
this.updatedAt,
});
/// Constructs a `Fund` instance.
///
/// [id] is the unique identifier of the fund.
/// [organizationId] is the identifier of the organization to which the fund belongs.
/// [name] is the name of the fund.
/// [taxDeductible] indicates whether the fund is tax-deductible.
/// [isDefault] indicates whether the fund is the default fund.
/// [isArchived] indicates whether the fund is archived.
/// [creator] is the user who created the fund.
/// [campaigns] is a list of campaign identifiers associated with the fund.
/// [createdAt] is the timestamp of when the fund was created.
/// [updatedAt] is the timestamp of when the fund was last updated.
Fund({
required this.id,
required this.organizationId,
required this.name,
this.taxDeductible,
this.isDefault,
this.isArchived,
this.creator,
this.campaigns,
this.createdAt,
this.updatedAt,
});


/// Creates a `Fund` instance from a JSON object.
///
/// The [json] parameter is a map containing the fund data.
///
/// Returns an instance of `Fund`.
factory Fund.fromJson(Map<String, dynamic> json) {
return Fund(
id: json['_id'] as String?,
organizationId: json['organizationId'] as String?,
name: json['name'] as String?,
taxDeductible: json['taxDeductible'] as bool?,
isDefault: json['isDefault'] as bool?,
isArchived: json['isArchived'] as bool?,
creator: json['creator'] == null
? null
: User.fromJson(
json['creator'] as Map<String, dynamic>,
fromOrg: true,
),
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => e as Campaign)
.toList(),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
Comment on lines +36 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix unsafe type casting in campaigns parsing

The current implementation has potential runtime issues:

  1. Direct casting of dynamic to Campaign in the campaigns list is unsafe
  2. Missing proper parsing of campaign objects

Apply this diff to fix the parsing:

       campaigns: (json['campaigns'] as List<dynamic>?)
-          ?.map((e) => e as Campaign)
+          ?.map((e) => Campaign.fromJson(e as Map<String, dynamic>))
           .toList(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
factory Fund.fromJson(Map<String, dynamic> json) {
return Fund(
id: json['_id'] as String?,
organizationId: json['organizationId'] as String?,
name: json['name'] as String?,
taxDeductible: json['taxDeductible'] as bool?,
isDefault: json['isDefault'] as bool?,
isArchived: json['isArchived'] as bool?,
creator: json['creator'] == null
? null
: User.fromJson(
json['creator'] as Map<String, dynamic>,
fromOrg: true,
),
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => e as Campaign)
.toList(),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
factory Fund.fromJson(Map<String, dynamic> json) {
return Fund(
id: json['_id'] as String?,
organizationId: json['organizationId'] as String?,
name: json['name'] as String?,
taxDeductible: json['taxDeductible'] as bool?,
isDefault: json['isDefault'] as bool?,
isArchived: json['isArchived'] as bool?,
creator: json['creator'] == null
? null
: User.fromJson(
json['creator'] as Map<String, dynamic>,
fromOrg: true,
),
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => Campaign.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}


/// The unique identifier of the fund.
final String? id;

/// The identifier of the organization to which the fund belongs.
final String? organizationId;

/// The name of the fund.
final String? name;

/// Indicates whether the fund is tax-deductible.
final bool? taxDeductible;

/// Indicates whether the fund is the default fund.
final bool? isDefault;

/// Indicates whether the fund is archived.
final bool? isArchived;

/// The identifier of the user who created the fund.
final User? creator;

/// A list of campaign identifiers associated with the fund.
final List<Campaign>? campaigns;

/// The timestamp of when the fund was created.
final DateTime? createdAt;

/// The timestamp of when the fund was last updated.
final DateTime? updatedAt;
}
90 changes: 90 additions & 0 deletions lib/models/funds/fund_campaign.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:talawa/models/funds/fund_pledges.dart';

/// The `Campaign` class represents a fundraising campaign in the application.
class Campaign {
/// Constructs a `FundraisingCampaign` instance.
///
/// [id] is the unique identifier of the campaign.
/// [fundId] is the identifier of the fund to which the campaign belongs.
/// [name] is the name of the campaign.
/// [startDate] is the start date of the campaign.
/// [endDate] is the end date of the campaign.
/// [fundingGoal] is the funding goal of the campaign.
/// [currency] is the currency used for the campaign.
/// [pledges] is a list of pledge identifiers associated with the campaign.
/// [createdAt] is the timestamp of when the campaign was created.
/// [updatedAt] is the timestamp of when the campaign was last updated.
Campaign({
this.id,
this.fundId,
this.name,
this.startDate,
this.endDate,
this.fundingGoal,
this.currency,
this.pledges,
this.createdAt,
this.updatedAt,
});
Comment on lines +17 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider making essential fields required and add validation.

The current implementation makes all fields nullable, which might be too permissive. Consider:

  1. Making essential fields non-nullable (e.g., name, startDate, endDate, fundingGoal)
  2. Adding validation for:
    • Start date before end date
    • Positive funding goal
    • Valid currency codes
 Campaign({
-  this.id,
-  this.fundId,
-  this.name,
-  this.startDate,
-  this.endDate,
-  this.fundingGoal,
-  this.currency,
+  this.id,
+  this.fundId,
+  required this.name,
+  required this.startDate,
+  required this.endDate,
+  required this.fundingGoal,
+  required this.currency,
   this.pledges,
   this.createdAt,
   this.updatedAt,
-});
+}) {
+  if (startDate.isAfter(endDate)) {
+    throw ArgumentError('Start date must be before end date');
+  }
+  if (fundingGoal <= 0) {
+    throw ArgumentError('Funding goal must be positive');
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Campaign({
this.id,
this.fundId,
this.name,
this.startDate,
this.endDate,
this.fundingGoal,
this.currency,
this.pledges,
this.createdAt,
this.updatedAt,
});
Campaign({
this.id,
this.fundId,
required this.name,
required this.startDate,
required this.endDate,
required this.fundingGoal,
required this.currency,
this.pledges,
this.createdAt,
this.updatedAt,
}) {
if (startDate.isAfter(endDate)) {
throw ArgumentError('Start date must be before end date');
}
if (fundingGoal <= 0) {
throw ArgumentError('Funding goal must be positive');
}
}


/// Creates a `Campaign` instance from a JSON object.
///
/// The [json] parameter is a map containing the campaign data.
///
/// Returns an instance of `Campaign`.
factory Campaign.fromJson(Map<String, dynamic> json) {
return Campaign(
id: json['_id'] as String?,
fundId: json['fundId'] as String?,
name: json['name'] as String?,
startDate: json['startDate'] != null
? DateTime.parse(json['startDate'] as String)
: null,
endDate: json['endDate'] != null
? DateTime.parse(json['endDate'] as String)
: null,
fundingGoal: (json['fundingGoal'] is int)
? (json['fundingGoal'] as int).toDouble()

Check warning on line 47 in lib/models/funds/fund_campaign.dart

View check run for this annotation

Codecov / codecov/patch

lib/models/funds/fund_campaign.dart#L47

Added line #L47 was not covered by tests
: json['fundingGoal'] as double?,
currency: json['currency'] as String?,
pledges:
(json['pledges'] as List<dynamic>?)?.map((e) => e as Pledge).toList(),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
Comment on lines +35 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical issues in JSON deserialization.

There are several issues in the fromJson method:

  1. The pledges mapping will fail at runtime as it directly casts dynamic to Pledge
  2. Missing error handling for invalid date formats
  3. No validation of parsed values
 factory Campaign.fromJson(Map<String, dynamic> json) {
+  // Helper function for safe date parsing
+  DateTime? parseDate(String? dateStr) {
+    if (dateStr == null) return null;
+    try {
+      return DateTime.parse(dateStr);
+    } catch (e) {
+      throw FormatException('Invalid date format: $dateStr');
+    }
+  }
+
   return Campaign(
     id: json['_id'] as String?,
     fundId: json['fundId'] as String?,
     name: json['name'] as String?,
-    startDate: json['startDate'] != null
-        ? DateTime.parse(json['startDate'] as String)
-        : null,
-    endDate: json['endDate'] != null
-        ? DateTime.parse(json['endDate'] as String)
-        : null,
+    startDate: parseDate(json['startDate'] as String?),
+    endDate: parseDate(json['endDate'] as String?),
     fundingGoal: (json['fundingGoal'] is int)
         ? (json['fundingGoal'] as int).toDouble()
         : json['fundingGoal'] as double?,
     currency: json['currency'] as String?,
-    pledges:
-        (json['pledges'] as List<dynamic>?)?.map((e) => e as Pledge).toList(),
-    createdAt: json['createdAt'] != null
-        ? DateTime.parse(json['createdAt'] as String)
-        : null,
-    updatedAt: json['updatedAt'] != null
-        ? DateTime.parse(json['updatedAt'] as String)
-        : null,
+    pledges: (json['pledges'] as List<dynamic>?)
+        ?.map((e) => Pledge.fromJson(e as Map<String, dynamic>))
+        .toList(),
+    createdAt: parseDate(json['createdAt'] as String?),
+    updatedAt: parseDate(json['updatedAt'] as String?),
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
factory Campaign.fromJson(Map<String, dynamic> json) {
return Campaign(
id: json['_id'] as String?,
fundId: json['fundId'] as String?,
name: json['name'] as String?,
startDate: json['startDate'] != null
? DateTime.parse(json['startDate'] as String)
: null,
endDate: json['endDate'] != null
? DateTime.parse(json['endDate'] as String)
: null,
fundingGoal: (json['fundingGoal'] is int)
? (json['fundingGoal'] as int).toDouble()
: json['fundingGoal'] as double?,
currency: json['currency'] as String?,
pledges:
(json['pledges'] as List<dynamic>?)?.map((e) => e as Pledge).toList(),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
factory Campaign.fromJson(Map<String, dynamic> json) {
// Helper function for safe date parsing
DateTime? parseDate(String? dateStr) {
if (dateStr == null) return null;
try {
return DateTime.parse(dateStr);
} catch (e) {
throw FormatException('Invalid date format: $dateStr');
}
}
return Campaign(
id: json['_id'] as String?,
fundId: json['fundId'] as String?,
name: json['name'] as String?,
startDate: parseDate(json['startDate'] as String?),
endDate: parseDate(json['endDate'] as String?),
fundingGoal: (json['fundingGoal'] is int)
? (json['fundingGoal'] as int).toDouble()
: json['fundingGoal'] as double?,
currency: json['currency'] as String?,
pledges: (json['pledges'] as List<dynamic>?)
?.map((e) => Pledge.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: parseDate(json['createdAt'] as String?),
updatedAt: parseDate(json['updatedAt'] as String?),
);
}


/// The unique identifier of the campaign.
final String? id;

/// The identifier of the fund to which the campaign belongs.
final String? fundId;

/// The name of the campaign.
final String? name;

/// The start date of the campaign.
final DateTime? startDate;

/// The end date of the campaign.
final DateTime? endDate;

/// The funding goal of the campaign.
final double? fundingGoal;

/// The currency used for the campaign.
final String? currency;

/// A list of pledge identifiers associated with the campaign.
final List<Pledge>? pledges;

/// The timestamp of when the campaign was created.
final DateTime? createdAt;

/// The timestamp of when the campaign was last updated.
final DateTime? updatedAt;
}
90 changes: 90 additions & 0 deletions lib/models/funds/fund_pledges.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:talawa/models/funds/fund_campaign.dart';
import 'package:talawa/models/user/user_info.dart';

/// The `Pledge` class represents a pledge for a fundraising campaign in the application.
class Pledge {
/// Constructs a `Pledge` instance.
///
/// [id] is the unique identifier of the pledge.
/// [campaigns] is a list of campaign identifiers associated with the pledge.
/// [users] is a list of user identifiers associated with the pledge.
/// [startDate] is the start date of the pledge.
/// [endDate] is the end date of the pledge.
/// [amount] is the amount pledged.
/// [currency] is the currency of the pledged amount.
/// [createdAt] is the timestamp of when the pledge was created.
/// [updatedAt] is the timestamp of when the pledge was last updated.
Pledge({
this.id,
this.campaigns,
this.pledgers,
this.startDate,
this.endDate,
this.amount,
this.currency,
this.createdAt,
this.updatedAt,
});
Comment on lines +8 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix constructor parameter documentation and consider required fields.

  1. The parameter documentation refers to users but the parameter is named pledgers.
  2. Consider making essential fields required to prevent invalid states.

Suggested improvements:

-  /// [users] is a list of user identifiers associated with the pledge.
+  /// [pledgers] is a list of users who made this pledge.
   ///
   Pledge({
-    this.id,
-    this.campaigns,
-    this.pledgers,
-    this.startDate,
-    this.endDate,
-    this.amount,
-    this.currency,
+    required this.campaigns,
+    required this.pledgers,
+    required this.startDate,
+    required this.endDate,
+    required this.amount,
+    required this.currency,
+    this.id,
     this.createdAt,
     this.updatedAt,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// [id] is the unique identifier of the pledge.
/// [campaigns] is a list of campaign identifiers associated with the pledge.
/// [users] is a list of user identifiers associated with the pledge.
/// [startDate] is the start date of the pledge.
/// [endDate] is the end date of the pledge.
/// [amount] is the amount pledged.
/// [currency] is the currency of the pledged amount.
/// [createdAt] is the timestamp of when the pledge was created.
/// [updatedAt] is the timestamp of when the pledge was last updated.
Pledge({
this.id,
this.campaigns,
this.pledgers,
this.startDate,
this.endDate,
this.amount,
this.currency,
this.createdAt,
this.updatedAt,
});
/// [id] is the unique identifier of the pledge.
/// [campaigns] is a list of campaign identifiers associated with the pledge.
/// [pledgers] is a list of users who made this pledge.
/// [startDate] is the start date of the pledge.
/// [endDate] is the end date of the pledge.
/// [amount] is the amount pledged.
/// [currency] is the currency of the pledged amount.
/// [createdAt] is the timestamp of when the pledge was created.
/// [updatedAt] is the timestamp of when the pledge was last updated.
Pledge({
required this.campaigns,
required this.pledgers,
required this.startDate,
required this.endDate,
required this.amount,
required this.currency,
this.id,
this.createdAt,
this.updatedAt,
});


/// Creates a `Pledge` instance from a JSON object.
///
/// The [json] parameter is a map containing the pledge data.
///
/// Returns an instance of `Pledge`.
factory Pledge.fromJson(Map<String, dynamic> json) {
return Pledge(
id: json['_id'] as String?,
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => e as Campaign)
.toList(),
pledgers: json['users'] == null
? null
: (json['users'] as List<dynamic>?)
?.map(
(e) => User.fromJson(e as Map<String, dynamic>, fromOrg: true),
)
.toList(),
startDate: json['startDate'] != null
? DateTime.parse(json['startDate'] as String)
: null,
endDate: json['endDate'] != null
? DateTime.parse(json['endDate'] as String)
: null,
amount: json['amount'] as int?,
currency: json['currency'] as String?,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
Comment on lines +34 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address potential runtime errors in JSON deserialization.

Several issues need attention:

  1. Unsafe casting of campaigns could cause runtime errors
  2. Missing validation for amount and currency
  3. Inconsistent field mapping between JSON and class properties

Suggested improvements:

   factory Pledge.fromJson(Map<String, dynamic> json) {
+    // Validate amount
+    final amount = json['amount'] as int?;
+    if (amount != null && amount <= 0) {
+      throw FormatException('Pledge amount must be positive');
+    }
+
+    // Validate currency format
+    final currency = json['currency'] as String?;
+    if (currency != null && currency.length != 3) {
+      throw FormatException('Currency must be a 3-letter ISO code');
+    }
+
     return Pledge(
       id: json['_id'] as String?,
       campaigns: (json['campaigns'] as List<dynamic>?)
-          ?.map((e) => e as Campaign)
+          ?.map((e) => Campaign.fromJson(e as Map<String, dynamic>))
           .toList(),
       pledgers: json['users'] == null
           ? null
           : (json['users'] as List<dynamic>?)
               ?.map(
                 (e) => User.fromJson(e as Map<String, dynamic>, fromOrg: true),
               )
               .toList(),
       startDate: json['startDate'] != null
           ? DateTime.parse(json['startDate'] as String)
           : null,
       endDate: json['endDate'] != null
           ? DateTime.parse(json['endDate'] as String)
           : null,
-      amount: json['amount'] as int?,
-      currency: json['currency'] as String?,
+      amount: amount,
+      currency: currency,
       createdAt: json['createdAt'] != null
           ? DateTime.parse(json['createdAt'] as String)
           : null,
       updatedAt: json['updatedAt'] != null
           ? DateTime.parse(json['updatedAt'] as String)
           : null,
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
factory Pledge.fromJson(Map<String, dynamic> json) {
return Pledge(
id: json['_id'] as String?,
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => e as Campaign)
.toList(),
pledgers: json['users'] == null
? null
: (json['users'] as List<dynamic>?)
?.map(
(e) => User.fromJson(e as Map<String, dynamic>, fromOrg: true),
)
.toList(),
startDate: json['startDate'] != null
? DateTime.parse(json['startDate'] as String)
: null,
endDate: json['endDate'] != null
? DateTime.parse(json['endDate'] as String)
: null,
amount: json['amount'] as int?,
currency: json['currency'] as String?,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
factory Pledge.fromJson(Map<String, dynamic> json) {
// Validate amount
final amount = json['amount'] as int?;
if (amount != null && amount <= 0) {
throw FormatException('Pledge amount must be positive');
}
// Validate currency format
final currency = json['currency'] as String?;
if (currency != null && currency.length != 3) {
throw FormatException('Currency must be a 3-letter ISO code');
}
return Pledge(
id: json['_id'] as String?,
campaigns: (json['campaigns'] as List<dynamic>?)
?.map((e) => Campaign.fromJson(e as Map<String, dynamic>))
.toList(),
pledgers: json['users'] == null
? null
: (json['users'] as List<dynamic>?)
?.map(
(e) => User.fromJson(e as Map<String, dynamic>, fromOrg: true),
)
.toList(),
startDate: json['startDate'] != null
? DateTime.parse(json['startDate'] as String)
: null,
endDate: json['endDate'] != null
? DateTime.parse(json['endDate'] as String)
: null,
amount: amount,
currency: currency,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}


/// The unique identifier of the pledge.
final String? id;

/// A list of campaign identifiers associated with the pledge.
final List<Campaign>? campaigns;

/// A list of user identifiers associated with the pledge.
final List<User>? pledgers;

/// The start date of the pledge.
final DateTime? startDate;

/// The end date of the pledge.
final DateTime? endDate;

/// The amount pledged.
final int? amount;

/// The currency of the pledged amount.
final String? currency;

/// The timestamp of when the pledge was created.
final DateTime? createdAt;

/// The timestamp of when the pledge was last updated.
final DateTime? updatedAt;
}
2 changes: 1 addition & 1 deletion lib/services/event_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class EventService extends BaseFeedManager<Event> {
final String currentOrgID = _currentOrg.id!;
// mutation to fetch the events
final String mutation = EventQueries().fetchOrgEvents(currentOrgID);
final result = await _dbFunctions.gqlAuthQuery(mutation);
final result = await _dbFunctions.gqlAuthMutation(mutation);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

⚠️ Potential issue

Using mutation for a query operation

The code is incorrectly using gqlAuthMutation to execute what is actually a GraphQL query operation. The fetchOrgEvents in EventQueries is defined as a query operation, but EventService is using gqlAuthMutation to execute it, which is incorrect.

Fix:

- final result = await _dbFunctions.gqlAuthMutation(mutation);
+ final result = await _dbFunctions.gqlAuthQuery(mutation);

• The fetchOrgEvents operation in lib/utils/event_queries.dart is explicitly defined as a query operation
• Using gqlAuthMutation for a query operation violates GraphQL operation semantics and could cause issues with caching and optimization

🔗 Analysis chain

Critical: Incorrect use of GraphQL mutation for fetching data

The change from gqlAuthQuery to gqlAuthMutation for fetching events is incorrect according to GraphQL best practices. Queries should be used for fetching data, while mutations are reserved for operations that modify data. This change could:

  1. Impact caching behavior as GraphQL clients treat mutations differently from queries
  2. Affect performance as mutations bypass certain optimizations available for queries
  3. Confuse other developers as it violates GraphQL conventions

Apply this diff to fix the issue:

-    final result = await _dbFunctions.gqlAuthMutation(mutation);
+    final result = await _dbFunctions.gqlAuthQuery(mutation);

Let's verify if this is an isolated issue or part of a pattern:


Let me analyze the concern about using gqlAuthMutation instead of gqlAuthQuery for fetching data. I'll need to verify the actual usage and context by checking the event service implementation.


Let me check the EventQueries to understand if the query being used is actually a mutation or query operation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other potential misuses of mutations for fetching data
rg -B 2 -A 2 'gqlAuthMutation.*fetch|fetch.*gqlAuthMutation'

Length of output: 517999


Script:

#!/bin/bash
# Check the event service implementation and related files
rg -B 5 -A 5 "fetchDataFromApi|gqlAuthMutation|gqlAuthQuery" lib/services/event_service.dart

Length of output: 6893


Script:

#!/bin/bash
# Check the event queries implementation
rg -B 2 -A 2 "fetchOrgEvents" lib/utils/event_queries.dart

Length of output: 173


if (result.data == null) {
throw Exception('unable to fetch data');
Expand Down
Loading
Loading