Files
ASP.NET/ProjectOrientedSessions/Session09/Session09 Backend.md
T
AminandGitHub 39e8c28bd0 Update Session09 Backend.md
updated up to endpoint of reservation
2025-07-09 18:32:29 +03:30

13 KiB

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

  • Fix GUID generation and async handling in ticket creation. (Use Guid.NewGuid() instead of new Guid())
SerialNumber = Guid.NewGuid().ToString("N")
  • Make SerialNumber, TicketOrderId, BaseAmount publicly settable in DTOs if they're private.
  • Check Transaction and make sure SerialNumber is not a required property.

Branching

  • Create the feature/ticket-reservation branch based on develop

Ticket Ordering System

🧱 Domain and Infrastructure Setup

  • Check out new ERD: Here
  • Add migrations for the above database changes.

🧑‍💼 Path to Service Layer

  • Create DTOs CreateTravellerTicketDto and CreateTicketOrderDto and the mappings:
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; }
}
public class CreateTicketOrderDto
{
    public long TransportationId { get; set; }
    public List<CreateTravellerTicketDto> 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
public Task<List<Seat>> 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
public enum TicketStatusEnum
{
    Reserved = 1,
    Paid = 2,
    CancelledByUser = 3,
    CancelledBySystem = 4,
    Used = 5,
    Expired = 6
}
public enum VehicleTypeEnum
{
    Airplane = 1,
    Train = 2,
    Bus = 3
}
public enum TransactionTypeEnum
{
    Deposit = 1,
    Withdraw = 2
}
  • Add the lock-service to lock the transportation through reservation, then register it:
public class TransportationLockService : ITransportationLockService
{
    private readonly ConcurrentDictionary<long, SemaphoreSlim> _locks = new();

    public async Task<IDisposable> 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
public async Task<Result<long>> CreateAsync(long accountId, TransactionDto dto)
{
    Transaction transaction = new();
    _mapper.Map(dto, transaction);
    transaction.AccountId = accountId;

    await _transactionRepository.InsertAsync(transaction);
    await _unitOfWork.CompleteAsync();
    return Result<long>.Success(transaction.Id);
}
  • Add method PayForTicketOrderAsync in AccountService
public async Task<Result<long>> PayForTicketOrderAsync(long accountId, long ticketOrderId, decimal baseAmount, decimal finalAmount)
{
    var account = await _accountRepository.GetByIdAsync(accountId);
    if (account == null)
    {
        return Result<long>.Error(0, "Account not found");
    }

    if (account.Balance < finalAmount)
    {
        return Result<long>.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
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
{
    // get the account
    var account = await _accountRepository.GetByIdAsync(accountId);
    if (account == null)
    {
        return Result<long>.Error(0, "Account not found");
    }

    // get the transportation
    var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
    if (transportation == null)
    {
        return Result<long>.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<long>.Error(0, "Not enough money");
        }
        // check validity of transportation
        var checkSeats = ValidateTransportationAndSeats(transportation, dto.Travellers);
        if (!string.IsNullOrEmpty(checkSeats))
        {
            return Result<long>.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<long>.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<long>.Success(ticketOrder.Id);
    }
}
  • Register TicketOrderService in DI container.

🎯 Controller Layer

  • Create TicketOrderController with the endpoint POST /CreateTicketOrder
[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "User")]
public class TicketOrderController : ControllerBase
{
    private readonly IUserContext _userContext;
    private readonly ITicketOrderService _ticketOrderService;

    public TicketOrderController(IUserContext userContext,
        ITicketOrderService ticketOrderService)
    {
        _userContext = userContext;
        _ticketOrderService = ticketOrderService;
    }

    [HttpPost("create-order")]
    public async Task<IActionResult> CreateTicketOrder([FromBody] CreateTicketOrderDto dto)
    {
        long accountId = _userContext.GetUserId();
        // check for account-id to be valid
        if (accountId <= 0)
        {
            return Unauthorized();
        }

        var result = await _ticketOrderService.CreateTicketOrderAsync(accountId, dto);
        if (result.IsSuccess)
        {
            return Ok(result.Data);
        }

        return result.Status switch
        {
            ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
            ResultStatus.NotFound => NotFound(result.ErrorMessage),
            ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
            _ => StatusCode(500, result.ErrorMessage)
        };
    }
}

🗃️ Repository Layer

Implement there methods in TicketOrderRepository and its interface

  • FindAndLoadAllDetails
public Task<TicketOrder?> FindAndLoadAllDetailsAsync(long id)
{
    var ticketOrder = DbSet
        .Include(to => to.Transportation).ThenInclude(t => t.FromLocation).ThenInclude(fl => fl.City)
        .Include(to => to.Transportation).ThenInclude(t => t.ToLocation).ThenInclude(tl => tl.City)
        .Include(to => to.Tickets).ThenInclude(t => t.Traveler)
        .Where(to => to.Id == id).FirstOrDefaultAsync();

    return ticketOrder;
}
  • GetAllByBuyerId
public async Task<List<TicketOrder>> GetAllByBuyerId(long buyerId)
{
    var ticketOrders = await DbSet
        .Include(to => to.Transaction)
        .Include(to => to.Transportation).ThenInclude(t => t.FromLocation)
        .Include(to => to.Transportation).ThenInclude(t => t.ToLocation)
        .Include(to => to.Transportation).ThenInclude(t => t.Company)
        .Include(to => to.Transportation).ThenInclude(t => t.Vehicle)
        .Where(to => to.BuyerId == buyerId).ToListAsync();
    return ticketOrders;
}

Transportation and Seat Selection

  • Add TransportationSeatDto.
  • Add method GetSeatsByVehicleId in ISeatRepository and implement it.
  • Add mapping from Seat to TransportationSeatDto.
  • Add GetTransportationSeatsAsync in ITransportationService and implement.
  • Add GetTransportationSeats endpoint in TransportationController.
  • Fix vehicle and seat-related mapping issues (e.g., missing VehicleTypeId, logic errors).
  • Ensure RemainingCapacity is treated as calculated (ignored in EF, removed from schema).

Coupon System

🏗️ Domain & Infrastructure

  • Add Coupon entity with IsExpired, CouponCode (unique).
  • Add ICouponRepository, CouponRepository.
  • Add DiscountDto, CouponValidationRequestDto.
  • Add migrations for new Coupon table.

🧑‍💼 Service & Logic

  • Create ICouponService, implement validation logic.
  • Add coupon validation in TicketOrderService.

🎯 Controller

  • Add ValidateCoupon endpoint in CouponController.

Payment & Transactions

  • Add CouponId to PayForTicketOrderAsync in IAccountService & implementation.
  • Add CouponId to TransactionDto (adjusted to CouponCode later).
  • Add TopUpAccount logic (DTO, Controller, Service).
  • Add Transaction relationship to TicketOrder as one-to-one.
  • Add logic for creating transactions with tickets.
  • Add endpoint GetMyTransactions and DTOs (TransactionDto).

Ticket Review & Confirmation

  • Add TicketOrderSummaryDto, map details: from/to city, company, vehicle, time.
  • Implement GetTravelOrderDetails endpoint to fetch ticket summary.
  • Display number of travelers, per-seat price, and total cost.
  • Include coupon entry and balance payment option.
  • Use ticket + person data (via GetTicketOrderTravelersDetails).

PDF Generation

  • Install QuestPDF in infrastructure.
  • Create IPdfGenerator, implement with QuestPDF.
  • Register IPdfGenerator service.
  • Create PdfGenerator logic to render ticket PDFs.
  • Add PDF download endpoint (DownloadPdf) in TicketOrderController.

Merge

  • Create a PR and merge the current branch with develop