Skip to content

Commit a479618

Browse files
committed
Refactor SQL queries and enhance member statistics
Refactored SQL queries to use enums for subscription and payment statuses, improving maintainability and reducing hardcoding risks. Added new fields (`HasActiveSubscription`, `PaidInsurance`, `ActivePaidInsuranceCount`) to DTOs for better member tracking. Updated insurance payment logic to ensure data consistency and prevent duplicate payments. Normalized insurance payment dates to improve date handling. Removed legacy SQL queries and cleaned up redundant code. Enhanced comments for better readability.
1 parent b0cb68c commit a479618

File tree

6 files changed

+124
-74
lines changed

6 files changed

+124
-74
lines changed

GymMgmt.Application/Features/Members/PayInsurrance/CreateInsurancePaymentCommandHandler.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ public async Task<bool> Handle( CreateInsurancePaymentCommand request,Cancellati
4343
var dateTimenow = DateTime.Now;
4444

4545
var payment =member.RecordInsurancePayment(dateTimenow, clubsettings, request.Refrence);
46-
47-
48-
4946
return payment != null;
5047
}
5148
}

GymMgmt.Application/Features/Members/Queries/GetMemberStatistics/GetMemberStatisticsHandler.cs

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Dapper;
22
using GymMgmt.Application.Common.Interfaces;
3+
using GymMgmt.Domain.Common.Enums;
34
using MediatR;
45

56

@@ -18,33 +19,50 @@ public async Task<MemberStatisticsDto> Handle(GetMemberStatisticsQuery request,
1819
{
1920
using var connection = _connectionFactory.CreateNewConnection();
2021

21-
// We fetch the GracePeriod days from settings dynamically
22-
// Then we use CASE WHEN to categorize every subscription efficiently
23-
var sql = @"
24-
DECLARE @GraceDays INT = (SELECT TOP 1 SubscriptionGracePeriodInDays FROM ClubSettings);
25-
DECLARE @Now DATETIME2 = SYSDATETIME();
26-
27-
SELECT
28-
COUNT(CASE
29-
WHEN s.Status = 'Active' AND s.EndDate >= @Now
30-
THEN 1 END) as ActiveCount,
31-
32-
COUNT(CASE
33-
WHEN s.Status = 'Active' AND s.EndDate < @Now AND s.EndDate >= DATEADD(day, -@GraceDays, @Now)
34-
THEN 1 END) as GracePeriodCount,
35-
36-
COUNT(CASE
37-
WHEN (s.Status = 'Active' AND s.EndDate < DATEADD(day, -@GraceDays, @Now)) OR s.Status = 'Expired'
38-
THEN 1 END) as ExpiredCount,
39-
40-
COUNT(CASE
41-
WHEN s.Status = 'Cancelled'
42-
THEN 1 END) as CancelledCount
43-
44-
FROM Subscriptions s
45-
-- Ensure we only count the latest/relevant subscription per member if needed
46-
-- For now, this counts ALL subscriptions in the system
47-
WHERE s.WillNotRenew = 0 OR s.Status = 'Cancelled'";
22+
// 1. Refactor-Proof Enum Names
23+
// We prepare these strings so we don't hardcode 'Insurance' or 'Paid'
24+
var insuranceType = PaymentType.Insurance.ToString();
25+
var paidStatus = PaymentStatus.Paid.ToString();
26+
var activeSubStatus = SubscriptionStatus.Active.ToString();
27+
var expiredSubStatus = SubscriptionStatus.Expired.ToString();
28+
var cancelledSubStatus = SubscriptionStatus.Cancelled.ToString();
29+
30+
// 2. The Query
31+
var sql = $@"
32+
DECLARE @GraceDays INT = (SELECT TOP 1 SubscriptionGracePeriodInDays FROM ClubSettings);
33+
DECLARE @Now DATETIME2 = SYSDATETIME();
34+
35+
SELECT
36+
-- 1. Subscription Counts (From Subscriptions Table)
37+
COUNT(CASE
38+
WHEN s.Status = '{activeSubStatus}' AND s.EndDate >= @Now
39+
THEN 1 END) as ActiveCount,
40+
41+
COUNT(CASE
42+
WHEN s.Status = '{activeSubStatus}' AND s.EndDate < @Now AND s.EndDate >= DATEADD(day, -@GraceDays, @Now)
43+
THEN 1 END) as GracePeriodCount,
44+
45+
COUNT(CASE
46+
WHEN (s.Status = '{activeSubStatus}' AND s.EndDate < DATEADD(day, -@GraceDays, @Now)) OR s.Status = '{expiredSubStatus}'
47+
THEN 1 END) as ExpiredCount,
48+
49+
COUNT(CASE
50+
WHEN s.Status = '{cancelledSubStatus}'
51+
THEN 1 END) as CancelledCount,
52+
53+
-- 2. Insurance Count (Scalar Subquery from Payments Table)
54+
-- We count DISTINCT MemberId to ensure we don't double-count if a member has overlapping records (edge case)
55+
(
56+
SELECT COUNT(DISTINCT MemberId)
57+
FROM Payments p
58+
WHERE p.Type = '{insuranceType}'
59+
AND p.Status = '{paidStatus}'
60+
AND @Now >= p.PeriodStart
61+
AND @Now <= p.PeriodEnd
62+
) as ActivePaidInsuranceCount
63+
64+
FROM Subscriptions s
65+
WHERE s.WillNotRenew = 0 OR s.Status = '{cancelledSubStatus}'";
4866

4967
return await connection.QuerySingleAsync<MemberStatisticsDto>(sql);
5068
}

GymMgmt.Application/Features/Members/Queries/GetmembersByStatus/GetMembersByStatusHandler.cs

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Dapper;
22
using GymMgmt.Application.Common.Interfaces;
33
using GymMgmt.Application.Common.Models;
4+
using GymMgmt.Domain.Common.Enums;
45
using MediatR;
56

67

@@ -19,60 +20,89 @@ public async Task<PaginatedList<MemberListDto>> Handle(GetMembersByStatusQuery r
1920
{
2021
using var connection = _connectionFactory.CreateNewConnection();
2122

22-
// 1. Dynamic WHERE Clause
23-
// Note: We alias Subscriptions as 's' and Members as 'm' later
23+
// 1. Refactor-Proof Enum Names
24+
// We use the actual C# Enum to generate the string values ('Insurance', 'Paid')
25+
// If you rename the Enum member, this SQL updates automatically.
26+
var insuranceType = PaymentType.Insurance.ToString();
27+
var paidStatus = PaymentStatus.Paid.ToString();
28+
var activeStatus = SubscriptionStatus.Active.ToString(); // Assuming you have this Enum too
29+
var cancelledStatus = SubscriptionStatus.Cancelled.ToString();
30+
31+
// 2. Filter Logic
2432
string whereClause = request.StatusFilter switch
2533
{
26-
MemberStatusFilter.All => "", // No filter
27-
28-
MemberStatusFilter.Active =>
29-
"WHERE s.Status = 'Active' AND s.EndDate >= @Now",
34+
MemberStatusFilter.All => "",
35+
MemberStatusFilter.Active => $"WHERE s.Status = '{activeStatus}' AND s.EndDate >= @Now",
3036

3137
MemberStatusFilter.GracePeriod =>
32-
"WHERE s.Status = 'Active' AND s.EndDate < @Now AND s.EndDate >= DATEADD(day, -@GraceDays, @Now)",
38+
$"WHERE s.Status = '{activeStatus}' AND s.EndDate < @Now AND s.EndDate >= DATEADD(day, -@GraceDays, @Now)",
3339

3440
MemberStatusFilter.Expired =>
35-
"WHERE (s.Status = 'Active' AND s.EndDate < DATEADD(day, -@GraceDays, @Now)) OR s.Status = 'Expired'",
41+
$"WHERE (s.Status = '{activeStatus}' AND s.EndDate < DATEADD(day, -@GraceDays, @Now)) OR s.Status = '{nameof(SubscriptionStatus.Expired)}'",
3642

37-
MemberStatusFilter.Cancelled =>
38-
"WHERE s.Status = 'Cancelled'",
43+
MemberStatusFilter.Cancelled => $"WHERE s.Status = '{cancelledStatus}'",
3944

4045
_ => "WHERE 1=0"
4146
};
4247

4348
var sql = $@"
44-
DECLARE @GraceDays INT = (SELECT TOP 1 SubscriptionGracePeriodInDays FROM ClubSettings);
45-
DECLARE @Now DATETIME2 = SYSDATETIME();
46-
47-
-- Count Total (Matching the filter)
48-
SELECT COUNT(*)
49-
FROM Members m
50-
LEFT JOIN Subscriptions s ON m.Id = s.MemberId
51-
{whereClause};
52-
53-
-- Get Data
54-
SELECT
55-
m.Id as MemberId,
56-
m.FirstName + ' ' + m.LastName as FullName,
57-
ISNULL(s.PlanName, 'No Plan') as PlanName, -- Handle NULL for prospects
58-
ISNULL(s.EndDate, '1900-01-01') as EndDate, -- Handle NULL dates
59-
m.PhoneNumber,
60-
CASE
61-
WHEN s.EndDate IS NULL THEN 0
62-
ELSE DATEDIFF(day, s.EndDate, @Now)
63-
END as DaysOverdue
64-
FROM Members m
65-
LEFT JOIN Subscriptions s ON m.Id = s.MemberId
66-
{whereClause}
67-
ORDER BY
68-
CASE WHEN s.EndDate IS NULL THEN 1 ELSE 0 END, -- Put 'No Plan' people at the bottom
69-
s.EndDate ASC
70-
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;";
49+
DECLARE @GraceDays INT = (SELECT TOP 1 SubscriptionGracePeriodInDays FROM ClubSettings);
50+
51+
-- Use Parameter for Time to keep Application Server as the Source of Truth
52+
-- (We pass @Now from C# below)
53+
54+
-- 1. Count Total
55+
SELECT COUNT(*)
56+
FROM Members m
57+
LEFT JOIN Subscriptions s ON m.Id = s.MemberId
58+
{whereClause};
59+
60+
-- 2. Get Data
61+
SELECT
62+
m.Id as MemberId,
63+
m.FirstName + ' ' + m.LastName as FullName,
64+
ISNULL(s.PlanName, 'No Plan') as PlanName,
65+
ISNULL(s.EndDate, '1900-01-01') as EndDate,
66+
m.PhoneNumber,
67+
68+
CASE
69+
WHEN s.EndDate IS NULL THEN 0
70+
ELSE DATEDIFF(day, s.EndDate, @Now)
71+
END as DaysOverdue,
72+
73+
-- HAS ACTIVE SUBSCRIPTION?
74+
CASE WHEN EXISTS (
75+
SELECT 1
76+
FROM Subscriptions sub
77+
WHERE sub.MemberId = m.Id
78+
AND sub.Status = '{activeStatus}'
79+
AND sub.EndDate >= @Now
80+
) THEN 1 ELSE 0 END as HasActiveSubscription,
81+
82+
-- HAS VALID INSURANCE?
83+
CASE WHEN EXISTS (
84+
SELECT 1
85+
FROM Payments p
86+
WHERE p.MemberId = m.Id
87+
AND p.Type = '{insuranceType}' -- Inject 'Insurance'
88+
AND p.Status = '{paidStatus}' -- Inject 'Paid'
89+
AND @Now >= p.PeriodStart
90+
AND @Now <= p.PeriodEnd
91+
) THEN 1 ELSE 0 END as PaidInsurance
92+
93+
FROM Members m
94+
LEFT JOIN Subscriptions s ON m.Id = s.MemberId
95+
{whereClause}
96+
ORDER BY
97+
CASE WHEN s.EndDate IS NULL THEN 1 ELSE 0 END,
98+
s.EndDate ASC
99+
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;";
71100

72101
using var multi = await connection.QueryMultipleAsync(sql, new
73102
{
74103
Skip = (request.PageNumber - 1) * request.PageSize,
75-
Take = request.PageSize
104+
Take = request.PageSize,
105+
Now = DateTime.Now // Pass time from C#
76106
});
77107

78108
var totalCount = await multi.ReadFirstAsync<int>();

GymMgmt.Application/Features/Members/Queries/MemberListDto.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public class MemberListDto
1313
public string PlanName { get; set; } = string.Empty;
1414
public DateTime EndDate { get; set; }
1515
public string PhoneNumber { get; set; } = string.Empty;
16-
public int DaysOverdue { get; set; } // Useful for UI
16+
public bool HasActiveSubscription { get; set; } // NEW
17+
public bool PaidInsurance { get; set; } //NEW
18+
public int DaysOverdue { get; set; }
1719
}
1820
}

GymMgmt.Application/Features/Members/Queries/MemberStatisticsDto.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public record MemberStatisticsDto(
1010
int ActiveCount,
1111
int GracePeriodCount,
1212
int ExpiredCount,
13-
int CancelledCount
14-
);
13+
int CancelledCount,
14+
int ActivePaidInsuranceCount
15+
);
1516
}

GymMgmt.Domain/Entities/Members/Member.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,12 @@ public bool CanEnterGym(ClubSettings settings)
9090
!s.IsInGracePeriod(settings.SubscriptionGracePeriodInDays));
9191
}
9292

93-
public void MarkInsuranceAsPaid()
93+
private void MarkInsuranceAsPaid(DateTime paymentDate)
9494
{
95-
if (HasPaidInsurance)
95+
// Check if they were ALREADY insured on the specific date they are trying to pay for.
96+
if (IsInsuredOn(paymentDate))
9697
throw new InsuranceFeeAlreadyPaidException(GetFullName());
98+
9799
HasPaidInsurance = true;
98100
}
99101
public Subscription StartSubscription(
@@ -203,7 +205,7 @@ public Payment RecordInsurancePayment(
203205
if (IsInsuredOn(paymentDate))
204206
throw new InsuranceAlreadyActiveException(Id.Value,paymentDate);
205207

206-
MarkInsuranceAsPaid(); // to prevent the Payment object to be in the _payments collection in memory in case the same instance in the same unit of work raised saveChange
208+
MarkInsuranceAsPaid(paymentDate);
207209

208210
// --- NORMALIZE THE DATES ---
209211

0 commit comments

Comments
 (0)