Add Deployment
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
vehicles = [
|
||||
{"id": 1, "title": "Volvo 9700", "type": 1, "capacity": 50},
|
||||
{"id": 2, "title": "Scania Touring", "type": 1, "capacity": 52},
|
||||
{"id": 3, "title": "TGV Duplex", "type": 2, "capacity": 510},
|
||||
{"id": 4, "title": "Shinkansen N700", "type": 2, "capacity": 1323},
|
||||
{"id": 5, "title": "Boeing 777", "type": 3, "capacity": 396},
|
||||
{"id": 6, "title": "Airbus A380", "type": 3, "capacity": 469}
|
||||
]
|
||||
|
||||
def generate_seats(vehicle):
|
||||
seats = []
|
||||
vehicle_id = vehicle["id"]
|
||||
capacity = vehicle["capacity"]
|
||||
vtype = vehicle["type"]
|
||||
vip_rows = 3 if capacity >= 50 else 1
|
||||
|
||||
if vtype == 1: # Bus: 2+2 layout
|
||||
seats_per_row = 4
|
||||
elif vtype == 2: # Train: 2+2 layout
|
||||
seats_per_row = 4
|
||||
elif vtype == 3: # Airplane: 3+4+3 (approx 10 per row)
|
||||
seats_per_row = 10
|
||||
else:
|
||||
seats_per_row = 4 # default
|
||||
|
||||
rows = (capacity + seats_per_row - 1) // seats_per_row
|
||||
|
||||
seat_id = 1
|
||||
for row in range(1, rows + 1):
|
||||
for col in range(1, seats_per_row + 1):
|
||||
if seat_id > capacity:
|
||||
break
|
||||
seat = {
|
||||
"Id": seat_id,
|
||||
"VehicleId": vehicle_id,
|
||||
"Row": row,
|
||||
"Column": col,
|
||||
"IsVIP": row <= vip_rows,
|
||||
"IsAvailable": True,
|
||||
"Description": ""
|
||||
}
|
||||
seats.append(seat)
|
||||
seat_id += 1
|
||||
return seats
|
||||
|
||||
# Generate and print all
|
||||
all_seats = []
|
||||
for v in vehicles:
|
||||
seats = generate_seats(v)
|
||||
all_seats.extend(seats)
|
||||
|
||||
# Print as SQL INSERTs (optional)
|
||||
for s in all_seats:
|
||||
print(f"INSERT INTO Seats (Id, VehicleId, Row, Column, IsVIP, IsAvailable, Description) VALUES "
|
||||
f"({s['Id']}, {s['VehicleId']}, {s['Row']}, {s['Column']}, {str(s['IsVIP']).lower()}, "
|
||||
f"{str(s['IsAvailable']).lower()}, '{s['Description']}');")
|
||||
@@ -0,0 +1,560 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## Fixes
|
||||
- [ ] Check for missing `.ValueGeneratedOnAdd()` in all entity configurations in ```Infrastructure/Configurations```.
|
||||
|
||||
- [ ] Adjust the database setup according to the second version of ERD
|
||||
- Some aggregates are added/modified to generate the new database
|
||||
- New ERD: [Here](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/docs/AlibabaERD-Version02.pdf)
|
||||
- Note: Pay attention to the changes in logic and implementation of Person table (Id number is not unique anymore). So remove this code in PersonConfiguration:
|
||||
|
||||
```csharp
|
||||
builder.HasIndex(p => p.IdNumber)
|
||||
.IsUnique();
|
||||
```
|
||||
|
||||
- [ ] Add actual data in `TicketStatus`, `Gender`, `TransactionTypes`
|
||||
- You can either add the data in DbContext or the database itself
|
||||
|
||||
- [ ] Add data in `Seat`, `Person`, `TicketOrder`, `Transaction`, `Ticket` for test. It is recommended to write python code for generating data for Seat table, according to the data already stored in Transportation and related Vehicle data. There is also a [SeatGenerator](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/Session08/SeatGenerator.py) in this repository, as well.
|
||||
|
||||
- [ ] Fix claim extraction (`sub` → standardize JWT claim mapping). (`IUserContext` implementation)
|
||||
|
||||
First, create an interface in Application layer:
|
||||
```csharp
|
||||
public interface IUserContext
|
||||
{
|
||||
long GetUserId();
|
||||
}
|
||||
```
|
||||
Then, implement it in WebAPI in Auth folder:
|
||||
```csharp
|
||||
public class UserContext : IUserContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public UserContext(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public long GetUserId()
|
||||
{
|
||||
var userIdStr = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (long.TryParse(userIdStr, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
throw new InvalidOperationException("User ID is not available or invalid.");
|
||||
}
|
||||
}
|
||||
```
|
||||
Finally, register things in Program.cs
|
||||
```csharp
|
||||
// register user context
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IUserContext, UserContext>();
|
||||
```
|
||||
|
||||
## ✅ Profile Page (Account Info Tab)
|
||||
|
||||
- [ ] Create `ProfileDto` to represent combined data for account, person, bank detail, balance.
|
||||
```csharp
|
||||
public class ProfileDto
|
||||
{
|
||||
// from account
|
||||
public string AccountPhoneNumber { get; set; }
|
||||
public string Email { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
|
||||
// from person
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string IdNumber { get; set; }
|
||||
public string PersonPhoneNumber { get; set; }
|
||||
public DateTime? BirthDate { get; set; }
|
||||
|
||||
// from bank-account
|
||||
public string IBAN { get; set; }
|
||||
public string BankAccountNumber { get; set; }
|
||||
public int CardNumber { get; set; }
|
||||
}
|
||||
```
|
||||
You can also find another version of this DTO in [Here](https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/DTOs/Account/ProfileDto.cs)
|
||||
|
||||
- [ ] Add `GetProfileAsync` in `AccountRepository` and expose via `AccountController`.
|
||||
- First, map dto to aggregate
|
||||
```csharp
|
||||
CreateMap<Account, ProfileDto>()
|
||||
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
|
||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||
.ForMember(dest => dest.Balance, opt => opt.MapFrom(src => src.Balance))
|
||||
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.Person != null ? src.Person.FirstName : ""))
|
||||
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.Person != null ? src.Person.LastName : ""))
|
||||
.ForMember(dest => dest.IdNumber, opt => opt.MapFrom(src => src.Person != null ? src.Person.IdNumber : ""))
|
||||
.ForMember(dest => dest.BirthDate, opt => opt.MapFrom(src => src.Person != null ? src.Person.BirthDate : (DateTime?) null))
|
||||
.ForMember(dest => dest.IBAN, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.IBAN : ""))
|
||||
.ForMember(dest => dest.BankAccountNumber, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.BankAccountNumber : ""))
|
||||
.ForMember(dest => dest.CardNumber, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.CardNumber : ""));
|
||||
```
|
||||
- Then, add the equivalent methods for AccountRepository, AccountService and AccountController
|
||||
```csharp
|
||||
public class AccountRepository : BaseRepository<AlibabaDbContext, Account, long>, IAccountRepository
|
||||
{
|
||||
public AccountRepository(AlibabaDbContext context) : base(context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<Account> GetByPhoneNumberAsync(string phoneNumber)
|
||||
{
|
||||
var user = await DbSet.Include(a => a.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<Account> GetProfileAsync(long accountId)
|
||||
{
|
||||
var profile = await DbSet
|
||||
.Include(a => a.Person)
|
||||
.Include(a => a.BankAccount)
|
||||
.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```csharp
|
||||
public class AccountService : IAccountService
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public async Task<Result<ProfileDto>> GetProfileAsync(long accountId)
|
||||
{
|
||||
var result = await _accountRepository.GetProfileAsync(accountId);
|
||||
if (result == null)
|
||||
{
|
||||
return Result<ProfileDto>.NotFound(null);
|
||||
}
|
||||
|
||||
return Result<ProfileDto>.Success(_mapper.Map<ProfileDto>(result));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
private readonly IUserContext _userContext;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public AccountController(IUserContext userContext, IAccountService accountService)
|
||||
{
|
||||
_userContext = userContext;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
[HttpGet("profile")]
|
||||
public async Task<IActionResult> GetProfile()
|
||||
{
|
||||
// get account-id from token
|
||||
long userId = _userContext.GetUserId();
|
||||
// check for user-id to be valid
|
||||
if (userId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetProfileAsync(userId);
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: You should add necessary interfaces and implement them
|
||||
|
||||
- [ ] Implement:
|
||||
- [ ] Edit Email (with validation): `EditEmailDto`, service method, and controller endpoint.
|
||||
```csharp
|
||||
public class EditEmailDto
|
||||
{
|
||||
[EmailAddress(ErrorMessage = "Invalid email address format")]
|
||||
public string NewEmail { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit Password: `EditPasswordDto`, service and controller.
|
||||
```csharp
|
||||
public class EditPasswordDto
|
||||
{
|
||||
[Required(ErrorMessage = "Old password is required")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "New password is required")]
|
||||
[MinLength(8, ErrorMessage = "At least 8 chars")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Password doesn't match")]
|
||||
public string ConfirmNewPassword { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit Person Info: `PersonDto`, and endpoint to `upsert` personal data.
|
||||
```csharp
|
||||
public class PersonDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long CreatorId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Firstname is required")]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Lastname is required")]
|
||||
public string LastName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "National Id number is required")]
|
||||
[RegularExpression(@"^\d{10}$", ErrorMessage = "National ID number must be exactly 10 digits")]
|
||||
public string IdNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Gender is required")]
|
||||
public short GenderId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Phone number is required")]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Birth date is required")]
|
||||
public DateTime BirthDate { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit `BankAccountDetail`: `UpsertBankAccountDetailDto` and relevant logic.
|
||||
```csharp
|
||||
public class UpsertBankAccountDto
|
||||
{
|
||||
[MinLength(24)]
|
||||
[MaxLength(24)]
|
||||
public string? IBAN { get; set; }
|
||||
|
||||
[MinLength(16)]
|
||||
[MaxLength(16)]
|
||||
public string? CardNumber { get; set; }
|
||||
|
||||
[MinLength(8)]
|
||||
public string? BankAccountNumber { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add mapping for all Dtos and check related properties like `CreatorAccountId`.
|
||||
|
||||
## ✅ List of Travelers
|
||||
|
||||
- [ ] Add `GetMyPeople` endpoint in `AccountController`. To do so, first add essential methods in `PersonRepository`, `AccountService` and related interfaces.
|
||||
|
||||
- [ ] Implement `UpsertAccountPerson` and `UpsertPerson`. Note that they should be considered separated.
|
||||
```csharp
|
||||
public async Task<Result<long>> UpsertAccountPersonAsync(long accountId, PersonDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
throw new Exception("Account not found");
|
||||
}
|
||||
|
||||
// if account is not null, update its person
|
||||
Person person;
|
||||
if (account.PersonId.HasValue)
|
||||
{
|
||||
person = await _personRepository.GetByIdAsync(account.PersonId.Value);
|
||||
if (person == null)
|
||||
{
|
||||
return Result<long>.Error(0, "No person found for this account");
|
||||
}
|
||||
|
||||
_mapper.Map(dto, person);
|
||||
person.CreatorId = account.Id;
|
||||
person.Id = account.PersonId.Value;
|
||||
_personRepository.Update(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
person = _mapper.Map<Person>(dto);
|
||||
person.CreatorId = account.Id;
|
||||
await _personRepository.InsertAsync(person);
|
||||
}
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
account.PersonId = person.Id;
|
||||
_accountRepository.Update(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
return Result<long>.Success(person.Id);
|
||||
}
|
||||
|
||||
public async Task<Result<long>> UpsertPersonAsync(long accountId, PersonDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
throw new Exception("Account not found");
|
||||
}
|
||||
|
||||
Person person = (await _personRepository.FindAsync(p => p.IdNumber == dto.IdNumber && p.CreatorId == accountId)).FirstOrDefault();
|
||||
if (person != null)
|
||||
{
|
||||
if (dto.Id > 0 && dto.Id != person.Id)
|
||||
{
|
||||
return Result<long>.Error(0, "A person with this id number exists");
|
||||
}
|
||||
_mapper.Map(dto, person);
|
||||
person.CreatorId = accountId;
|
||||
_personRepository.Update(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
person = _mapper.Map<Person>(dto);
|
||||
person.CreatorId = accountId;
|
||||
await _personRepository.InsertAsync(person);
|
||||
}
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
return Result<long>.Success(person.Id);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[HttpPost("account-person")]
|
||||
public async Task<IActionResult> UpsertAccountPerson([FromBody] PersonDto dto)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _personService.UpsertAccountPersonAsync(accountId, dto);
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Success => NoContent(),
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("person")]
|
||||
public async Task<IActionResult> UpsertPerson([FromBody] PersonDto dto)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _personService.UpsertPersonAsync(accountId, dto);
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Success => NoContent(),
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## ✅ My Travels Tab
|
||||
|
||||
- [ ] Create `TicketOrderSummaryDto` (includes cities, vehicle name, price, etc.).
|
||||
```csharp
|
||||
public class TicketOrderSummaryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string SerialNumber { get; set; }
|
||||
public DateTime BoughtAt { get; set; }
|
||||
|
||||
// transaction
|
||||
public decimal Price { get; set; }
|
||||
|
||||
// transportation
|
||||
public DateTime TravelStartDate { get; set; }
|
||||
public DateTime? TravelEndDate { get; set; }
|
||||
|
||||
// city
|
||||
public string FromCity { get; set; }
|
||||
public string ToCity { get; set; }
|
||||
|
||||
// company
|
||||
public string CompanyName { get; set; }
|
||||
|
||||
// vehicle data
|
||||
public short VehicleTypeId { get; set; }
|
||||
public string VehicleName { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add `GetTravels` in `AccountService`, and expose `GetMyTravels` in controller.
|
||||
|
||||
First, update interfaces and TicketOrderRepository, add the **mappings** and then go for the other things
|
||||
|
||||
In AccountService:
|
||||
```csharp
|
||||
public async Task<Result<List<TicketOrderSummaryDto>>> GetTravelsAsync(long accountId)
|
||||
{
|
||||
var result = await _ticketOrderRepository.GetAllByBuyerId(accountId);
|
||||
if (result == null)
|
||||
{
|
||||
return Result<List<TicketOrderSummaryDto>>.NotFound(null);
|
||||
}
|
||||
|
||||
return Result<List<TicketOrderSummaryDto>>.Success(_mapper.Map<List<TicketOrderSummaryDto>>(result));
|
||||
}
|
||||
```
|
||||
|
||||
In AccountController:
|
||||
```csharp
|
||||
[HttpGet("my-travels")]
|
||||
public async Task<IActionResult> GetMyTravels()
|
||||
{
|
||||
long buyerId = _userContext.GetUserId();
|
||||
if (buyerId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetTravelsAsync(buyerId);
|
||||
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)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ My Transactions Tab
|
||||
|
||||
- [ ] Create `TransactionDto` and mapping.
|
||||
```csharp
|
||||
public class TransactionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public short TransactionTypeId { get; set; }
|
||||
public long AccountId { get; set; }
|
||||
public long? TicketOrderId { get; set; }
|
||||
public decimal BaseAmount { get; set; }
|
||||
public decimal FinalAmount { get; set; }
|
||||
public required string SerialNumber { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string TransactionType { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add method to get transactions by `AccountId` in `TransactionRepository`.
|
||||
```csharp
|
||||
public async Task<List<Transaction>> GetTransactionsByAccountIdAsync(long accountId)
|
||||
{
|
||||
var transactions = await DbSet
|
||||
.Include(t => t.TransactionType)
|
||||
.Include(t => t.TicketOrder)
|
||||
.Where(t => t.AccountId == accountId).ToListAsync();
|
||||
return transactions;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Expose `GetMyTransactions` in `AccountController`. It's obvious you should first add the essential method `GetTransactionsAsync` in `AccountService`.
|
||||
```csharp
|
||||
[HttpGet("my-transactions")]
|
||||
public async Task<IActionResult> GetMyTransactions()
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetTransactionsAsync(accountId);
|
||||
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)
|
||||
};
|
||||
}
|
||||
```
|
||||
- [ ] Add modal to simulate balance top-up (manual input).
|
||||
```csharp
|
||||
public class TopUpDto
|
||||
{
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Add `TransactionService` and use its method `CreateTopUpAsync` to create and add a new transaction in `AccountService`. Then add an endpoint just like before.
|
||||
```csharp
|
||||
public async Task<Result<long>> TopUpAsync(long accountId, TopUpDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
return Result<long>.Error(0, "Account not found");
|
||||
}
|
||||
|
||||
account.Deposit(dto.Amount);
|
||||
_accountRepository.Update(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
var transactionId = await _transactionService.CreateTopUpAsync(accountId, dto.Amount);
|
||||
return Result<long>.Success(transactionId.Data);
|
||||
}
|
||||
```
|
||||
|
||||
## Postman
|
||||
Considering that all endpoints in `AccountController` require Authorization, You need to test your API in **Postman**.
|
||||
|
||||
<br />
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c2/Postman_%28software%29.png" width="60%">
|
||||
|
||||
Postman is a client which lets the user test api professionally.
|
||||
You can download it in [this link](https://www.postman.com/downloads/) and get started with it using [this video](https://www.youtube.com/watch?v=wEOLZq-7DYs&pp=0gcJCfwAo7VqN5tD)
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
- Check the codes as they're put here before **debugging**, you can check them with the repositories.
|
||||
- Complete the task step by step in each endpoint to preserve the principals of *Clean Architecture*.
|
||||
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## ⚙️ Tooling & Fixes
|
||||
- [ ] Install and configure `react-hook-form`
|
||||
```
|
||||
npm install react-hook-form
|
||||
```
|
||||
|
||||
## 🔐 Account & Authentication
|
||||
- [ ] Use Axios request interceptor to attach token to requests and handle logout logic
|
||||
```ts
|
||||
// add a request interceptor to include JWT token if available
|
||||
agent.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token;
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// add a response interceptor to handle 401 Unauthorized
|
||||
agent.interceptors.response.use(
|
||||
(res) => res,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = "/login"; // redirect to login
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
- [ ] Provide a method for each new endpoint implemented in backend
|
||||
```ts
|
||||
const Profile = {
|
||||
getProfile: () => request.get<ProfileDto>('/account/profile'),
|
||||
editEmail: (data: EditEmailDto) => request.put<void>('/account/email', data),
|
||||
editPassword: (data: EditPasswordDto) => request.put<void>('/account/password', data),
|
||||
upsertAccountPerson: (data: PersonDto) => request.post<number>('/account/account-person', data),
|
||||
upsertPerson: (data: PersonDto) => request.post<number>('/account/person', data),
|
||||
upsertBankDetail: (data: UpsertBankAccountDetailDto) => request.post<void>('/account/bank-detail', data),
|
||||
getMyPeople: () => request.get<PersonDto[]>('/account/my-people'),
|
||||
getMyTravels: () => request.get<TicketOrderSummaryDto[]>('/account/my-travels'),
|
||||
getMyTransactions: () => request.get<TransactionDto[]>('/account/my-transactions'),
|
||||
topUp: (data : topUpDto) => request.post<number>('/account/top-up', data)
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## 🗂️ Additional Profile Tabs (initial setup)
|
||||
|
||||
- [ ] Add empty pages/tabs for:
|
||||
- [ ] `ProfileSummary`
|
||||
- [ ] `MyTravels`
|
||||
- [ ] `MyTransactions`
|
||||
- [ ] `MyPeople`
|
||||
|
||||
- [ ] Implement `ProfilePage` using the components created. This is an example of how it can be done:
|
||||
```ts
|
||||
import InfoAccount from "./InfoAccount";
|
||||
import InfoPeople from "./InfoPeople";
|
||||
import InfoTransactions from "./InfoTransactions";
|
||||
import InfoTravels from "./InfoTravels";
|
||||
import { useState } from "react";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Account", component: <InfoAccount /> },
|
||||
{ label: "Transactions", component: <InfoTransactions /> },
|
||||
{ label: "Travels", component: <InfoTravels /> },
|
||||
{ label: "People", component: <InfoPeople /> }
|
||||
];
|
||||
|
||||
const Profile = () => {
|
||||
const [selected, setSelected] = useState(0);
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<aside className="md:w-64 w-full bg-white rounded-lg shadow p-4 flex md:flex-col flex-row gap-2 md:gap-0 mb-4 md:mb-0">
|
||||
{tabs.map((tab, idx) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
className={`text-right px-4 py-2 rounded transition font-medium text-base md:text-lg w-full ${
|
||||
selected === idx
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "hover:bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
onClick={() => setSelected(idx)}
|
||||
type="button"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
<main className="flex-1 min-w-0">{tabs[selected].component}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
```
|
||||
|
||||
- [ ] Create prototype of `ProfilePage` and integrate with navbar. (Create a button or link to access `ProfilePage`, only when user is logged in)
|
||||
|
||||
- [ ] Define and adjust routes for profile and its tabs
|
||||
```ts
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
|
||||
```
|
||||
|
||||
- [ ] Implement tab handling inside `ProfilePage`. You can also do it **route-based**.
|
||||
|
||||
## Profile Summary
|
||||
### 📦 DTOs / Models
|
||||
- Add models for:
|
||||
- [ ] `EditEmailDto`
|
||||
```ts
|
||||
export interface EditEmailDto {
|
||||
newEmail: string;
|
||||
}
|
||||
```
|
||||
- [ ] `EditPasswordDto`
|
||||
```ts
|
||||
export interface EditPasswordDto {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
||||
```
|
||||
- [ ] `PersonDto`
|
||||
```ts
|
||||
export interface PersonDto {
|
||||
id?: number;
|
||||
creatorAccountId?: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
idNumber: string;
|
||||
genderId: number;
|
||||
phoneNumber: string;
|
||||
birthDate: string;
|
||||
}
|
||||
```
|
||||
- [ ] `ProfileDto`
|
||||
```ts
|
||||
export interface ProfileDto {
|
||||
accountPhoneNumber: string;
|
||||
email: string;
|
||||
balance: number;
|
||||
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
idNumber: string;
|
||||
personPhoneNumber: string;
|
||||
birthDate: Date | string | null;
|
||||
|
||||
iban: string;
|
||||
bankAccountNumber: string;
|
||||
cardNumber: string;
|
||||
}
|
||||
```
|
||||
- [ ] `UpsertBankAccountDetailDto`
|
||||
```ts
|
||||
export interface UpsertBankAccountDto {
|
||||
iban?: string;
|
||||
bankAccountNumber?: string;
|
||||
cardNumber?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement the process of showing and editing the data. To do so, you can use modal components and set their functionality in the pages.
|
||||
|
||||
## 🧍 List of Travelers
|
||||
- [ ] Implement `ListOfTravelers` page for showing, editing, and adding new people. You can use the same modal used for `UpsertAccountPerson`.
|
||||
|
||||
## 💳 Transactions Module
|
||||
- [ ] Add `TransactionDto` model
|
||||
```ts
|
||||
export interface TransactionDto {
|
||||
id: number;
|
||||
transactionTypeId: number;
|
||||
accountId: number;
|
||||
ticketOrderId?: number;
|
||||
baseAmount: number;
|
||||
finalAmount: number;
|
||||
serialNumber: string;
|
||||
createdAt: Date | string;
|
||||
description?: string;
|
||||
transactionType: string;
|
||||
}
|
||||
```
|
||||
- [ ] Implement `MyTransactions` page
|
||||
Note: It is recommended to add a component to be displayed as a card, including the table of information, then use it in the page of MyTransactions
|
||||
|
||||
## 🚆 Travel Module
|
||||
- [ ] Add `TicketOrderSummaryDto` model
|
||||
```ts
|
||||
export interface TicketOrderSummaryDto {
|
||||
id: number;
|
||||
serialNumber: string;
|
||||
boughtAt: Date | string;
|
||||
|
||||
price: number;
|
||||
|
||||
travelStartDate: Date | string;
|
||||
travelEndDate: Date | string;
|
||||
|
||||
fromCity: string;
|
||||
toCity: string;
|
||||
|
||||
companyName: string;
|
||||
|
||||
vehicleTypeId: number;
|
||||
vehicleName: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement `MyTravels` page. Note that this page can be similarly implemented by a card.
|
||||
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user