using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Expenses; using Meezi.API.Services; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/expenses")] public class ExpensesController : CafeApiControllerBase { private readonly IExpenseService _expenses; private readonly IValidator _createValidator; public ExpensesController( IExpenseService expenses, IValidator createValidator) { _expenses = expenses; _createValidator = createValidator; } [HttpPost] public async Task Create( string cafeId, [FromBody] CreateExpenseRequest request, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (string.IsNullOrEmpty(tenant.UserId)) return StatusCode(StatusCodes.Status401Unauthorized, new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); if (!CanLogExpense(tenant.Role)) return StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError("FORBIDDEN", "You cannot log expenses."))); var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var result = await _expenses.CreateExpenseAsync(cafeId, request, tenant.UserId, ct); return ExpenseResult(result, StatusCodes.Status201Created); } [HttpGet] public async Task List( string cafeId, [FromQuery] string branchId, [FromQuery] string from, [FromQuery] string to, ITenantContext tenant, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (string.IsNullOrWhiteSpace(branchId)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "branchId is required.", "branchId"))); if (!DateOnly.TryParse(from, out var fromDate) || !DateOnly.TryParse(to, out var toDate)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Invalid from/to. Use yyyy-MM-dd.", "from"))); if (fromDate > toDate) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "from must be on or before to.", "from"))); var data = await _expenses.GetExpensesAsync(cafeId, branchId, fromDate, toDate, page, pageSize, ct); return Ok(new PagedApiResponse( true, data.Items, new PagedMeta(data.Total, page, pageSize))); } [HttpDelete("{id}")] public async Task Delete( string cafeId, string id, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (!CanDeleteExpense(tenant.Role)) return StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError("FORBIDDEN", "Only managers can delete expenses."))); var result = await _expenses.DeleteExpenseAsync(cafeId, id, ct); if (!result.Success) { return result.ErrorCode switch { "NOT_FOUND" => NotFoundError("Expense not found."), _ => BadRequest(new ApiResponse(false, null, new ApiError(result.ErrorCode ?? "ERROR", "Delete failed."))) }; } return Ok(new ApiResponse(true, null)); } private static bool CanLogExpense(EmployeeRole? role) => role is EmployeeRole.Owner or EmployeeRole.Manager or EmployeeRole.Cashier; private static bool CanDeleteExpense(EmployeeRole? role) => role is EmployeeRole.Owner or EmployeeRole.Manager; private IActionResult ExpenseResult(ExpenseServiceResult result, int successStatus = StatusCodes.Status200OK) { if (result.Success) return StatusCode(successStatus, new ApiResponse(true, result.Data)); return result.ErrorCode switch { "BRANCH_NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(result.ErrorCode, "Branch not found.", result.Field))), "SHIFT_NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(result.ErrorCode, "Shift not found.", result.Field))), "SHIFT_BRANCH_MISMATCH" => BadRequest(new ApiResponse(false, null, new ApiError(result.ErrorCode, "Shift does not belong to this branch.", result.Field))), "SHIFT_ALREADY_CLOSED" => BadRequest(new ApiResponse(false, null, new ApiError(result.ErrorCode, "Shift is already closed.", result.Field))), _ => BadRequest(new ApiResponse(false, null, new ApiError(result.ErrorCode ?? "ERROR", "Could not create expense.", result.Field))) }; } }