profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
ExpensesController.cs 7,130 bytes
1 using App.BLL.Helpers;
2 using App.BLL.DTO;
3 using App.BLL.Services;
4 using App.Domain;
5 using App.DTO.Mappers;
6 using App.DTO.v1;
7 using Asp.Versioning;
8 using Microsoft.AspNetCore.Authentication.JwtBearer;
9 using Microsoft.AspNetCore.Authorization;
10 using Microsoft.AspNetCore.Mvc;
11 using System.Net;
12 using System.Security.Claims;
13
14 namespace WebApp.ApiControllers;
15
16 [ApiVersion("1.0")]
17 [Route("api/v{version:apiVersion}/[controller]")]
18 [ApiController]
19 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
20 public class ExpensesController : ControllerBase
21 {
22 private readonly IExpenseService _expenseService;
23 private readonly ITripService _tripService;
24
25 public ExpensesController(IExpenseService expenseService, ITripService tripService)
26 {
27 _expenseService = expenseService;
28 _tripService = tripService;
29 }
30
31 private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
32
33 // GET: api/v1/expenses/trip/{tripId}
34 [HttpGet("trip/{tripId:guid}")]
35 [Produces("application/json")]
36 [ProducesResponseType<List<ExpenseDto>>((int)HttpStatusCode.OK)]
37 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
38 public async Task<ActionResult<List<ExpenseDto>>> GetTripExpenses(Guid tripId)
39 {
40 var userId = GetUserId();
41
42 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
43
44 var trip = await _tripService.GetRawByIdAsync(tripId);
45 var tripDefaultCode = trip?.DefaultCurrency?.Code;
46
47 var expenses = await _expenseService.GetByTripIdAsync(tripId, userId);
48
49 return Ok(expenses.Select(e => WithTripConversion(ExpenseMapper.MapToDto(e), e.Currency?.Code, tripDefaultCode)).ToList());
50 }
51
52 private static ExpenseDto WithTripConversion(ExpenseDto dto, string? fromCode, string? tripDefaultCode)
53 {
54 if (fromCode != null && tripDefaultCode != null)
55 dto.AmountInTripCurrency = CurrencyConverter.Convert(dto.Amount, fromCode, tripDefaultCode);
56 return dto;
57 }
58
59 // POST: api/v1/expenses
60 [HttpPost]
61 [Produces("application/json")]
62 [Consumes("application/json")]
63 [ProducesResponseType<ExpenseDto>((int)HttpStatusCode.Created)]
64 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
65 public async Task<ActionResult<ExpenseDto>> CreateExpense([FromBody] ExpenseCreateDto dto)
66 {
67 var userId = GetUserId();
68
69 if (!await _tripService.IsParticipantAsync(dto.TripId, userId)) return Forbid();
70
71 var payerId = dto.PaidByUserId ?? userId;
72 if (payerId != userId && !await _tripService.IsParticipantAsync(dto.TripId, payerId))
73 return BadRequest("Selected payer is not a participant of this trip.");
74
75 var trip = await _tripService.GetRawByIdAsync(dto.TripId);
76 if (trip == null) return NotFound();
77 if (trip.Status != ETripStatus.Active)
78 return BadRequest("Cannot add expenses to a settled trip.");
79
80 var expense = new ExpenseBllDto
81 {
82 TripId = dto.TripId,
83 PaidByUserId = payerId,
84 BudgetCategoryId = dto.BudgetCategoryId,
85 CurrencyId = dto.CurrencyId,
86 Amount = dto.Amount,
87 Description = dto.Description,
88 ExpenseDate = dto.ExpenseDate,
89 SplitMethod = Enum.Parse<ESplitMethod>(dto.SplitMethod)
90 };
91
92 var participants = dto.Splits?.Select(s => s.UserId).ToArray() ?? Array.Empty<Guid>();
93 var amounts = dto.Splits?.Select(s => s.Amount).ToArray() ?? Array.Empty<decimal>();
94 var percentages = dto.Splits?.Select(s => s.Percentage ?? 0m).ToArray() ?? Array.Empty<decimal>();
95
96 var created = await _expenseService.CreateExpenseWithSplitsAsync(expense, participants, amounts, percentages);
97
98 var reloaded = await _expenseService.GetByIdWithDetailsAsync(created.Id, userId);
99
100 var tripDefaultCode = trip.DefaultCurrency?.Code;
101 return CreatedAtAction(nameof(GetExpense), new { id = reloaded!.Id },
102 WithTripConversion(ExpenseMapper.MapToDto(reloaded), reloaded.Currency?.Code, tripDefaultCode));
103 }
104
105 // GET: api/v1/expenses/{id}
106 [HttpGet("{id:guid}")]
107 [Produces("application/json")]
108 [ProducesResponseType<ExpenseDto>((int)HttpStatusCode.OK)]
109 [ProducesResponseType((int)HttpStatusCode.NotFound)]
110 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
111 public async Task<ActionResult<ExpenseDto>> GetExpense(Guid id)
112 {
113 var userId = GetUserId();
114
115 var raw = await _expenseService.GetRawByIdAsync(id);
116 if (raw == null) return NotFound();
117
118 var expense = await _expenseService.GetByIdWithDetailsAsync(id, userId);
119 if (expense == null) return Forbid();
120
121 var trip = await _tripService.GetRawByIdAsync(expense.TripId);
122 var tripDefaultCode = trip?.DefaultCurrency?.Code;
123
124 return Ok(WithTripConversion(ExpenseMapper.MapToDto(expense), expense.Currency?.Code, tripDefaultCode));
125 }
126
127 // PUT: api/v1/expenses/{id}
128 [HttpPut("{id:guid}")]
129 [Consumes("application/json")]
130 [ProducesResponseType((int)HttpStatusCode.NoContent)]
131 [ProducesResponseType((int)HttpStatusCode.NotFound)]
132 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
133 public async Task<IActionResult> UpdateExpense(Guid id, [FromBody] ExpenseCreateDto dto)
134 {
135 var userId = GetUserId();
136
137 var splits = dto.Splits?.Select(s => (s.UserId, s.Amount, s.Percentage)).ToList()
138 ?? new List<(Guid UserId, decimal Amount, decimal? Percentage)>();
139
140 var (ok, errorCode) = await _expenseService.UpdateExpenseWithSplitsFromDtoAsync(
141 id,
142 dto.Amount,
143 dto.Description,
144 dto.ExpenseDate,
145 Enum.Parse<ESplitMethod>(dto.SplitMethod),
146 dto.BudgetCategoryId,
147 dto.CurrencyId,
148 splits,
149 userId);
150
151 if (!ok)
152 {
153 return errorCode switch
154 {
155 "notfound" => NotFound(),
156 "forbidden" => Forbid(),
157 "badstatus" => BadRequest("Cannot edit expenses on a settled trip."),
158 _ => NotFound()
159 };
160 }
161
162 return NoContent();
163 }
164
165 // DELETE: api/v1/expenses/{id}
166 [HttpDelete("{id:guid}")]
167 [ProducesResponseType((int)HttpStatusCode.NoContent)]
168 [ProducesResponseType((int)HttpStatusCode.NotFound)]
169 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
170 public async Task<IActionResult> DeleteExpense(Guid id)
171 {
172 var userId = GetUserId();
173
174 var expense = await _expenseService.GetRawByIdAsync(id);
175 if (expense == null) return NotFound();
176
177 if (!await _expenseService.CanEditExpenseAsync(expense, userId)) return Forbid();
178
179 var trip = await _tripService.GetRawByIdAsync(expense.TripId);
180 if (trip != null && trip.Status != ETripStatus.Active)
181 return BadRequest("Cannot delete expenses on a settled trip.");
182
183 await _expenseService.DeleteExpenseWithSplitsAsync(id);
184
185 return NoContent();
186 }
187 }
188