165 lines
7.2 KiB
C#
165 lines
7.2 KiB
C#
using Fitness_core_BE.DTOs.TimeBankCard;
|
|
using Fitness_core_BE.Helpers;
|
|
using Fitness_core_BE.Infra.UoW;
|
|
using Fitness_core_BE.Interfaces;
|
|
using Fitness_core_BE.Interfaces.Services;
|
|
using Fitness_core_BE.Models;
|
|
using System.Text.Json;
|
|
|
|
namespace Fitness_core_BE.Services
|
|
{
|
|
public class TimeBankCardService : ITimeBankCardService
|
|
{
|
|
private readonly ITimeBankCardRepository _timeBankCardRepository;
|
|
private readonly IUnitOfWork _uow;
|
|
private readonly UserRoleHelper _userRoleHelper;
|
|
|
|
public TimeBankCardService(ITimeBankCardRepository timeBankCardRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper)
|
|
{
|
|
_timeBankCardRepository = timeBankCardRepository;
|
|
_uow = uow;
|
|
_userRoleHelper = userRoleHelper;
|
|
}
|
|
|
|
public async Task<object> IssueCard(object payload, HttpContext httpContext)
|
|
{
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
var rfidTagId = data["rfidTagId"].GetString() ?? throw new ArgumentException("rfidTagId is required");
|
|
var totalMinutes = data["totalMinutesPurchased"].GetInt32();
|
|
if (totalMinutes < 1) throw new ArgumentException("totalMinutesPurchased must be at least 1");
|
|
|
|
if (await _timeBankCardRepository.GetByRfidTagId(rfidTagId) != null)
|
|
throw new ArgumentException("This RFID tag already has an active time-bank card");
|
|
|
|
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
|
|
|
var card = new TimeBankCard
|
|
{
|
|
FKClientId = clientId,
|
|
RfidTagId = rfidTagId,
|
|
TotalMinutesPurchased = totalMinutes,
|
|
MinutesRemaining = totalMinutes,
|
|
AmountPaid = data.ContainsKey("amountPaid") ? data["amountPaid"].GetDecimal() : 0m,
|
|
FKCustomerId = data.ContainsKey("customerId") && data["customerId"].ValueKind != JsonValueKind.Null
|
|
? Guid.Parse(data["customerId"].GetString()!)
|
|
: null,
|
|
ExpiresAt = data.ContainsKey("expiresAt") && data["expiresAt"].ValueKind != JsonValueKind.Null
|
|
? DateTime.Parse(data["expiresAt"].GetString()!).ToUniversalTime()
|
|
: null
|
|
};
|
|
|
|
var result = await _timeBankCardRepository.CreateTimeBankCard(card);
|
|
await _uow.CommitAsync();
|
|
|
|
return MapToDto(result);
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> Swipe(object payload)
|
|
{
|
|
await _uow.BeginAsync();
|
|
try
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
var rfidTagId = data["rfidTagId"].GetString() ?? throw new ArgumentException("rfidTagId is required");
|
|
var facilityId = Guid.Parse(data["facilityId"].GetString()!);
|
|
|
|
var card = await _timeBankCardRepository.GetByRfidTagId(rfidTagId)
|
|
?? throw new KeyNotFoundException("No active time-bank card found for this RFID tag");
|
|
|
|
if (card.ExpiresAt.HasValue && card.ExpiresAt.Value < DateTime.UtcNow)
|
|
throw new ArgumentException("This card has expired");
|
|
|
|
int? minutesDeducted = null;
|
|
|
|
if (!card.ActiveSessionStartedAt.HasValue)
|
|
{
|
|
// Tap-in: just open the session. Reject outright if there's no balance left
|
|
// at all, so a spent card doesn't get to "start the clock" on a visit it
|
|
// can never pay for.
|
|
if (card.MinutesRemaining < 1)
|
|
throw new ArgumentException("This card has no time remaining");
|
|
|
|
card.ActiveSessionStartedAt = DateTime.UtcNow;
|
|
card.ActiveSessionFacilityId = facilityId;
|
|
}
|
|
else
|
|
{
|
|
// Tap-out: bill for actual elapsed time, rounded up to the next whole minute
|
|
// (a 61-second visit still costs 2 minutes, not 1) -- capped at whatever
|
|
// balance remains rather than letting a forgotten tap-out run the balance
|
|
// negative.
|
|
var elapsed = DateTime.UtcNow - card.ActiveSessionStartedAt.Value;
|
|
var elapsedMinutes = Math.Max(1, (int)Math.Ceiling(elapsed.TotalMinutes));
|
|
minutesDeducted = Math.Min(elapsedMinutes, card.MinutesRemaining);
|
|
|
|
card.MinutesRemaining -= minutesDeducted.Value;
|
|
|
|
await _timeBankCardRepository.AddUsageLog(new TimeBankCardUsageLog
|
|
{
|
|
FKTimeBankCardId = card.TimeBankCardId,
|
|
FKClientId = card.FKClientId,
|
|
FKFacilityId = card.ActiveSessionFacilityId ?? facilityId,
|
|
MinutesDeducted = minutesDeducted.Value,
|
|
MinutesRemainingAfter = card.MinutesRemaining
|
|
});
|
|
|
|
card.ActiveSessionStartedAt = null;
|
|
card.ActiveSessionFacilityId = null;
|
|
}
|
|
|
|
await _uow.CommitAsync();
|
|
return MapToDto(card, minutesDeducted);
|
|
}
|
|
catch
|
|
{
|
|
await _uow.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<object> GetBalance(object payload)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
var rfidTagId = data["rfidTagId"].GetString() ?? throw new ArgumentException("rfidTagId is required");
|
|
|
|
var card = await _timeBankCardRepository.GetByRfidTagId(rfidTagId)
|
|
?? throw new KeyNotFoundException("No active time-bank card found for this RFID tag");
|
|
|
|
return MapToDto(card);
|
|
}
|
|
|
|
public async Task<object> GetMyCards(object payload, HttpContext httpContext)
|
|
{
|
|
var cusId = await _userRoleHelper.GetCusId(DataExtractor.GetUserIdFromContext(httpContext));
|
|
var cards = await _timeBankCardRepository.GetByCustomerId(cusId);
|
|
return cards.Select(c => MapToDto(c)).ToList();
|
|
}
|
|
|
|
private static TimeBankCardDto MapToDto(TimeBankCard card, int? minutesDeductedThisSwipe = null) => new()
|
|
{
|
|
TimeBankCardId = card.TimeBankCardId,
|
|
RfidTagId = card.RfidTagId,
|
|
CustomerId = card.FKCustomerId,
|
|
TotalMinutesPurchased = card.TotalMinutesPurchased,
|
|
MinutesRemaining = card.MinutesRemaining,
|
|
AmountPaid = card.AmountPaid,
|
|
PurchasedAt = card.PurchasedAt,
|
|
ExpiresAt = card.ExpiresAt,
|
|
IsActive = card.IsActive,
|
|
SessionOpen = card.ActiveSessionStartedAt.HasValue,
|
|
SessionStartedAt = card.ActiveSessionStartedAt,
|
|
SessionFacilityName = card.ActiveSessionFacility?.Name,
|
|
MinutesDeductedThisSwipe = minutesDeductedThisSwipe
|
|
};
|
|
}
|
|
}
|