using Meezi.API.Models.Shifts; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Meezi.API.Services; public record ShiftServiceResult(bool Success, T? Data, string? ErrorCode = null, string? Field = null); public interface IShiftService { Task> OpenShiftAsync( string cafeId, string branchId, decimal openingCash, string userId, CancellationToken cancellationToken = default); Task> CloseShiftAsync( string cafeId, string shiftId, decimal closingCash, string userId, CancellationToken cancellationToken = default); Task GetCurrentShiftAsync( string cafeId, string branchId, CancellationToken cancellationToken = default); Task?> GetTransactionsAsync( string cafeId, string shiftId, CancellationToken cancellationToken = default); Task> RecordTransactionAsync( string cafeId, string shiftId, CashTransactionType type, PaymentMethod method, decimal amount, string createdByUserId, string? referenceId = null, string? note = null, CancellationToken cancellationToken = default); Task> RequireOpenShiftForBranchAsync( string cafeId, string branchId, CancellationToken cancellationToken = default); } public class ShiftService : IShiftService { private readonly AppDbContext _db; public ShiftService(AppDbContext db) => _db = db; public async Task> OpenShiftAsync( string cafeId, string branchId, decimal openingCash, string userId, CancellationToken cancellationToken = default) { var branch = await _db.Branches.FirstOrDefaultAsync( b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, cancellationToken); if (branch is null) return new ShiftServiceResult(false, null, "BRANCH_NOT_FOUND", "branchId"); var hasOpen = await _db.RegisterShifts.AnyAsync( s => s.BranchId == branchId && s.CafeId == cafeId && s.Status == ShiftStatus.Open, cancellationToken); if (hasOpen) return new ShiftServiceResult(false, null, "SHIFT_ALREADY_OPEN", "branchId"); var employeeExists = await _db.Employees.AnyAsync( e => e.Id == userId && e.CafeId == cafeId, cancellationToken); if (!employeeExists) return new ShiftServiceResult(false, null, "USER_NOT_FOUND", "userId"); var now = DateTime.UtcNow; var shift = new Shift { CafeId = cafeId, BranchId = branchId, OpenedByUserId = userId, OpenedAt = now, OpeningCash = openingCash, ExpectedCash = openingCash, Status = ShiftStatus.Open, CreatedAt = now }; _db.RegisterShifts.Add(shift); await _db.SaveChangesAsync(cancellationToken); return new ShiftServiceResult(true, ToDto(shift)); } public async Task> CloseShiftAsync( string cafeId, string shiftId, decimal closingCash, string userId, CancellationToken cancellationToken = default) { var shift = await _db.RegisterShifts .Include(s => s.Transactions) .FirstOrDefaultAsync(s => s.Id == shiftId && s.CafeId == cafeId, cancellationToken); if (shift is null) return new ShiftServiceResult(false, null, "SHIFT_NOT_FOUND"); if (shift.Status != ShiftStatus.Open) return new ShiftServiceResult(false, null, "SHIFT_ALREADY_CLOSED"); shift.ExpectedCash = CalculateExpectedCash(shift.OpeningCash, shift.Transactions); shift.ClosingCash = closingCash; shift.Discrepancy = closingCash - shift.ExpectedCash; shift.ClosedByUserId = userId; shift.ClosedAt = DateTime.UtcNow; shift.Status = ShiftStatus.Closed; await _db.SaveChangesAsync(cancellationToken); return new ShiftServiceResult(true, ToDto(shift)); } public async Task GetCurrentShiftAsync( string cafeId, string branchId, CancellationToken cancellationToken = default) { var shift = await _db.RegisterShifts .AsNoTracking() .FirstOrDefaultAsync( s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open, cancellationToken); return shift is null ? null : ToDto(shift); } public async Task?> GetTransactionsAsync( string cafeId, string shiftId, CancellationToken cancellationToken = default) { var shiftExists = await _db.RegisterShifts.AnyAsync( s => s.Id == shiftId && s.CafeId == cafeId, cancellationToken); if (!shiftExists) return null; var rows = await _db.CashTransactions .AsNoTracking() .Where(t => t.ShiftId == shiftId && t.CafeId == cafeId) .OrderBy(t => t.CreatedAt) .ToListAsync(cancellationToken); return rows.Select(ToTransactionDto).ToList(); } public async Task> RecordTransactionAsync( string cafeId, string shiftId, CashTransactionType type, PaymentMethod method, decimal amount, string createdByUserId, string? referenceId = null, string? note = null, CancellationToken cancellationToken = default) { if (amount <= 0) return new ShiftServiceResult(false, null, "INVALID_AMOUNT", "amount"); var shift = await _db.RegisterShifts.FirstOrDefaultAsync( s => s.Id == shiftId && s.CafeId == cafeId, cancellationToken); if (shift is null) return new ShiftServiceResult(false, null, "SHIFT_NOT_FOUND"); if (shift.Status != ShiftStatus.Open) return new ShiftServiceResult(false, null, "SHIFT_ALREADY_CLOSED"); var tx = new CashTransaction { CafeId = cafeId, BranchId = shift.BranchId, ShiftId = shiftId, Type = type, Method = method, Amount = amount, ReferenceId = referenceId, Note = note, CreatedByUserId = createdByUserId, CreatedAt = DateTime.UtcNow }; _db.CashTransactions.Add(tx); await _db.SaveChangesAsync(cancellationToken); return new ShiftServiceResult(true, ToTransactionDto(tx)); } public async Task> RequireOpenShiftForBranchAsync( string cafeId, string branchId, CancellationToken cancellationToken = default) { var shift = await _db.RegisterShifts.FirstOrDefaultAsync( s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open, cancellationToken); if (shift is null) return new ShiftServiceResult(false, null, "NO_OPEN_SHIFT", "branchId"); return new ShiftServiceResult(true, shift); } internal static decimal CalculateExpectedCash(decimal openingCash, IEnumerable transactions) { var cashPayments = transactions .Where(t => t.Type == CashTransactionType.OrderPayment && t.Method == PaymentMethod.Cash) .Sum(t => t.Amount); var withdrawals = transactions .Where(t => t.Type == CashTransactionType.Withdrawal) .Sum(t => t.Amount); return openingCash + cashPayments - withdrawals; } private static ShiftDto ToDto(Shift s) => new( s.Id, s.CafeId, s.BranchId, s.OpenedByUserId, s.ClosedByUserId, s.OpenedAt, s.ClosedAt, s.OpeningCash, s.ClosingCash, s.ExpectedCash, s.Discrepancy, s.Status); private static CashTransactionDto ToTransactionDto(CashTransaction t) => new( t.Id, t.ShiftId, t.BranchId, t.Type, t.Method, t.Amount, t.ReferenceId, t.Note, t.CreatedByUserId, t.CreatedAt); }