Yuyuqt commited on
Commit
7fe61c9
·
1 Parent(s): 244fdfb

add: membership view

Browse files
Backend/Features/Loyalty/LoyaltyController.cs CHANGED
@@ -1,7 +1,9 @@
 
1
  using Microsoft.AspNetCore.Authorization;
2
  using Microsoft.AspNetCore.Mvc;
3
  using System.Security.Claims;
4
  using System.Threading.Tasks;
 
5
 
6
  namespace Backend.Features.Loyalty
7
  {
@@ -11,10 +13,12 @@ namespace Backend.Features.Loyalty
11
  public class LoyaltyController : ControllerBase
12
  {
13
  private readonly ILoyaltyService _loyaltyService;
 
14
 
15
- public LoyaltyController(ILoyaltyService loyaltyService)
16
  {
17
  _loyaltyService = loyaltyService;
 
18
  }
19
 
20
  [HttpGet("my-account")]
@@ -32,11 +36,86 @@ namespace Backend.Features.Loyalty
32
  return NotFound("Loyalty account not found.");
33
  }
34
 
35
- return Ok(new
36
- {
37
- currentBalance = account.CurrentBalance,
38
- tier = account.Tier
39
  });
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
  }
 
1
+ using Backend.Features.Subscriptions;
2
  using Microsoft.AspNetCore.Authorization;
3
  using Microsoft.AspNetCore.Mvc;
4
  using System.Security.Claims;
5
  using System.Threading.Tasks;
6
+ using System.Linq;
7
 
8
  namespace Backend.Features.Loyalty
9
  {
 
13
  public class LoyaltyController : ControllerBase
14
  {
15
  private readonly ILoyaltyService _loyaltyService;
16
+ private readonly ISubscriptionService _subscriptionService;
17
 
18
+ public LoyaltyController(ILoyaltyService loyaltyService, ISubscriptionService subscriptionService)
19
  {
20
  _loyaltyService = loyaltyService;
21
+ _subscriptionService = subscriptionService;
22
  }
23
 
24
  [HttpGet("my-account")]
 
36
  return NotFound("Loyalty account not found.");
37
  }
38
 
39
+ return Ok(new
40
+ {
41
+ currentBalance = account.CurrentBalance,
42
+ tier = account.Tier
43
  });
44
  }
45
+
46
+ [HttpGet("my-redemptions")]
47
+ public async Task<IActionResult> GetMyRedemptions()
48
+ {
49
+ var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
50
+ if (string.IsNullOrEmpty(userIdStr))
51
+ {
52
+ return Unauthorized();
53
+ }
54
+
55
+ var redemptions = await _loyaltyService.GetUserRedemptionsAsync(userIdStr);
56
+ return Ok(redemptions);
57
+ }
58
+
59
+ [HttpPost("claim")]
60
+ public async Task<IActionResult> ClaimReward([FromBody] ClaimRewardRequestDto request)
61
+ {
62
+ var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
63
+ if (string.IsNullOrEmpty(userIdStr))
64
+ {
65
+ return Unauthorized();
66
+ }
67
+
68
+ var (success, message) = await _loyaltyService.ClaimRewardAsync(userIdStr, request.RewardId, request.Notes ?? "Redeemed via Library Web Application");
69
+
70
+ if (success)
71
+ {
72
+ return Ok(new { message });
73
+ }
74
+
75
+ return BadRequest(new { message });
76
+ }
77
+
78
+ [HttpGet("admin/redemptions/pending")]
79
+ [Authorize(Roles = "Librarian")]
80
+ public async Task<IActionResult> GetPendingRedemptions()
81
+ {
82
+ var redemptions = await _loyaltyService.GetPendingRedemptionsAsync();
83
+ return Ok(redemptions);
84
+ }
85
+
86
+ [HttpPost("admin/redemptions/{id}/fulfill")]
87
+ [Authorize(Roles = "Librarian")]
88
+ public async Task<IActionResult> FulfillRedemption(string id)
89
+ {
90
+ // 1. Get all pending redemptions to find the details of the one we want
91
+ var pending = await _loyaltyService.GetPendingRedemptionsAsync();
92
+ var redemption = pending.FirstOrDefault(r => r.Id == id);
93
+
94
+ if (redemption == null)
95
+ {
96
+ return NotFound("Redemption record not found or already processed.");
97
+ }
98
+
99
+ // 2. Call the external loyalty system to mark as fulfilled
100
+ var success = await _loyaltyService.UpdateRedemptionStatusAsync(id, "Fulfilled");
101
+ if (!success)
102
+ {
103
+ return BadRequest("Failed to update status in the loyalty system.");
104
+ }
105
+
106
+ // 3. side effect: Grant membership in the library system
107
+ bool membershipGranted = false;
108
+ if (int.TryParse(redemption.ExternalUserId?.Trim(), out int userId))
109
+ {
110
+ membershipGranted = await _subscriptionService.HandleLoyaltyRedemptionAsync(userId, redemption.RewardId, redemption.Id);
111
+ }
112
+
113
+ if (!membershipGranted)
114
+ {
115
+ return BadRequest(new { message = $"Redemption fulfilled in loyalty system, but failed to grant library membership. The Reward ID '{redemption.RewardId}' may not be correctly mapped in the library system, or the user already has this membership." });
116
+ }
117
+
118
+ return Ok(new { message = $"Redemption fulfilled and {redemption.RewardName} membership granted successfully." });
119
+ }
120
  }
121
  }
Backend/Features/Loyalty/LoyaltyModels.cs ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Text.Json.Serialization;
3
+
4
+ namespace Backend.Features.Loyalty
5
+ {
6
+ public class LoyaltyRedemptionDto
7
+ {
8
+ [JsonPropertyName("id")]
9
+ public string Id { get; set; } = string.Empty;
10
+
11
+ [JsonPropertyName("rewardId")]
12
+ public string RewardId { get; set; } = string.Empty;
13
+
14
+ [JsonPropertyName("rewardName")]
15
+ public string RewardName { get; set; } = string.Empty;
16
+
17
+ [JsonPropertyName("pointsSpent")]
18
+ public double PointsSpent { get; set; }
19
+
20
+ [JsonPropertyName("status")]
21
+ public string Status { get; set; } = string.Empty;
22
+
23
+ [JsonPropertyName("externalUserId")]
24
+ public string ExternalUserId { get; set; } = string.Empty;
25
+
26
+ [JsonPropertyName("createdAt")]
27
+ public DateTime CreatedAt { get; set; }
28
+ }
29
+
30
+ public class ClaimRewardRequestDto
31
+ {
32
+ public string RewardId { get; set; } = string.Empty;
33
+ public string? Notes { get; set; }
34
+ }
35
+ }
Backend/Features/Loyalty/LoyaltyService.cs CHANGED
@@ -1,10 +1,12 @@
1
  using System;
 
2
  using System.Net.Http;
3
  using System.Net.Http.Json;
4
  using System.Text.Json;
5
  using System.Text.Json.Serialization;
6
  using System.Threading.Tasks;
7
  using Microsoft.Extensions.Logging;
 
8
 
9
  namespace Backend.Features.Loyalty
10
  {
@@ -13,6 +15,10 @@ namespace Backend.Features.Loyalty
13
  Task<bool> RegisterUserAsync(string externalUserId, string email, string mobile);
14
  Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile);
15
  Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId);
 
 
 
 
16
  }
17
 
18
  public class LoyaltyService : ILoyaltyService
@@ -106,6 +112,94 @@ namespace Backend.Features.Loyalty
106
  return null;
107
  }
108
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
  public class AccountLookupResponse
 
1
  using System;
2
+ using System.Collections.Generic;
3
  using System.Net.Http;
4
  using System.Net.Http.Json;
5
  using System.Text.Json;
6
  using System.Text.Json.Serialization;
7
  using System.Threading.Tasks;
8
  using Microsoft.Extensions.Logging;
9
+ using System.Linq;
10
 
11
  namespace Backend.Features.Loyalty
12
  {
 
15
  Task<bool> RegisterUserAsync(string externalUserId, string email, string mobile);
16
  Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile);
17
  Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId);
18
+ Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes);
19
+ Task<IEnumerable<LoyaltyRedemptionDto>> GetUserRedemptionsAsync(string externalUserId);
20
+ Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync();
21
+ Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status);
22
  }
23
 
24
  public class LoyaltyService : ILoyaltyService
 
112
  return null;
113
  }
114
  }
115
+
116
+ public async Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes)
117
+ {
118
+ try
119
+ {
120
+ var payload = new
121
+ {
122
+ externalUserId = externalUserId,
123
+ rewardId = rewardId,
124
+ notes = notes
125
+ };
126
+
127
+ var response = await _httpClient.PostAsJsonAsync("/api/v1/redemption/claim", payload);
128
+ if (response.IsSuccessStatusCode)
129
+ {
130
+ return (true, "Reward claimed successfully!");
131
+ }
132
+
133
+ var error = await response.Content.ReadAsStringAsync();
134
+ _logger.LogWarning($"Failed to claim reward {rewardId} for user {externalUserId}: {error}");
135
+
136
+ try {
137
+ var errorDoc = JsonDocument.Parse(error);
138
+ if (errorDoc.RootElement.TryGetProperty("message", out var msgElement)) {
139
+ return (false, msgElement.GetString() ?? "Failed to claim reward.");
140
+ }
141
+ } catch { }
142
+
143
+ return (false, string.IsNullOrEmpty(error) ? "Failed to claim reward." : error);
144
+ }
145
+ catch (Exception ex)
146
+ {
147
+ _logger.LogError(ex, "Error while claiming reward.");
148
+ return (false, "An error occurred while processing your request.");
149
+ }
150
+ }
151
+
152
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetUserRedemptionsAsync(string externalUserId)
153
+ {
154
+ try
155
+ {
156
+ var response = await _httpClient.GetAsync($"/api/v1/redemption/history/{SystemId}/{externalUserId}");
157
+ if (response.IsSuccessStatusCode)
158
+ {
159
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
160
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
161
+ }
162
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
163
+ }
164
+ catch (Exception ex)
165
+ {
166
+ _logger.LogError(ex, $"Error fetching redemptions for user {externalUserId}.");
167
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
168
+ }
169
+ }
170
+
171
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync()
172
+ {
173
+ try
174
+ {
175
+ var response = await _httpClient.GetAsync("/api/v1/admin/redemptions/pending");
176
+ if (response.IsSuccessStatusCode)
177
+ {
178
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
179
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
180
+ }
181
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
182
+ }
183
+ catch (Exception ex)
184
+ {
185
+ _logger.LogError(ex, "Error fetching pending redemptions.");
186
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
187
+ }
188
+ }
189
+
190
+ public async Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status)
191
+ {
192
+ try
193
+ {
194
+ var response = await _httpClient.PutAsJsonAsync($"/api/v1/admin/redemptions/{redemptionId}/status", new { status });
195
+ return response.IsSuccessStatusCode;
196
+ }
197
+ catch (Exception ex)
198
+ {
199
+ _logger.LogError(ex, $"Error updating redemption status to {status} for {redemptionId}.");
200
+ return false;
201
+ }
202
+ }
203
  }
204
 
205
  public class AccountLookupResponse
Backend/Features/Subscriptions/SubscriptionController.cs CHANGED
@@ -66,6 +66,13 @@ namespace Backend.Features.Subscriptions
66
  return Ok(subscription);
67
  }
68
 
 
 
 
 
 
 
 
69
  [HttpPost("subscriptions/admin-subscribe")]
70
  [Authorize(Roles = "Librarian")]
71
  public async Task<ActionResult<SubscriptionDto>> AdminSubscribe([FromBody] AdminSubscribeRequest request)
 
66
  return Ok(subscription);
67
  }
68
 
69
+ [HttpGet("subscriptions/all")]
70
+ [Authorize(Roles = "Librarian")]
71
+ public async Task<ActionResult<IEnumerable<SubscriptionDto>>> GetSubscriptions()
72
+ {
73
+ return Ok(await _subscriptionService.GetAllSubscriptionsAsync());
74
+ }
75
+
76
  [HttpPost("subscriptions/admin-subscribe")]
77
  [Authorize(Roles = "Librarian")]
78
  public async Task<ActionResult<SubscriptionDto>> AdminSubscribe([FromBody] AdminSubscribeRequest request)
Backend/Features/Subscriptions/SubscriptionModels.cs CHANGED
@@ -11,6 +11,7 @@ namespace Backend.Features.Subscriptions
11
  public decimal Price { get; set; }
12
  public string Currency { get; set; } = "MMK";
13
  public int DurationMonths { get; set; }
 
14
  }
15
 
16
  public class SubscriptionDto
@@ -19,6 +20,8 @@ namespace Backend.Features.Subscriptions
19
  public int UserId { get; set; }
20
  public int MembershipId { get; set; }
21
  public string MembershipType { get; set; } = string.Empty;
 
 
22
  public DateTime StartDate { get; set; }
23
  public DateTime ExpiryDate { get; set; }
24
  public bool IsActive { get; set; }
 
11
  public decimal Price { get; set; }
12
  public string Currency { get; set; } = "MMK";
13
  public int DurationMonths { get; set; }
14
+ public string? LoyaltyRewardId { get; set; }
15
  }
16
 
17
  public class SubscriptionDto
 
20
  public int UserId { get; set; }
21
  public int MembershipId { get; set; }
22
  public string MembershipType { get; set; } = string.Empty;
23
+ public string UserEmail { get; set; } = string.Empty;
24
+ public string UserName { get; set; } = string.Empty;
25
  public DateTime StartDate { get; set; }
26
  public DateTime ExpiryDate { get; set; }
27
  public bool IsActive { get; set; }
Backend/Features/Subscriptions/SubscriptionService.cs CHANGED
@@ -8,7 +8,9 @@ namespace Backend.Features.Subscriptions
8
  {
9
  Task<IEnumerable<MembershipDto>> GetMembershipsAsync();
10
  Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId);
 
11
  Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId);
 
12
  }
13
 
14
  public class SubscriptionService : ISubscriptionService
@@ -32,7 +34,8 @@ namespace Backend.Features.Subscriptions
32
  MaxBooks = m.MaxBooks,
33
  BorrowingDays = m.BorrowingDays,
34
  Price = m.Price,
35
- DurationMonths = m.DurationMonths
 
36
  }).ToListAsync();
37
  }
38
 
@@ -49,6 +52,33 @@ namespace Backend.Features.Subscriptions
49
  return MapToDto(subscription);
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  public async Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId)
53
  {
54
  var membership = await _context.Memberships.FindAsync(membershipId);
@@ -98,6 +128,17 @@ namespace Backend.Features.Subscriptions
98
  return MapToDto(subscription);
99
  }
100
 
 
 
 
 
 
 
 
 
 
 
 
101
  private static SubscriptionDto MapToDto(UserSubscription subscription)
102
  {
103
  return new SubscriptionDto
@@ -106,6 +147,8 @@ namespace Backend.Features.Subscriptions
106
  UserId = subscription.UserId,
107
  MembershipId = subscription.MembershipId,
108
  MembershipType = subscription.Membership.Type,
 
 
109
  StartDate = subscription.StartDate,
110
  ExpiryDate = subscription.ExpiryDate,
111
  IsActive = subscription.IsActive
 
8
  {
9
  Task<IEnumerable<MembershipDto>> GetMembershipsAsync();
10
  Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId);
11
+ Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string id);
12
  Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId);
13
+ Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync();
14
  }
15
 
16
  public class SubscriptionService : ISubscriptionService
 
34
  MaxBooks = m.MaxBooks,
35
  BorrowingDays = m.BorrowingDays,
36
  Price = m.Price,
37
+ DurationMonths = m.DurationMonths,
38
+ LoyaltyRewardId = m.LoyaltyRewardId
39
  }).ToListAsync();
40
  }
41
 
 
52
  return MapToDto(subscription);
53
  }
54
 
55
+ public async Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string id)
56
+ {
57
+ // externalUserId matches how other loyalty calls identify users
58
+ string externalUserId = userId.ToString();
59
+
60
+ try
61
+ {
62
+ // Attempt to claim the reward via the loyalty service
63
+ var (success, message) = await _loyaltyService.ClaimRewardAsync(externalUserId, rewardId, id);
64
+
65
+ if (!success)
66
+ {
67
+ return false;
68
+ }
69
+
70
+ // Optionally update the redemption status in the loyalty system
71
+ await _loyaltyService.UpdateRedemptionStatusAsync(id, "CLAIMED");
72
+
73
+ return true;
74
+ }
75
+ catch
76
+ {
77
+ // Swallow exceptions to maintain interface contract; caller can handle false result
78
+ return false;
79
+ }
80
+ }
81
+
82
  public async Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId)
83
  {
84
  var membership = await _context.Memberships.FindAsync(membershipId);
 
128
  return MapToDto(subscription);
129
  }
130
 
131
+ public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
132
+ {
133
+ var subscriptions = await _context.UserSubscriptions
134
+ .Include(s => s.Membership)
135
+ .Include(s => s.User)
136
+ .OrderByDescending(s => s.StartDate)
137
+ .ToListAsync();
138
+
139
+ return subscriptions.Select(MapToDto);
140
+ }
141
+
142
  private static SubscriptionDto MapToDto(UserSubscription subscription)
143
  {
144
  return new SubscriptionDto
 
147
  UserId = subscription.UserId,
148
  MembershipId = subscription.MembershipId,
149
  MembershipType = subscription.Membership.Type,
150
+ UserEmail = subscription.User?.Email ?? "Unknown",
151
+ UserName = subscription.User?.FullName ?? "Unknown",
152
  StartDate = subscription.StartDate,
153
  ExpiryDate = subscription.ExpiryDate,
154
  IsActive = subscription.IsActive
Database/Models/Membership.cs CHANGED
@@ -1,4 +1,4 @@
1
- using System;
2
  using System.Collections.Generic;
3
 
4
  namespace Database.Models;
@@ -16,6 +16,7 @@ public partial class Membership
16
  public decimal Price { get; set; }
17
 
18
  public int DurationMonths { get; set; }
 
19
 
20
  public virtual ICollection<UserSubscription> UserSubscriptions { get; set; } = new List<UserSubscription>();
21
  }
 
1
+ using System;
2
  using System.Collections.Generic;
3
 
4
  namespace Database.Models;
 
16
  public decimal Price { get; set; }
17
 
18
  public int DurationMonths { get; set; }
19
+ public string? LoyaltyRewardId { get; set; }
20
 
21
  public virtual ICollection<UserSubscription> UserSubscriptions { get; set; } = new List<UserSubscription>();
22
  }
Database/add_reward_id_to_memberships.sql ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USE LibraryManagement;
2
+ GO
3
+
4
+ -- Add RewardId column if it doesn't exist
5
+ IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Memberships') AND name = 'RewardId')
6
+ BEGIN
7
+ ALTER TABLE Memberships ADD RewardId NVARCHAR(50) NULL;
8
+ END
9
+ GO
10
+
11
+ -- Populate RewardId for existing memberships
12
+ UPDATE Memberships SET RewardId = '39aff010-693e-450e-85e7-ae6c3f1a2078' WHERE Type = 'Basic Monthly';
13
+ UPDATE Memberships SET RewardId = '94bc0b20-1a87-4e10-a778-dd48552470d0' WHERE Type = 'Basic Yearly';
14
+ GO
Database/db.sql CHANGED
@@ -57,6 +57,7 @@ CREATE TABLE Memberships (
57
  BorrowingDays INT NOT NULL,
58
  Price DECIMAL(10, 2) NOT NULL,
59
  DurationMonths INT NOT NULL,
 
60
  CONSTRAINT IX_Memberships_Type UNIQUE (Type)
61
  );
62
  GO
 
57
  BorrowingDays INT NOT NULL,
58
  Price DECIMAL(10, 2) NOT NULL,
59
  DurationMonths INT NOT NULL,
60
+ LoyaltyRewardId NVARCHAR(50) NULL,
61
  CONSTRAINT IX_Memberships_Type UNIQUE (Type)
62
  );
63
  GO
Database/remove_total_membership.sql ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- EXTREMELY AGGRESSIVE script to remove TotalMembership and ALL its dependencies
2
+ USE LibraryManagement;
3
+ GO
4
+
5
+ PRINT 'Starting aggressive cleanup of TotalMembership column...';
6
+
7
+ -- 1. Specifically drop the constraint the user mentioned if it exists
8
+ IF EXISTS (SELECT * FROM sys.objects WHERE name = 'DF__UserSubsc__Total__5DCAEF64' AND type = 'D')
9
+ BEGIN
10
+ PRINT 'Explicitly dropping known constraint DF__UserSubsc__Total__5DCAEF64';
11
+ ALTER TABLE UserSubscriptions DROP CONSTRAINT DF__UserSubsc__Total__5DCAEF64;
12
+ END
13
+
14
+ -- 2. Dynamically find and drop ALL other default constraints on this column
15
+ DECLARE @SQL nvarchar(MAX) = '';
16
+ SELECT @SQL += 'ALTER TABLE UserSubscriptions DROP CONSTRAINT ' + d.name + ';' + CHAR(13)
17
+ FROM sys.default_constraints d
18
+ INNER JOIN sys.columns c ON d.parent_column_id = c.column_id AND d.parent_object_id = c.object_id
19
+ WHERE c.object_id = OBJECT_ID('UserSubscriptions')
20
+ AND c.name = 'TotalMembership';
21
+
22
+ IF @SQL <> ''
23
+ BEGIN
24
+ PRINT 'Dropping additional dynamic constraints...';
25
+ EXEC sp_executesql @SQL;
26
+ END
27
+
28
+ -- 3. Find and drop any Indexes that use this column
29
+ SET @SQL = '';
30
+ SELECT @SQL += 'DROP INDEX ' + i.name + ' ON UserSubscriptions;' + CHAR(13)
31
+ FROM sys.indexes i
32
+ INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
33
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
34
+ WHERE i.object_id = OBJECT_ID('UserSubscriptions')
35
+ AND c.name = 'TotalMembership';
36
+
37
+ IF @SQL <> ''
38
+ BEGIN
39
+ PRINT 'Dropping dependent indexes...';
40
+ EXEC sp_executesql @SQL;
41
+ END
42
+
43
+ -- 4. Check for and drop foreign keys if they somehow depend on this (rare for this name but possible)
44
+ SET @SQL = '';
45
+ SELECT @SQL += 'ALTER TABLE UserSubscriptions DROP CONSTRAINT ' + fk.name + ';' + CHAR(13)
46
+ FROM sys.foreign_keys fk
47
+ INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
48
+ INNER JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id
49
+ WHERE fk.parent_object_id = OBJECT_ID('UserSubscriptions')
50
+ AND c.name = 'TotalMembership';
51
+
52
+ IF @SQL <> ''
53
+ BEGIN
54
+ PRINT 'Dropping dependent foreign keys...';
55
+ EXEC sp_executesql @SQL;
56
+ END
57
+
58
+ -- 5. Finally, drop the column
59
+ IF EXISTS (
60
+ SELECT * FROM sys.columns
61
+ WHERE object_id = OBJECT_ID('UserSubscriptions')
62
+ AND name = 'TotalMembership'
63
+ )
64
+ BEGIN
65
+ ALTER TABLE UserSubscriptions DROP COLUMN TotalMembership;
66
+ PRINT 'SUCCESS: Dropped column TotalMembership from UserSubscriptions.';
67
+ END
68
+ ELSE
69
+ BEGIN
70
+ PRINT 'Done: Column TotalMembership does not exist in UserSubscriptions.';
71
+ END
72
+ GO
Frontend/Controllers/MembershipController.cs ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using Frontend.Services;
3
+ using Frontend.Models.Dtos;
4
+ using Frontend.Models;
5
+ using Microsoft.AspNetCore.Authorization;
6
+ using System.Threading.Tasks;
7
+ using System.Linq;
8
+ using System.Collections.Generic;
9
+
10
+ namespace Frontend.Controllers
11
+ {
12
+ [Authorize(Roles = "Member")]
13
+ public class MembershipController : Controller
14
+ {
15
+ private readonly LibraryApiClient _apiClient;
16
+
17
+ public MembershipController(LibraryApiClient apiClient)
18
+ {
19
+ _apiClient = apiClient;
20
+ }
21
+
22
+ public async Task<IActionResult> Index()
23
+ {
24
+ var subscription = await _apiClient.GetMySubscriptionAsync();
25
+ var memberships = await _apiClient.GetMembershipsAsync();
26
+ var redemptions = await _apiClient.GetMyRedemptionsAsync();
27
+ var loyaltyAccount = await _apiClient.GetMyLoyaltyAccountAsync();
28
+
29
+ // Filter redemptions to only show memberships that are Pending (Queued)
30
+ // We use LoyaltyRewardId to identify which redemptions are memberships
31
+ var membershipRewardIds = memberships
32
+ .Where(m => !string.IsNullOrEmpty(m.LoyaltyRewardId))
33
+ .Select(m => m.LoyaltyRewardId)
34
+ .ToList();
35
+
36
+ var queuedMemberships = redemptions
37
+ .Where(r => (r.Status == "Pending" || r.Status == "PENDING") && membershipRewardIds.Contains(r.LoyaltyRewardId))
38
+ .ToList();
39
+
40
+ var viewModel = new MembershipViewModel
41
+ {
42
+ CurrentSubscription = subscription,
43
+ AvailableMemberships = memberships.ToList(),
44
+ QueuedMemberships = queuedMemberships,
45
+ LoyaltyAccount = loyaltyAccount
46
+ };
47
+
48
+ return View(viewModel);
49
+ }
50
+ }
51
+ }
Frontend/Controllers/SubscriptionsController.cs CHANGED
@@ -21,10 +21,12 @@ namespace Frontend.Controllers
21
  public async Task<IActionResult> Index()
22
  {
23
  var memberships = await _apiClient.GetMembershipsAsync();
 
24
 
25
  var viewModel = new SubscriptionsViewModel
26
  {
27
- AvailableMemberships = memberships?.ToList() ?? new List<MembershipDto>()
 
28
  };
29
 
30
  return View(viewModel);
 
21
  public async Task<IActionResult> Index()
22
  {
23
  var memberships = await _apiClient.GetMembershipsAsync();
24
+ var activeSubscriptions = await _apiClient.GetAllSubscriptionsAsync();
25
 
26
  var viewModel = new SubscriptionsViewModel
27
  {
28
+ AvailableMemberships = memberships?.ToList() ?? new List<MembershipDto>(),
29
+ ActiveSubscriptions = activeSubscriptions?.ToList() ?? new List<SubscriptionDto>()
30
  };
31
 
32
  return View(viewModel);
Frontend/Models/Dtos/LibraryDtos.cs CHANGED
@@ -102,6 +102,7 @@ namespace Frontend.Models.Dtos
102
  public int BorrowingDays { get; set; }
103
  public decimal Price { get; set; }
104
  public int DurationMonths { get; set; }
 
105
  }
106
 
107
  public class SubscriptionDto
@@ -110,9 +111,12 @@ namespace Frontend.Models.Dtos
110
  public int UserId { get; set; }
111
  public int MembershipId { get; set; }
112
  public string MembershipType { get; set; } = string.Empty;
 
 
113
  public DateTime StartDate { get; set; }
114
  public DateTime ExpiryDate { get; set; }
115
  public bool IsActive { get; set; }
 
116
  }
117
 
118
  public class SubscribeRequest
 
102
  public int BorrowingDays { get; set; }
103
  public decimal Price { get; set; }
104
  public int DurationMonths { get; set; }
105
+ public string? LoyaltyRewardId { get; set; }
106
  }
107
 
108
  public class SubscriptionDto
 
111
  public int UserId { get; set; }
112
  public int MembershipId { get; set; }
113
  public string MembershipType { get; set; } = string.Empty;
114
+ public string UserName { get; set; } = string.Empty;
115
+ public string UserEmail { get; set; } = string.Empty;
116
  public DateTime StartDate { get; set; }
117
  public DateTime ExpiryDate { get; set; }
118
  public bool IsActive { get; set; }
119
+ public bool IsExpired => DateTime.UtcNow > ExpiryDate;
120
  }
121
 
122
  public class SubscribeRequest
Frontend/Models/Dtos/LoyaltyRedemptionDto.cs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+
3
+ namespace Frontend.Models.Dtos
4
+ {
5
+ public class LoyaltyRedemptionDto
6
+ {
7
+ public Guid Id { get; set; }
8
+ public string ExternalUserId { get; set; } = string.Empty;
9
+ public string RewardName { get; set; } = string.Empty;
10
+ public string? LoyaltyRewardId { get; set; }
11
+ public int PointsSpent { get; set; }
12
+ public string Status { get; set; } = string.Empty;
13
+ public DateTime CreatedAt { get; set; }
14
+ }
15
+ }
Frontend/Models/MembershipViewModel.cs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Frontend.Models.Dtos;
2
+ using System.Collections.Generic;
3
+
4
+ namespace Frontend.Models
5
+ {
6
+ public class MembershipViewModel
7
+ {
8
+ public SubscriptionDto? CurrentSubscription { get; set; }
9
+ public List<MembershipDto> AvailableMemberships { get; set; } = new List<MembershipDto>();
10
+ public List<LoyaltyRedemptionDto> QueuedMemberships { get; set; } = new List<LoyaltyRedemptionDto>();
11
+ public LoyaltyAccountDto? LoyaltyAccount { get; set; }
12
+ }
13
+ }
Frontend/Models/SubscriptionsViewModel.cs CHANGED
@@ -5,5 +5,6 @@ namespace Frontend.Models
5
  public class SubscriptionsViewModel
6
  {
7
  public List<MembershipDto> AvailableMemberships { get; set; } = new();
 
8
  }
9
  }
 
5
  public class SubscriptionsViewModel
6
  {
7
  public List<MembershipDto> AvailableMemberships { get; set; } = new();
8
+ public List<SubscriptionDto> ActiveSubscriptions { get; set; } = new();
9
  }
10
  }
Frontend/Services/LibraryApiClient.cs CHANGED
@@ -157,6 +157,11 @@ namespace Frontend.Services
157
  var response = await _httpClient.PostAsJsonAsync("api/subscriptions/admin-subscribe", request);
158
  return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
159
  }
 
 
 
 
 
160
  #endregion
161
 
162
  public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
@@ -183,6 +188,13 @@ namespace Frontend.Services
183
  }
184
  }
185
 
 
 
 
 
 
 
 
186
  public async Task<bool> RequestReturnAsync(int borrowingId)
187
  {
188
  var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
 
157
  var response = await _httpClient.PostAsJsonAsync("api/subscriptions/admin-subscribe", request);
158
  return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
159
  }
160
+
161
+ public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
162
+ {
163
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
164
+ }
165
  #endregion
166
 
167
  public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
 
188
  }
189
  }
190
 
191
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetMyRedemptionsAsync()
192
+ {
193
+ try {
194
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>("api/Loyalty/my-redemptions") ?? Enumerable.Empty<LoyaltyRedemptionDto>();
195
+ } catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
196
+ }
197
+
198
  public async Task<bool> RequestReturnAsync(int borrowingId)
199
  {
200
  var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
Frontend/Views/Membership/Index.cshtml ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model Frontend.Models.MembershipViewModel
2
+
3
+ @{
4
+ ViewData["Title"] = "My Membership";
5
+ var hasActiveSub = Model.CurrentSubscription != null && Model.CurrentSubscription.IsActive;
6
+ }
7
+
8
+ <div class="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
9
+ <!-- Header with Loyalty Summary -->
10
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white p-8 rounded-2xl shadow-sm border border-slate-100">
11
+ <div>
12
+ <h1 class="text-3xl font-extrabold text-slate-900 tracking-tight">Membership</h1>
13
+ <p class="text-slate-500 mt-1">Manage your subscription and library benefits</p>
14
+ </div>
15
+
16
+ @if (Model.LoyaltyAccount != null)
17
+ {
18
+ <div class="flex items-center gap-4 bg-slate-50 px-6 py-4 rounded-xl border border-slate-200/60">
19
+ <div class="bg-amber-100 p-2.5 rounded-lg text-amber-600">
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg>
21
+ </div>
22
+ <div>
23
+ <div class="text-xs font-bold text-slate-400 uppercase tracking-widest">Available Points</div>
24
+ <div class="text-2xl font-black text-slate-900 leading-none">@Model.LoyaltyAccount.CurrentBalance.ToString("N0")</div>
25
+ </div>
26
+ </div>
27
+ }
28
+ </div>
29
+
30
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
31
+ <!-- Current Subscription Card -->
32
+ <div class="lg:col-span-2 space-y-8">
33
+ <section>
34
+ <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
35
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-slate-400"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
36
+ Active Plan
37
+ </h2>
38
+
39
+ @if (hasActiveSub)
40
+ {
41
+ <div class="relative overflow-hidden bg-slate-900 rounded-3xl p-8 text-white group shadow-xl">
42
+ <!-- Decorative element -->
43
+ <div class="absolute -right-20 -top-20 w-64 h-64 bg-white/5 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-500"></div>
44
+
45
+ <div class="relative z-10">
46
+ <div class="flex justify-between items-start mb-10">
47
+ <div>
48
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-green-500/20 text-green-400 border border-green-500/30 mb-3 uppercase tracking-wider">Active Status</span>
49
+ <h3 class="text-4xl font-black tracking-tight">@Model.CurrentSubscription.MembershipType</h3>
50
+ </div>
51
+ <div class="h-14 w-14 bg-white/10 rounded-2xl flex items-center justify-center backdrop-blur-md">
52
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="grid grid-cols-2 gap-8 py-8 border-t border-white/10">
57
+ <div>
58
+ <div class="text-slate-400 text-sm font-medium mb-1">Start Date</div>
59
+ <div class="text-lg font-bold">@Model.CurrentSubscription.StartDate.ToString("MMM dd, yyyy")</div>
60
+ </div>
61
+ <div>
62
+ <div class="text-slate-400 text-sm font-medium mb-1">Expiry Date</div>
63
+ <div class="text-lg font-bold">@Model.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="pt-6 flex items-center gap-2 text-slate-400 text-sm italic">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
69
+ Pro-tip: Maintain your subscription to earn loyalty points every month!
70
+ </div>
71
+ </div>
72
+ </div>
73
+ }
74
+ else
75
+ {
76
+ <div class="bg-white border-2 border-dashed border-slate-200 rounded-3xl p-12 text-center">
77
+ <div class="bg-slate-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 text-slate-400">
78
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
79
+ </div>
80
+ <h3 class="text-xl font-bold text-slate-900">No Active Subscription</h3>
81
+ <p class="text-slate-500 mt-2 mb-8 max-w-xs mx-auto">Get a membership to borrow books and unlock premium library features.</p>
82
+ <a href="#plans" class="btn-primary px-8 py-3 rounded-xl shadow-lg shadow-slate-900/10">Browse Plans</a>
83
+ </div>
84
+ }
85
+ </section>
86
+
87
+ <!-- Queued Memberships -->
88
+ @if (Model.QueuedMemberships.Any())
89
+ {
90
+ <section>
91
+ <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-amber-500"><path d="M10 10 2.5 2.5"/><path d="M21.5 2.5 14 10"/><path d="m21.5 21.5-7.5-7.5"/><path d="M10 14 2.5 21.5"/><circle cx="12" cy="12" r="3"/></svg>
93
+ Queued Membership
94
+ </h2>
95
+ <div class="space-y-3">
96
+ @foreach (var queued in Model.QueuedMemberships)
97
+ {
98
+ <div class="flex items-center justify-between bg-amber-50 border border-amber-100 p-5 rounded-2xl shadow-sm">
99
+ <div class="flex items-center gap-4">
100
+ <div class="h-12 w-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shadow-sm border border-amber-100">
101
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
102
+ </div>
103
+ <div>
104
+ <div class="font-bold text-slate-900">@queued.RewardName</div>
105
+ <div class="text-sm text-slate-500">Redeemed on @queued.CreatedAt.ToString("MMM dd, yyyy")</div>
106
+ </div>
107
+ </div>
108
+ <div class="text-right">
109
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700 border border-amber-200 uppercase tracking-wider">Pending Approval</span>
110
+ <div class="text-xs text-slate-400 mt-1">Librarian will review soon</div>
111
+ </div>
112
+ </div>
113
+ }
114
+ </div>
115
+ </section>
116
+ }
117
+ </div>
118
+
119
+ <!-- Right Side Panel: Benefits -->
120
+ <div class="space-y-8">
121
+ <div class="bg-white border border-slate-100 rounded-3xl p-8 shadow-sm">
122
+ <h3 class="text-lg font-bold text-slate-900 mb-6">Your Benefits</h3>
123
+ <ul class="space-y-6">
124
+ @{
125
+ var currentPlan = Model.AvailableMemberships.FirstOrDefault(m => hasActiveSub && m.Type == Model.CurrentSubscription.MembershipType);
126
+ }
127
+ <li class="flex gap-4">
128
+ <div class="shrink-0 h-10 w-10 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.MaxBooks ?? 1))</div>
129
+ <div>
130
+ <div class="font-bold text-slate-900">Max Books</div>
131
+ <div class="text-sm text-slate-500">Number of books you can borrow simultaneously.</div>
132
+ </div>
133
+ </li>
134
+ <li class="flex gap-4">
135
+ <div class="shrink-0 h-10 w-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.BorrowingDays ?? 14))</div>
136
+ <div>
137
+ <div class="font-bold text-slate-900">Borrowing Days</div>
138
+ <div class="text-sm text-slate-500">Duration you can keep each borrowed book.</div>
139
+ </div>
140
+ </li>
141
+ <li class="flex gap-4">
142
+ <div class="shrink-0 h-10 w-10 bg-violet-50 text-violet-600 rounded-xl flex items-center justify-center">
143
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
144
+ </div>
145
+ <div>
146
+ <div class="font-bold text-slate-900">Digital Access</div>
147
+ <div class="text-sm text-slate-500">Browse and search the full library catalog online.</div>
148
+ </div>
149
+ </li>
150
+ </ul>
151
+
152
+ <div class="mt-10 p-5 bg-slate-900 rounded-2xl text-white text-center">
153
+ <p class="text-sm font-medium mb-3">Want more books?</p>
154
+ <a href="#plans" class="text-sm font-bold text-indigo-400 hover:text-indigo-300 transition-colors">Upgrade Plan &rarr;</a>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Explore All Plans Section -->
161
+ <section id="plans" class="pt-10">
162
+ <div class="text-center mb-10">
163
+ <h2 class="text-3xl font-black text-slate-900">Explore Membership Plans</h2>
164
+ <p class="text-slate-500 mt-2">Find the perfect library experience for your reading habits</p>
165
+ </div>
166
+
167
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
168
+ @foreach (var plan in Model.AvailableMemberships)
169
+ {
170
+ var isCurrent = hasActiveSub && plan.Type == Model.CurrentSubscription.MembershipType;
171
+
172
+ <div class="group relative bg-white border @(isCurrent ? "border-slate-900 ring-2 ring-slate-900 ring-offset-2" : "border-slate-200") rounded-3xl p-8 transition-all duration-300 hover:shadow-2xl hover:-translate-y-2">
173
+ @if (isCurrent)
174
+ {
175
+ <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-slate-900 text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest shadow-xl">Current Plan</div>
176
+ }
177
+
178
+ <div class="mb-8">
179
+ <h3 class="text-sm font-black text-slate-400 uppercase tracking-widest mb-2">@plan.Type</h3>
180
+ <div class="flex items-baseline">
181
+ <span class="text-3xl font-black text-slate-900">@plan.Price.ToString("N0")</span>
182
+ <span class="text-slate-500 font-bold ml-1 text-sm uppercase">MMK</span>
183
+ </div>
184
+ <div class="text-slate-400 text-xs font-bold mt-1">/ @(plan.DurationMonths == 1 ? "Month" : $"{plan.DurationMonths} Months")</div>
185
+ </div>
186
+
187
+ <ul class="space-y-4 mb-10">
188
+ <li class="flex items-center gap-3 text-sm">
189
+ <div class="h-5 w-5 rounded-full bg-slate-100 flex items-center justify-center text-slate-900">
190
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
191
+ </div>
192
+ <span class="text-slate-600 font-medium font-bold">@plan.MaxBooks Books limit</span>
193
+ </li>
194
+ <li class="flex items-center gap-3 text-sm">
195
+ <div class="h-5 w-5 rounded-full bg-slate-100 flex items-center justify-center text-slate-900">
196
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
197
+ </div>
198
+ <span class="text-slate-600 font-medium font-bold">@plan.BorrowingDays Days keep</span>
199
+ </li>
200
+ </ul>
201
+
202
+ @if (isCurrent)
203
+ {
204
+ <button disabled class="w-full py-4 rounded-2xl bg-slate-100 text-slate-400 font-bold text-sm cursor-not-allowed">Active Now</button>
205
+ }
206
+ else
207
+ {
208
+ <a href="#" class="block w-full py-4 rounded-2xl bg-white border-2 border-slate-900 text-slate-900 text-center font-bold text-sm transition-all duration-200 group-hover:bg-slate-900 group-hover:text-white">Select Plan</a>
209
+ }
210
+ </div>
211
+ }
212
+ </div>
213
+ </section>
214
+ </div>
215
+
216
+ <style>
217
+ body {
218
+ background-color: #f8fafc;
219
+ }
220
+ </style>
Frontend/Views/Shared/_Layout.cshtml CHANGED
@@ -79,6 +79,13 @@
79
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
80
  Active Loans
81
  </a>
 
 
 
 
 
 
 
82
  @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Librarian"))
83
  {
84
  <a href="/Members" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Members" ? "nav-link-active" : "nav-link-inactive")">
 
79
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
80
  Active Loans
81
  </a>
82
+ @if (User.Identity?.IsAuthenticated == true && !User.IsInRole("Librarian"))
83
+ {
84
+ <a asp-controller="Membership" asp-action="Index" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Membership" ? "nav-link-active" : "nav-link-inactive")">
85
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
86
+ My Membership
87
+ </a>
88
+ }
89
  @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Librarian"))
90
  {
91
  <a href="/Members" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Members" ? "nav-link-active" : "nav-link-inactive")">
Frontend/Views/Subscriptions/Index.cshtml CHANGED
@@ -45,3 +45,62 @@
45
  }
46
  </div>
47
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
  </div>
47
  </div>
48
+
49
+ <!-- Active Subscriptions Section -->
50
+ <div class="mt-12 bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
51
+ <div class="px-6 py-4 border-b border-slate-200 bg-slate-50 flex items-center justify-between">
52
+ <h2 class="text-sm font-bold text-slate-900 uppercase tracking-wider">Active User Subscriptions</h2>
53
+ <span class="px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-900 text-white">@Model.ActiveSubscriptions.Count Total</span>
54
+ </div>
55
+ <div class="overflow-x-auto">
56
+ <table class="min-w-full divide-y divide-slate-200">
57
+ <thead class="bg-slate-50">
58
+ <tr>
59
+ <th class="px-6 py-3 text-left text-[10px] font-black text-slate-500 uppercase tracking-widest">User</th>
60
+ <th class="px-6 py-3 text-left text-[10px] font-black text-slate-500 uppercase tracking-widest">Plan Type</th>
61
+ <th class="px-6 py-3 text-left text-[10px] font-black text-slate-500 uppercase tracking-widest">Start Date</th>
62
+ <th class="px-6 py-3 text-left text-[10px] font-black text-slate-500 uppercase tracking-widest">Expiry Date</th>
63
+ <th class="px-6 py-3 text-left text-[10px] font-black text-slate-500 uppercase tracking-widest">Status</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody class="bg-white divide-y divide-slate-200">
67
+ @if (!Model.ActiveSubscriptions.Any())
68
+ {
69
+ <tr>
70
+ <td colspan="5" class="px-6 py-12 text-center text-slate-500 text-sm italic">
71
+ No active subscriptions found in the system.
72
+ </td>
73
+ </tr>
74
+ }
75
+ @foreach (var sub in Model.ActiveSubscriptions)
76
+ {
77
+ <tr class="hover:bg-slate-50 transition-colors">
78
+ <td class="px-6 py-4">
79
+ <div class="text-sm font-bold text-slate-900">@sub.UserName</div>
80
+ <div class="text-xs text-slate-400">@sub.UserEmail</div>
81
+ </td>
82
+ <td class="px-6 py-4">
83
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-slate-900 text-white tracking-tight">@sub.MembershipType</span>
84
+ </td>
85
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600">@sub.StartDate.ToString("MMM dd, yyyy")</td>
86
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600 font-bold">@sub.ExpiryDate.ToString("MMM dd, yyyy")</td>
87
+ <td class="px-6 py-4 whitespace-nowrap">
88
+ @if (sub.IsActive && !sub.IsExpired)
89
+ {
90
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
91
+ }
92
+ else if (sub.IsExpired)
93
+ {
94
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Expired</span>
95
+ }
96
+ else
97
+ {
98
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-800">Inactive</span>
99
+ }
100
+ </td>
101
+ </tr>
102
+ }
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ </div>
Frontend/Views/Subscriptions/LoyaltyClaims.cshtml ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model List<Frontend.Models.Dtos.LoyaltyRedemptionDto>
2
+ @{
3
+ ViewData["Title"] = "Loyalty Reward Claims";
4
+ }
5
+
6
+ <div class="p-8">
7
+ <div class="flex items-center justify-between mb-8">
8
+ <div>
9
+ <h1 class="text-3xl font-bold text-slate-900">Loyalty Reward Claims</h1>
10
+ <p class="text-slate-500 mt-1">Review and fulfill member reward redemptions.</p>
11
+ </div>
12
+ <div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-lg border border-blue-100 flex items-center gap-2">
13
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
14
+ <span class="text-sm font-semibold">@Model.Count Pending Claims</span>
15
+ </div>
16
+ </div>
17
+
18
+ @if (TempData["Success"] != null)
19
+ {
20
+ <div class="mb-6 p-4 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-xl flex items-center gap-3">
21
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
22
+ <span class="font-medium">@TempData["Success"]</span>
23
+ </div>
24
+ }
25
+
26
+ @if (TempData["Error"] != null)
27
+ {
28
+ <div class="mb-6 p-4 bg-rose-50 border border-rose-200 text-rose-700 rounded-xl flex items-center gap-3">
29
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
30
+ <span class="font-medium">@TempData["Error"]</span>
31
+ </div>
32
+ }
33
+
34
+ <div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
35
+ <table class="w-full text-left border-collapse">
36
+ <thead>
37
+ <tr class="bg-slate-50 border-b border-slate-200">
38
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Member ID</th>
39
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Reward</th>
40
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Points Spent</th>
41
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Date</th>
42
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Actions</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody class="divide-y divide-slate-100">
46
+ @if (!Model.Any())
47
+ {
48
+ <tr>
49
+ <td colspan="5" class="px-6 py-12 text-center text-slate-400 italic">
50
+ No pending reward claims found.
51
+ </td>
52
+ </tr>
53
+ }
54
+ @foreach (var claim in Model)
55
+ {
56
+ <tr class="hover:bg-slate-50 transition-colors">
57
+ <td class="px-6 py-4">
58
+ <div class="flex items-center gap-3">
59
+ <div class="h-10 w-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-xs shadow-sm border border-blue-200 uppercase">
60
+ @(claim.ExternalUserId?.Length >= 1 ? claim.ExternalUserId.Substring(0, Math.Min(2, claim.ExternalUserId.Length)) : "??")
61
+ </div>
62
+ <div class="flex flex-col">
63
+ <span class="font-bold text-slate-800 text-sm">Member #@claim.ExternalUserId</span>
64
+ <span class="text-[10px] text-slate-400 font-medium uppercase tracking-tighter">Library Account</span>
65
+ </div>
66
+ </div>
67
+ </td>
68
+ <td class="px-6 py-4">
69
+ <span class="font-semibold text-slate-900">@claim.RewardName</span>
70
+ </td>
71
+ <td class="px-6 py-4 text-slate-600">@claim.PointsSpent.ToString("N0") pts</td>
72
+ <td class="px-6 py-4 text-slate-500 text-sm">@claim.CreatedAt.ToString("MMM dd, yyyy")</td>
73
+ <td class="px-6 py-4 text-right">
74
+ <form asp-action="FulfillRedemption" method="post" class="inline">
75
+ <input type="hidden" name="id" value="@claim.Id" />
76
+ <button type="submit" class="px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition-all shadow-sm">
77
+ Fulfill Reward
78
+ </button>
79
+ </form>
80
+ </td>
81
+ </tr>
82
+ }
83
+ </tbody>
84
+ </table>
85
+ </div>
86
+ </div>