profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
TripsController.cs 8,136 bytes
1 using App.BLL.DTO;
2 using App.BLL.Services;
3 using App.Domain;
4 using App.DTO.Mappers;
5 using App.DTO.v1;
6 using Asp.Versioning;
7 using Microsoft.AspNetCore.Authentication.JwtBearer;
8 using Microsoft.AspNetCore.Authorization;
9 using Microsoft.AspNetCore.Mvc;
10 using System.Net;
11 using System.Security.Claims;
12
13 namespace WebApp.ApiControllers;
14
15 [ApiVersion("1.0")]
16 [Route("api/v{version:apiVersion}/[controller]")]
17 [ApiController]
18 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
19 public class TripsController : ControllerBase
20 {
21 private readonly ITripService _tripService;
22
23 public TripsController(ITripService tripService)
24 {
25 _tripService = tripService;
26 }
27
28 private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
29
30 // GET: api/v1/trips
31 [HttpGet]
32 [Produces("application/json")]
33 [ProducesResponseType<List<TripDto>>((int)HttpStatusCode.OK)]
34 public async Task<ActionResult<List<TripDto>>> GetTrips()
35 {
36 var userId = GetUserId();
37
38 var trips = await _tripService.GetUserTripsAsync(userId);
39
40 return Ok(trips.Select(t => TripMapper.MapToDto(t, includeParticipants: true)).ToList());
41 }
42
43 // POST: api/v1/trips
44 [HttpPost]
45 [Produces("application/json")]
46 [Consumes("application/json")]
47 [ProducesResponseType<TripDto>((int)HttpStatusCode.Created)]
48 public async Task<ActionResult<TripDto>> CreateTrip([FromBody] TripCreateDto dto)
49 {
50 var userId = GetUserId();
51
52 var trip = new TripBllDto
53 {
54 Name = dto.Name,
55 Description = dto.Description,
56 Destination = dto.Destination,
57 StartDate = dto.StartDate,
58 EndDate = dto.EndDate,
59 DefaultCurrencyId = dto.DefaultCurrencyId,
60 };
61
62 var created = await _tripService.CreateTripAsync(trip, userId);
63
64 // Reload with navigation properties
65 var reloaded = await _tripService.GetByIdWithDetailsAsync(created.Id, userId);
66
67 return CreatedAtAction(nameof(GetTrip), new { id = reloaded!.Id }, TripMapper.MapToDto(reloaded));
68 }
69
70 // GET: api/v1/trips/{id}
71 [HttpGet("{id:guid}")]
72 [Produces("application/json")]
73 [ProducesResponseType<TripDto>((int)HttpStatusCode.OK)]
74 [ProducesResponseType((int)HttpStatusCode.NotFound)]
75 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
76 public async Task<ActionResult<TripDto>> GetTrip(Guid id)
77 {
78 var userId = GetUserId();
79
80 var trip = await _tripService.GetByIdWithDetailsAsync(id, userId);
81 if (trip == null)
82 {
83 // distinguish "not participant" vs "doesn't exist"
84 if (!await _tripService.IsParticipantAsync(id, userId)) return Forbid();
85 return NotFound();
86 }
87
88 return Ok(TripMapper.MapToDto(trip, includeParticipants: true));
89 }
90
91 // PUT: api/v1/trips/{id}
92 [HttpPut("{id:guid}")]
93 [Consumes("application/json")]
94 [ProducesResponseType((int)HttpStatusCode.NoContent)]
95 [ProducesResponseType((int)HttpStatusCode.NotFound)]
96 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
97 public async Task<IActionResult> UpdateTrip(Guid id, [FromBody] TripUpdateDto dto)
98 {
99 if (id != dto.Id) return BadRequest();
100
101 var userId = GetUserId();
102
103 var trip = new TripBllDto
104 {
105 Id = id,
106 Name = dto.Name,
107 Description = dto.Description,
108 Destination = dto.Destination,
109 StartDate = dto.StartDate,
110 EndDate = dto.EndDate,
111 DefaultCurrencyId = dto.DefaultCurrencyId,
112 };
113
114 // Preserve existing status if not provided
115 var existing = await _tripService.GetRawByIdAsync(id);
116 if (existing == null)
117 {
118 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
119 return NotFound();
120 }
121 trip.Status = existing.Status;
122 if (!string.IsNullOrEmpty(dto.Status) && Enum.TryParse<ETripStatus>(dto.Status, out var status))
123 {
124 trip.Status = status;
125 }
126
127 var updated = await _tripService.UpdateAsync(trip, userId);
128 if (updated == null)
129 {
130 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
131 return NotFound();
132 }
133
134 return NoContent();
135 }
136
137 // DELETE: api/v1/trips/{id}
138 [HttpDelete("{id:guid}")]
139 [ProducesResponseType((int)HttpStatusCode.NoContent)]
140 [ProducesResponseType((int)HttpStatusCode.NotFound)]
141 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
142 public async Task<IActionResult> DeleteTrip(Guid id)
143 {
144 var userId = GetUserId();
145
146 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
147
148 var success = await _tripService.DeleteAsync(id, userId);
149 if (!success) return NotFound();
150
151 return NoContent();
152 }
153
154 // GET: api/v1/trips/{tripId}/participants
155 [HttpGet("{tripId:guid}/participants")]
156 [Produces("application/json")]
157 [ProducesResponseType<List<TripParticipantDto>>((int)HttpStatusCode.OK)]
158 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
159 public async Task<ActionResult<List<TripParticipantDto>>> GetParticipants(Guid tripId)
160 {
161 var userId = GetUserId();
162
163 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
164
165 var participants = await _tripService.GetParticipantsAsync(tripId, userId);
166
167 return Ok(participants.Select(TripMapper.MapParticipantToDto).ToList());
168 }
169
170 // POST: api/v1/trips/{id}/finalize
171 [HttpPost("{id:guid}/finalize")]
172 [ProducesResponseType((int)HttpStatusCode.OK)]
173 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
174 [ProducesResponseType((int)HttpStatusCode.BadRequest)]
175 public async Task<IActionResult> FinalizeTrip(Guid id)
176 {
177 var userId = GetUserId();
178
179 var (ok, errorCode) = await _tripService.FinalizeTripAsync(id, userId);
180 if (!ok)
181 {
182 return errorCode switch
183 {
184 "forbidden" => Forbid(),
185 "notfound" => NotFound(),
186 "badstatus" => BadRequest("Trip must be active to finalize."),
187 _ => NotFound()
188 };
189 }
190
191 return Ok();
192 }
193
194 // POST: api/v1/trips/{id}/reopen
195 [HttpPost("{id:guid}/reopen")]
196 [ProducesResponseType((int)HttpStatusCode.OK)]
197 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
198 [ProducesResponseType((int)HttpStatusCode.BadRequest)]
199 public async Task<IActionResult> ReopenTrip(Guid id)
200 {
201 var userId = GetUserId();
202
203 var (ok, errorCode) = await _tripService.ReopenTripAsync(id, userId);
204 if (!ok)
205 {
206 return errorCode switch
207 {
208 "forbidden" => Forbid(),
209 "notfound" => NotFound(),
210 "badstatus" => BadRequest("Trip must be settled to reopen."),
211 "payments-confirmed" => BadRequest("Cannot reopen \u2014 some payments are already confirmed."),
212 _ => NotFound()
213 };
214 }
215
216 return Ok();
217 }
218
219 // DELETE: api/v1/trips/{tripId}/participants/{userId}
220 [HttpDelete("{tripId:guid}/participants/{participantUserId:guid}")]
221 [ProducesResponseType((int)HttpStatusCode.NoContent)]
222 [ProducesResponseType((int)HttpStatusCode.NotFound)]
223 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
224 public async Task<IActionResult> RemoveParticipant(Guid tripId, Guid participantUserId)
225 {
226 var userId = GetUserId();
227
228 var (ok, errorCode) = await _tripService.RemoveParticipantAsync(tripId, participantUserId, userId);
229 if (!ok)
230 {
231 return errorCode switch
232 {
233 "forbidden" => Forbid(),
234 "notfound" => NotFound(),
235 "organizer" => BadRequest("Cannot remove the organizer"),
236 "self" => BadRequest("Cannot remove yourself"),
237 _ => NotFound()
238 };
239 }
240
241 return NoContent();
242 }
243 }
244