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 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 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 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 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 }; } }