From 7bb0154774e344a7dc75c368b6a4fbd4a21f58a3 Mon Sep 17 00:00:00 2001 From: Amin <69254513+AminGh05@users.noreply.github.com> Date: Wed, 9 Jul 2025 01:03:11 +0330 Subject: [PATCH] Update Session09 Backend.md updated to the CreateTicketOrderAsync method --- .../Session09/Session09 Backend.md | 277 +++++++++++++++--- 1 file changed, 244 insertions(+), 33 deletions(-) diff --git a/ProjectOrientedSessions/Session09/Session09 Backend.md b/ProjectOrientedSessions/Session09/Session09 Backend.md index 37c565a..fe866de 100644 --- a/ProjectOrientedSessions/Session09/Session09 Backend.md +++ b/ProjectOrientedSessions/Session09/Session09 Backend.md @@ -1,30 +1,17 @@ -Here's a **structured checklist document**, broken into **sections by feature area**. Each section contains tasks in implementation order (oldest to newest), rephrased as clear action items with contextual notes for implementation. -These are created based on this commit messages: -https://github.com/MehrdadShirvani/AlibabaClone-Backend/commits/develop/ +# Session 9 - Ticket Reservation +As this order is comming to an end, we are completing the functionality of application with the vital feature of reserving tickets ---- -# Miscellaneous / Fixes +## Miscellaneous / Fixes - [ ] Fix GUID generation and async handling in ticket creation. (Use `Guid.NewGuid()` instead of `new Guid()`) -- [ ] Make `SerialNumber`, `TicketOrderId`, `BaseAmount` publicly settable in DTOs. -- [ ] Fix issues in seat mappings and transportation queries. -# Fixes and Missing Parts from Session 08 +```csharp +SerialNumber = Guid.NewGuid().ToString("N") +``` -## 👤 Person & Account Info +- [ ] Make `SerialNumber`, `TicketOrderId`, `BaseAmount` publicly settable in DTOs if they're private. +- [ ] Check `Transaction` and make sure `SerialNumber` is not a required property. -- [ ] Add `Person`, `BankAccountDetail`, and navigation properties. -- [ ] Add `UpsertPersonAsync` and `UpsertBankAccountDetailAsync` in `IAccountService`. -- [ ] Add `UpsertPerson`, `UpsertBankDetail` endpoints in `AccountController`. -- [ ] Add `GetPeople`, `GetProfileAsync`, and DTOs (`ProfileDto`, `PersonDto`, `BankAccountDetailDto`). -- [ ] Handle person editing logic via `UpsertPerson` and adjust classes that used it the old way. -## 🔐 Auth & Settings - -- [ ] Complete the error handling of `EditEmailDto`, `EditPasswordDto`. -- [ ] Add `GetByEmailAsync` in `AccountRepository`. - - - -# Branching +## Branching - [ ] Create the feature/ticket-reservation branch based on develop # Ticket Ordering System @@ -32,17 +19,245 @@ https://github.com/MehrdadShirvani/AlibabaClone-Backend/commits/develop/ ## 🧱 Domain and Infrastructure Setup - [ ] Check out new ERD: [Here](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/docs/AlibabaERD-Version02.pdf) -- [ ] Create `TicketOrder`, `Ticket`, and `Transaction` entities. -- [ ] Add configurations for `TicketOrder`, `Ticket`, and `Transaction` (relationships, constraints). -- [ ] Adjust `Transportation` entity configuration to support ticketing. - [ ] Add migrations for the above database changes. ## 🧑‍💼 Service Layer -- [ ] Create `ITicketOrderService`, `TicketOrderService` and implement: - - [ ] `CreateTicketOrderAsync` - - [ ] `GenerateTicketsPdfAsync` -- [ ] Register `TicketOrderService` in DI container. +- [ ] Create DTOs `CreateTravellerTicketDto` and `CreateTicketOrderDto`: +```csharp +public class CreateTravellerTicketDto +{ + public long Id { get; set; } + public long CreatorId { get; set; } + + [Required(ErrorMessage = "First name is required")] + public required string FirstName { get; set; } + + [Required(ErrorMessage = "Last name is required")] + public required string LastName { get; set; } + + [Required(ErrorMessage = "Id number is required")] + [RegularExpression(@"^\d{10}$", ErrorMessage = "National ID number must be exactly 10 digits")] + public required string IdNumber { get; set; } + + [Required(ErrorMessage = "Gender should be identified")] + public required short GenderId { get; set; } + + [Required(ErrorMessage = "Phone number is required")] + [Phone(ErrorMessage = "Invalid phone number format")] + public required string PhoneNumber { get; set; } + + [Required(ErrorMessage = "Birth date is required")] + public DateTime BirthDate { get; set; } + + public long? SeatId { get; set; } + public bool IsVIP { get; set; } + public string? Description { get; set; } +} +``` +```csharp +public class CreateTicketOrderDto +{ + public long TransportationId { get; set; } + public List MyProperty { get; set; } +} +``` +Note: If you have the **Coupon** feature in your project, then add a `CouponCode` property in `CreateTicketOrderDto`. + +- [ ] Modify SeatRepository to add method `GetSeatsByVehicleIdAsync` +```csharp +public Task> GetSeatsByVehicleIdAsync(long vehicleId) +{ + var seats = DbSet + .Include(s => s.Vehicle) + .Include(s => s.Tickets).ThenInclude(t => t.Traveler) + .Where(s => s.VehicleId == vehicleId).ToListAsync(); + return seats; +} +``` +- [ ] Add Enum for **TicketStatus**, **VehicleType** and **TransactionType** +```csharp +public enum TicketStatusEnum +{ + Reserved = 1, + Paid = 2, + CancelledByUser = 3, + CancelledBySystem = 4, + Used = 5, + Expired = 6 +} +``` +```csharp +public enum VehicleTypeEnum +{ + Airplane = 1, + Train = 2, + Bus = 3 +} +``` +```csharp +public enum TransactionTypeEnum +{ + Deposit = 1, + Withdraw = 2 +} +``` + +- [ ] Add the lock-service to lock the transportation through reservation, then register it: +```csharp +public class TransportationLockService : ITransportationLockService +{ + private readonly ConcurrentDictionary _locks = new(); + + public async Task AcquireLockAsync(long transportationId) + { + var semaphore = _locks.GetOrAdd(transportationId, new SemaphoreSlim(1, 1)); + await semaphore.WaitAsync(); + return new Releaser(() => semaphore.Release()); + } + + private class Releaser : IDisposable + { + private readonly Action _release; + + public Releaser(Action release) + { + _release = release; + } + + public void Dispose() + { + _release(); + } + } +} +``` + +- [ ] Add method `CreateAsync` in `TransportationService` +```csharp +public async Task> CreateAsync(long accountId, TransactionDto dto) +{ + Transaction transaction = new(); + _mapper.Map(dto, transaction); + transaction.AccountId = accountId; + + await _transactionRepository.InsertAsync(transaction); + await _unitOfWork.CompleteAsync(); + return Result.Success(transaction.Id); +} +``` + +- [ ] Add method `PayForTicketOrderAsync` in `AccountService` +```csharp +public async Task> PayForTicketOrderAsync(long accountId, long ticketOrderId, decimal baseAmount, decimal finalAmount) +{ + var account = await _accountRepository.GetByIdAsync(accountId); + if (account == null) + { + return Result.Error(0, "Account not found"); + } + + if (account.Balance < finalAmount) + { + return Result.Error(0, "Not enough money"); + } + + account.Withdraw(finalAmount); + _accountRepository.Update(account); + await _unitOfWork.CompleteAsync(); + + TransactionDto dto = new() + { + CreatedAt = DateTime.UtcNow, + Description = "Payment for ticket order #" + ticketOrderId + " at " + DateTime.UtcNow, + BaseAmount = baseAmount, + FinalAmount = finalAmount, + SerialNumber = Guid.NewGuid().ToString("N"), + TicketOrderId = ticketOrderId, + TransactionTypeId = (int)TransactionTypeEnum.Withdraw, + TransactionType = TransactionTypeEnum.Withdraw.ToString() + }; + + return await _transactionService.CreateAsync(accountId, dto); +} +``` + +- [ ] Create `ITicketOrderService`, `TicketOrderService` and implement `CreateTicketOrderAsync` +```csharp +public async Task> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto) +{ + // get the account + var account = await _accountRepository.GetByIdAsync(accountId); + if (account == null) + { + return Result.Error(0, "Account not found"); + } + + // get the transportation + var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId); + if (transportation == null) + { + return Result.Error(0, "Transportation not found"); + } + + // lock the transportation through reservation + using (await _transportationLockService.AcquireLockAsync(dto.TransportationId)) + { + var baseAmount = transportation.BasePrice * dto.Travellers.Count; + if (account.Balance < baseAmount) + { + return Result.Error(0, "Not enough money"); + } + // check validity of transportation + var checkSeats = ValidateTransportationAndSeats(transportation, dto.Travellers); + if (!string.IsNullOrEmpty(checkSeats)) + { + return Result.Error(0, checkSeats); + } + + var finalAmount = baseAmount; + await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travellers); + await UpsertTravellers(accountId, dto.Travellers); + + // add the ticket order by the info we have + TicketOrder ticketOrder = new() + { + BuyerId = accountId, + CreatedAt = DateTime.UtcNow, + Description = "", + SerialNumber = Guid.NewGuid().ToString("N"), + TransportationId = dto.TransportationId + }; + await _ticketOrderRepository.InsertAsync(ticketOrder); + + foreach (var traveller in dto.Travellers) + { + if (!traveller.SeatId.HasValue) + { + return Result.Error(0, "Seat ID is required for each traveller"); + } + + Ticket ticket = new() + { + CreatedAt = DateTime.UtcNow, + Description = traveller.Description, + SeatId = traveller.SeatId.Value, + SerialNumber = Guid.NewGuid().ToString("N"), + TicketOrder = ticketOrder, + TicketStatusId = 1, + TravelerId = traveller.Id, + }; + await _ticketRepository.InsertAsync(ticket); + } + + await _unitOfWork.CompleteAsync(); + await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id, + baseAmount, finalAmount); + return Result.Success(ticketOrder.Id); + } +} +``` +- [ ] Register `TicketOrderService` in DI container. ## 🎯 Controller Layer @@ -125,7 +340,3 @@ https://github.com/MehrdadShirvani/AlibabaClone-Backend/commits/develop/ # Merge - [ ] Create a PR and merge the current branch with develop - ---- - -