Skip to content
This repository
Browse code

Merge pull request #418 from fsimonazzi/386-show-seat-types

Show the available seats in the ui
  • Loading branch information...
commit 3b0bee130cee37d0759b4443b1eb4bd554554a2c 2 parents ce43498 + 828fb28
jdom jdom authored
2  scripts/CreateDatabaseObjects.sql
@@ -203,6 +203,7 @@ CREATE TABLE [ConferenceRegistration].[ConferencesView](
203 203 [Tagline] [nvarchar](max) NULL,
204 204 [TwitterSearch] [nvarchar](max) NULL,
205 205 [StartDate] [datetimeoffset](7) NOT NULL,
  206 + [SeatsAvailabilityVersion] [int] NOT NULL,
206 207 [IsPublished] [bit] NOT NULL,
207 208 PRIMARY KEY CLUSTERED
208 209 (
@@ -288,6 +289,7 @@ CREATE TABLE [ConferenceRegistration].[ConferenceSeatTypesView](
288 289 [Description] [nvarchar](max) NULL,
289 290 [Price] [decimal](18, 2) NOT NULL,
290 291 [Quantity] [int] NOT NULL,
  292 + [AvailableQuantity] [int] NOT NULL,
291 293 PRIMARY KEY CLUSTERED
292 294 (
293 295 [Id] ASC
11 source/Conference/Conference.Web.Public/Controllers/RegistrationController.cs
@@ -296,7 +296,8 @@ private OrderViewModel CreateViewModel()
296 296 {
297 297 SeatType = s,
298 298 OrderItem = new DraftOrderItem(s.Id, 0),
299   - MaxSeatSelection = 20
  299 + AvailableQuantityForOrder = Math.Max(s.AvailableQuantity, 0),
  300 + MaxSelectionQuantity = Math.Max(Math.Min(s.AvailableQuantity, 20), 0)
300 301 }).ToList(),
301 302 };
302 303
@@ -314,11 +315,9 @@ private OrderViewModel CreateViewModel(DraftOrder order)
314 315 {
315 316 var seat = viewModel.Items.First(s => s.SeatType.Id == line.SeatType);
316 317 seat.OrderItem = line;
317   - if (line.RequestedSeats > line.ReservedSeats)
318   - {
319   - seat.PartiallyFulfilled = true;
320   - seat.MaxSeatSelection = line.ReservedSeats;
321   - }
  318 + seat.AvailableQuantityForOrder = seat.AvailableQuantityForOrder + line.ReservedSeats;
  319 + seat.MaxSelectionQuantity = Math.Min(seat.AvailableQuantityForOrder, 20);
  320 + seat.PartiallyFulfilled = line.RequestedSeats > line.ReservedSeats;
322 321 }
323 322
324 323 return viewModel;
4 source/Conference/Conference.Web.Public/Models/OrderItemViewModel.cs
@@ -23,6 +23,8 @@ public class OrderItemViewModel
23 23
24 24 public bool PartiallyFulfilled { get; set; }
25 25
26   - public int MaxSeatSelection { get; set; }
  26 + public int AvailableQuantityForOrder { get; set; }
  27 +
  28 + public int MaxSelectionQuantity { get; set; }
27 29 }
28 30 }
20 source/Conference/Conference.Web.Public/Views/Registration/StartRegistration.cshtml
@@ -26,7 +26,7 @@
26 26 <tr>
27 27 <th style="width: 75%">Registration type</th>
28 28 <th scope="col" style="text-align: right">Price</th>
29   - @*<th scope="col">Available</th>*@
  29 + <th scope="col">Available</th>
30 30 <th scope="col">Quantity</th>
31 31 </tr>
32 32 @for (var i = 0; i < this.Model.Items.Count; i++)
@@ -38,8 +38,18 @@
38 38 </td>
39 39 <td style="text-align: right">$<span class="unitPrice">@Html.DisplayFor(model => model.Items[i].SeatType.Price)</span>
40 40 </td>
41   - <td>@Html.DropDownList("Seats[" + i + "].Quantity", new SelectList(Enumerable.Range(0, this.Model.Items[i].MaxSeatSelection + 1), this.Model.Items[i].OrderItem.ReservedSeats), new { @class = "itemQuantity" })
42   - @Html.Hidden("Seats[" + i + "].SeatType", this.Model.Items[i].SeatType.Id)
  41 + <td style="text-align: right">@Html.DisplayFor(model => model.Items[i].AvailableQuantityForOrder)
  42 + </td>
  43 + <td>
  44 + @if ((this.Model.Items[i].MaxSelectionQuantity) > 0)
  45 + {
  46 + @Html.DropDownList("Seats[" + i + "].Quantity", new SelectList(Enumerable.Range(0, this.Model.Items[i].MaxSelectionQuantity + 1), this.Model.Items[i].OrderItem.ReservedSeats), new { @class = "itemQuantity" })
  47 + @Html.Hidden("Seats[" + i + "].SeatType", this.Model.Items[i].SeatType.Id)
  48 + }
  49 + else
  50 + {
  51 + <text>Sold out</text>
  52 + }
43 53 </td>
44 54 </tr>
45 55 if (this.Model.Items[i].PartiallyFulfilled)
@@ -59,7 +69,7 @@
59 69 <input class="form-promo__txt"><input class="form-promo__but" value="Submit" type="Submit">
60 70 </div>
61 71 </td>*@
62   - <td colspan="2" class="content__table-cell_right content__cell_total">Estimated Total:</td>
  72 + <td colspan="3" class="content__table-cell_right content__cell_total">Estimated Total:</td>
63 73 <td class="content__cell_total content__table-cell_left">$<span id="total">0</span></td>
64 74 </tr>
65 75 </table>
@@ -123,7 +133,7 @@
123 133 $(".lineItem").each(function (index, item) {
124 134 var unitPrice = $(item).find(".unitPrice").text();
125 135 var quantity = $(item).find(".itemQuantity input").val();
126   - total += unitPrice * quantity;
  136 + total += unitPrice * (quantity ? quantity : 0);
127 137 });
128 138
129 139 // round to 2 decimals
237 source/Conference/Registration.IntegrationTests/ConferenceViewModelGeneratorFixture.cs
@@ -20,6 +20,7 @@ namespace Registration.Tests.ConferenceViewModelGeneratorFixture
20 20 using Infrastructure.Messaging;
21 21 using Moq;
22 22 using Registration.Commands;
  23 + using Registration.Events;
23 24 using Registration.Handlers;
24 25 using Registration.IntegrationTests;
25 26 using Registration.ReadModel;
@@ -74,6 +75,7 @@ public void when_conference_created_then_conference_dto_populated()
74 75 Assert.Equal("description", dto.Description);
75 76 Assert.Equal("test", dto.Code);
76 77 Assert.Equal(0, dto.Seats.Count);
  78 + Assert.Equal(-1, dto.SeatsAvailabilityVersion);
77 79 }
78 80 }
79 81 }
@@ -197,6 +199,7 @@ public void when_seat_created_then_adds_seat_to_conference_dto()
197 199 Assert.Equal("seat", dto.Name);
198 200 Assert.Equal("description", dto.Description);
199 201 Assert.Equal(200, dto.Price);
  202 + Assert.Equal(0, dto.AvailableQuantity);
200 203 }
201 204
202 205 }
@@ -330,5 +333,239 @@ public void when_seats_removed_then_add_seats_command_sent()
330 333 Assert.Equal(seatId, e.SeatType);
331 334 Assert.Equal(50, e.Quantity);
332 335 }
  336 +
  337 + [Fact]
  338 + public void when_available_seats_change_then_updates_remaining_quantity()
  339 + {
  340 + var seatId = Guid.NewGuid();
  341 +
  342 + this.sut.Handle(new SeatCreated
  343 + {
  344 + ConferenceId = conferenceId,
  345 + SourceId = seatId,
  346 + Name = "seat",
  347 + Description = "description",
  348 + Price = 200,
  349 + });
  350 +
  351 + this.sut.Handle(new AvailableSeatsChanged
  352 + {
  353 + SourceId = conferenceId,
  354 + Version = 0,
  355 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  356 + });
  357 +
  358 + using (var context = new ConferenceRegistrationDbContext(dbName))
  359 + {
  360 + var dtos = context.Set<Conference>()
  361 + .Where(x => x.Id == conferenceId)
  362 + .SelectMany(x => x.Seats, (Conference, Seat) => new { Conference, Seat })
  363 + .FirstOrDefault(x => x.Seat.Id == seatId);
  364 +
  365 + Assert.NotNull(dtos);
  366 + Assert.Equal("seat", dtos.Seat.Name);
  367 + Assert.Equal("description", dtos.Seat.Description);
  368 + Assert.Equal(200, dtos.Seat.Price);
  369 + Assert.Equal(200, dtos.Seat.AvailableQuantity);
  370 + Assert.Equal(0, dtos.Conference.SeatsAvailabilityVersion);
  371 + }
  372 + }
  373 +
  374 + [Fact]
  375 + public void when_seats_are_reserved_then_updates_remaining_quantity()
  376 + {
  377 + var seatId = Guid.NewGuid();
  378 +
  379 + this.sut.Handle(new SeatCreated
  380 + {
  381 + ConferenceId = conferenceId,
  382 + SourceId = seatId,
  383 + Name = "seat",
  384 + Description = "description",
  385 + Price = 200,
  386 + });
  387 +
  388 + this.sut.Handle(new AvailableSeatsChanged
  389 + {
  390 + SourceId = conferenceId,
  391 + Version = 0,
  392 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  393 + });
  394 +
  395 + this.sut.Handle(new SeatsReserved
  396 + {
  397 + SourceId = conferenceId,
  398 + Version = 1,
  399 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = -50 } }
  400 + });
  401 +
  402 + using (var context = new ConferenceRegistrationDbContext(dbName))
  403 + {
  404 + var dtos = context.Set<Conference>()
  405 + .Where(x => x.Id == conferenceId)
  406 + .SelectMany(x => x.Seats, (Conference, Seat) => new { Conference, Seat })
  407 + .FirstOrDefault(x => x.Seat.Id == seatId);
  408 +
  409 + Assert.NotNull(dtos);
  410 + Assert.Equal("seat", dtos.Seat.Name);
  411 + Assert.Equal("description", dtos.Seat.Description);
  412 + Assert.Equal(200, dtos.Seat.Price);
  413 + Assert.Equal(150, dtos.Seat.AvailableQuantity);
  414 + Assert.Equal(1, dtos.Conference.SeatsAvailabilityVersion);
  415 + }
  416 + }
  417 +
  418 + [Fact]
  419 + public void when_seats_are_released_then_updates_remaining_quantity()
  420 + {
  421 + var seatId = Guid.NewGuid();
  422 +
  423 + this.sut.Handle(new SeatCreated
  424 + {
  425 + ConferenceId = conferenceId,
  426 + SourceId = seatId,
  427 + Name = "seat",
  428 + Description = "description",
  429 + Price = 200,
  430 + });
  431 +
  432 + this.sut.Handle(new AvailableSeatsChanged
  433 + {
  434 + SourceId = conferenceId,
  435 + Version = 0,
  436 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  437 + });
  438 +
  439 + this.sut.Handle(new SeatsReserved
  440 + {
  441 + SourceId = conferenceId,
  442 + Version = 1,
  443 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = -50 } }
  444 + });
  445 +
  446 + this.sut.Handle(new SeatsReservationCancelled
  447 + {
  448 + SourceId = conferenceId,
  449 + Version = 2,
  450 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = 50 } }
  451 + });
  452 +
  453 + using (var context = new ConferenceRegistrationDbContext(dbName))
  454 + {
  455 + var dtos = context.Set<Conference>()
  456 + .Where(x => x.Id == conferenceId)
  457 + .SelectMany(x => x.Seats, (Conference, Seat) => new { Conference, Seat })
  458 + .FirstOrDefault(x => x.Seat.Id == seatId);
  459 +
  460 + Assert.NotNull(dtos);
  461 + Assert.Equal("seat", dtos.Seat.Name);
  462 + Assert.Equal("description", dtos.Seat.Description);
  463 + Assert.Equal(200, dtos.Seat.Price);
  464 + Assert.Equal(200, dtos.Seat.AvailableQuantity);
  465 + Assert.Equal(2, dtos.Conference.SeatsAvailabilityVersion);
  466 + }
  467 + }
  468 +
  469 + [Fact]
  470 + public void when_seat_availability_update_event_has_version_equal_to_last_update_then_event_is_ignored()
  471 + {
  472 + var seatId = Guid.NewGuid();
  473 +
  474 + this.sut.Handle(new SeatCreated
  475 + {
  476 + ConferenceId = conferenceId,
  477 + SourceId = seatId,
  478 + Name = "seat",
  479 + Description = "description",
  480 + Price = 200,
  481 + });
  482 +
  483 + this.sut.Handle(new AvailableSeatsChanged
  484 + {
  485 + SourceId = conferenceId,
  486 + Version = 0,
  487 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  488 + });
  489 +
  490 + this.sut.Handle(new SeatsReserved
  491 + {
  492 + SourceId = conferenceId,
  493 + Version = 1,
  494 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = -50 } }
  495 + });
  496 +
  497 + this.sut.Handle(new SeatsReserved
  498 + {
  499 + SourceId = conferenceId,
  500 + Version = 1,
  501 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = -50 } }
  502 + });
  503 +
  504 + using (var context = new ConferenceRegistrationDbContext(dbName))
  505 + {
  506 + var dtos = context.Set<Conference>()
  507 + .Where(x => x.Id == conferenceId)
  508 + .SelectMany(x => x.Seats, (Conference, Seat) => new { Conference, Seat })
  509 + .FirstOrDefault(x => x.Seat.Id == seatId);
  510 +
  511 + Assert.NotNull(dtos);
  512 + Assert.Equal("seat", dtos.Seat.Name);
  513 + Assert.Equal("description", dtos.Seat.Description);
  514 + Assert.Equal(200, dtos.Seat.Price);
  515 + Assert.Equal(150, dtos.Seat.AvailableQuantity);
  516 + Assert.Equal(1, dtos.Conference.SeatsAvailabilityVersion);
  517 + }
  518 + }
  519 +
  520 + [Fact]
  521 + public void when_seat_availability_update_event_has_version_lower_than_last_update_then_event_is_ignored()
  522 + {
  523 + var seatId = Guid.NewGuid();
  524 +
  525 + this.sut.Handle(new SeatCreated
  526 + {
  527 + ConferenceId = conferenceId,
  528 + SourceId = seatId,
  529 + Name = "seat",
  530 + Description = "description",
  531 + Price = 200,
  532 + });
  533 +
  534 + this.sut.Handle(new AvailableSeatsChanged
  535 + {
  536 + SourceId = conferenceId,
  537 + Version = 0,
  538 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  539 + });
  540 +
  541 + this.sut.Handle(new SeatsReserved
  542 + {
  543 + SourceId = conferenceId,
  544 + Version = 1,
  545 + AvailableSeatsChanged = new[] { new SeatQuantity { SeatType = seatId, Quantity = -50 } }
  546 + });
  547 +
  548 + this.sut.Handle(new AvailableSeatsChanged
  549 + {
  550 + SourceId = conferenceId,
  551 + Version = 0,
  552 + Seats = new[] { new SeatQuantity { SeatType = seatId, Quantity = 200 } }
  553 + });
  554 +
  555 + using (var context = new ConferenceRegistrationDbContext(dbName))
  556 + {
  557 + var dtos = context.Set<Conference>()
  558 + .Where(x => x.Id == conferenceId)
  559 + .SelectMany(x => x.Seats, (Conference, Seat) => new { Conference, Seat })
  560 + .FirstOrDefault(x => x.Seat.Id == seatId);
  561 +
  562 + Assert.NotNull(dtos);
  563 + Assert.Equal("seat", dtos.Seat.Name);
  564 + Assert.Equal("description", dtos.Seat.Description);
  565 + Assert.Equal(200, dtos.Seat.Price);
  566 + Assert.Equal(150, dtos.Seat.AvailableQuantity);
  567 + Assert.Equal(1, dtos.Conference.SeatsAvailabilityVersion);
  568 + }
  569 + }
333 570 }
334 571 }
69 source/Conference/Registration/Handlers/ConferenceViewModelGenerator.cs
@@ -14,13 +14,16 @@
14 14 namespace Registration.Handlers
15 15 {
16 16 using System;
  17 + using System.Collections.Generic;
17 18 using System.Data.Entity;
18 19 using System.Diagnostics;
19 20 using System.Linq;
20 21 using Conference;
  22 + using Infrastructure.EventSourcing;
21 23 using Infrastructure.Messaging;
22 24 using Infrastructure.Messaging.Handling;
23 25 using Registration.Commands;
  26 + using Registration.Events;
24 27 using Registration.ReadModel;
25 28 using Registration.ReadModel.Implementation;
26 29
@@ -31,7 +34,10 @@ public class ConferenceViewModelGenerator :
31 34 IEventHandler<ConferencePublished>,
32 35 IEventHandler<ConferenceUnpublished>,
33 36 IEventHandler<SeatCreated>,
34   - IEventHandler<SeatUpdated>
  37 + IEventHandler<SeatUpdated>,
  38 + IEventHandler<AvailableSeatsChanged>,
  39 + IEventHandler<SeatsReserved>,
  40 + IEventHandler<SeatsReservationCancelled>
35 41 {
36 42 private readonly Func<ConferenceRegistrationDbContext> contextFactory;
37 43 private ICommandBus bus;
@@ -141,7 +147,7 @@ public void Handle(SeatCreated @event)
141 147 }
142 148 else
143 149 {
144   - Trace.TraceError("Failed to locate Conference read model for created seat, with conference id {0}.", @event.SourceId);
  150 + Trace.TraceError("Failed to locate Conference read model for created seat, with conference id {0}.", @event.ConferenceId);
145 151 }
146 152 }
147 153 }
@@ -194,6 +200,65 @@ public void Handle(SeatUpdated @event)
194 200 }
195 201 else
196 202 {
  203 + Trace.TraceError("Failed to locate Conference read model for updated seat type, with conference id {0}.", @event.ConferenceId);
  204 + }
  205 + }
  206 + }
  207 +
  208 + public void Handle(AvailableSeatsChanged @event)
  209 + {
  210 + this.UpdateAvailableQuantity(@event, @event.Seats);
  211 + }
  212 +
  213 + public void Handle(SeatsReserved @event)
  214 + {
  215 + this.UpdateAvailableQuantity(@event, @event.AvailableSeatsChanged);
  216 + }
  217 +
  218 + public void Handle(SeatsReservationCancelled @event)
  219 + {
  220 + this.UpdateAvailableQuantity(@event, @event.AvailableSeatsChanged);
  221 + }
  222 +
  223 + private void UpdateAvailableQuantity(IVersionedEvent @event, IEnumerable<SeatQuantity> seats)
  224 + {
  225 + using (var repository = this.contextFactory.Invoke())
  226 + {
  227 + var dto = repository.Set<Conference>().Include(x => x.Seats).FirstOrDefault(x => x.Id == @event.SourceId);
  228 + if (dto != null)
  229 + {
  230 + // This check assumes events might be received more than once, but not out of order
  231 + if (@event.Version > dto.SeatsAvailabilityVersion)
  232 + {
  233 + foreach (var seat in seats)
  234 + {
  235 + var seatDto = dto.Seats.FirstOrDefault(x => x.Id == seat.SeatType);
  236 + if (seatDto != null)
  237 + {
  238 + seatDto.AvailableQuantity += seat.Quantity;
  239 + }
  240 + else
  241 + {
  242 + // TODO should reject the entire update?
  243 + Trace.TraceError("Failed to locate Seat Type read model being updated with id {0}.", seat.SeatType);
  244 + }
  245 + }
  246 +
  247 + dto.SeatsAvailabilityVersion = @event.Version;
  248 +
  249 + repository.Save(dto);
  250 + }
  251 + else
  252 + {
  253 + Trace.TraceWarning(
  254 + "Ignoring availability update message with version {1} for conference id {0}, last known version {2}.",
  255 + @event.SourceId,
  256 + @event.Version,
  257 + dto.SeatsAvailabilityVersion);
  258 + }
  259 + }
  260 + else
  261 + {
197 262 Trace.TraceError("Failed to locate Conference read model for updated seat type, with conference id {0}.", @event.SourceId);
198 263 }
199 264 }
2  source/Conference/Registration/ReadModel/Conference.cs
@@ -31,6 +31,7 @@ public Conference(Guid id, string code, string name, string description, string
31 31 this.TwitterSearch = twitterSearch;
32 32 this.StartDate = startDate;
33 33 this.Seats = new ObservableCollection<SeatType>(seats);
  34 + this.SeatsAvailabilityVersion = -1;
34 35 }
35 36
36 37 protected Conference()
@@ -48,6 +49,7 @@ protected Conference()
48 49 public string TwitterSearch { get; set; }
49 50 public DateTimeOffset StartDate { get; set; }
50 51 public ICollection<SeatType> Seats { get; set; }
  52 + public int SeatsAvailabilityVersion { get; set; }
51 53
52 54 public bool IsPublished { get; set; }
53 55 }
2  source/Conference/Registration/ReadModel/SeatType.cs
@@ -26,6 +26,7 @@ public SeatType(Guid id, Guid conferenceId, string name, string description, dec
26 26 this.Description = description;
27 27 this.Price = price;
28 28 this.Quantity = quantity;
  29 + this.AvailableQuantity = 0;
29 30 }
30 31
31 32 protected SeatType()
@@ -40,5 +41,6 @@ protected SeatType()
40 41 public string Description { get; set; }
41 42 public decimal Price { get; set; }
42 43 public int Quantity { get; set; }
  44 + public int AvailableQuantity { get; set; }
43 45 }
44 46 }

0 comments on commit 3b0bee1

Please sign in to comment.
Something went wrong with that request. Please try again.