diff --git a/Application/Application.csproj b/Application/Application.csproj
index fbeb5fd..07f49de 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -19,4 +19,10 @@
+
+
+ C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.23\Microsoft.AspNetCore.Http.Abstractions.dll
+
+
+
diff --git a/Application/AssemblyReference.cs b/Application/AssemblyReference.cs
deleted file mode 100644
index 2c00c5e..0000000
--- a/Application/AssemblyReference.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Application;
-
-public sealed class AssemblyReference
-{
-
-}
\ No newline at end of file
diff --git a/Application/Features/Appointments/DTOs/CreateAppointment.cs b/Application/Features/Appointments/DTOs/CreateAppointment.cs
index 5d3c7bf..b11c44b 100644
--- a/Application/Features/Appointments/DTOs/CreateAppointment.cs
+++ b/Application/Features/Appointments/DTOs/CreateAppointment.cs
@@ -12,4 +12,6 @@ public class CreateAppointment
public DateTime EndTime { get; set; }
[Required]
public string AppoinmentTitle { get; set; }
+ [Required]
+ public Guid TenantId { get; set; }
}
\ No newline at end of file
diff --git a/Application/Features/Appointments/Services/AppointmentService.cs b/Application/Features/Appointments/Services/AppointmentService.cs
index c81356c..6e7bd74 100644
--- a/Application/Features/Appointments/Services/AppointmentService.cs
+++ b/Application/Features/Appointments/Services/AppointmentService.cs
@@ -40,7 +40,7 @@ public async Task CreateAppointmentAsync(CreateAppointment dto)
throw new UnauthorizedAccessException("Not Authenticated");
var userId = _userContext.UserId;
- var appointment = new Appointment(userId, dto.StartTime, dto.EndTime, dto.AppoinmentTitle);
+ var appointment = new Appointment(userId, dto.StartTime, dto.EndTime, dto.AppoinmentTitle,dto.TenantId);
if (!await _userRepository.IsUserExistsByIdAsync(userId))
throw new NotFoundException($"User with {userId} does not exist");
diff --git a/Application/Features/Payment/DTOs/CreatePaymentDto.cs b/Application/Features/Payment/DTOs/CreatePaymentDto.cs
index 2e6111d..4698eac 100644
--- a/Application/Features/Payment/DTOs/CreatePaymentDto.cs
+++ b/Application/Features/Payment/DTOs/CreatePaymentDto.cs
@@ -1,7 +1,19 @@
+using Domain.Entities;
+
namespace Application.Features.Payment.DTOs;
public class CreatePaymentDto
{
+ public Guid TenantId { get; set; }
+ public Guid AppointmentId { get; set; }
public long Amount { get; set; }
- public string PhoneNumber { get; set; }
+ public PaymentGateway Gateway { get; set; }
+
+ public string Description { get; set; }
+ public string? Mobile { get; set; }
+ public string? Email { get; set; }
+
+ public decimal? GatewayFee { get; set; }
+ public int? TokenExpiryInMinutes { get; set; }
+ public string? ReferrerId { get; set; }
}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/Saman/SamanTokenRequestDto.cs b/Application/Features/Payment/DTOs/Saman/SamanTokenRequestDto.cs
new file mode 100644
index 0000000..4f4dd17
--- /dev/null
+++ b/Application/Features/Payment/DTOs/Saman/SamanTokenRequestDto.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Application.Features.Payment.DTOs.Saman;
+
+public class SamanTokenRequestDto
+{
+ [JsonPropertyName("Action")] public string? Action { get; set; } = "token";
+ [JsonPropertyName("Amount")] public long Amount { get; set; }
+ [JsonPropertyName("Wage")] public decimal? Wage { get; set; }
+ [JsonPropertyName("ResNum")] public string? ResNum { get; set; }
+ [JsonPropertyName("CellNumber")] public string? CellNumber { get; set; }
+ [JsonPropertyName("TokenExpiryInMin")] public int TokenExpiryInMinutes { get; set; }
+ [JsonPropertyName("HashedCardNumber")] public string? HashedCardNumber { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/Saman/VerifySamanPayment.cs b/Application/Features/Payment/DTOs/Saman/VerifySamanPayment.cs
new file mode 100644
index 0000000..a33c2a4
--- /dev/null
+++ b/Application/Features/Payment/DTOs/Saman/VerifySamanPayment.cs
@@ -0,0 +1,19 @@
+
+namespace Application.Features.Payment.DTOs.Saman;
+
+public class VerifySamanPayment
+{
+ public TransactionDetail? TransactionDetail { get; set; }
+ public int? ResultCode { get; set; }
+ public string? ResultDescription { get; set; }
+ public bool Success { get; set; }
+
+}
+
+public class TransactionDetail
+{
+ public string? RRN { get; set; }
+ public string? RefNum { get; set; }
+ public string? MaskedPan { get; set; }
+ public string? HashedPan { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/SamanTokenResponseDto.cs b/Application/Features/Payment/DTOs/SamanTokenResponseDto.cs
new file mode 100644
index 0000000..010cc72
--- /dev/null
+++ b/Application/Features/Payment/DTOs/SamanTokenResponseDto.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Application.Features.Payment.DTOs;
+
+public class SamanTokenResponseDto
+{
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+ [JsonPropertyName("errorCode")]
+ public string? ErrorCode { get; set; }
+ [JsonPropertyName("errorDesc")]
+ public string? ErrorDescription { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/SandBoxCallBackDto.cs b/Application/Features/Payment/DTOs/SandBoxCallBackDto.cs
new file mode 100644
index 0000000..024e028
--- /dev/null
+++ b/Application/Features/Payment/DTOs/SandBoxCallBackDto.cs
@@ -0,0 +1,29 @@
+namespace Application.Features.Payment.DTOs;
+
+public class SandBoxCallBackDto
+{
+ public string? Authority { get; set; }
+ // public string? MID { get; set; }
+
+ public string? Status { get; set; }
+
+ public string? State { get; set; }
+
+ public string? RRN { get; set; }
+
+ public string? RefNum { get; set; }
+
+ public string? ResNum { get; set; }
+
+ public string? TraceNo { get; set; }
+
+ public long? Amount { get; set; }
+
+ public string? Wage { get; set; }
+
+ public string? CID { get; set; }
+
+ public string? SecurePan { get; set; }
+
+ public string? Token { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/SepTokenResponse.cs b/Application/Features/Payment/DTOs/SepTokenResponse.cs
deleted file mode 100644
index fa59ff3..0000000
--- a/Application/Features/Payment/DTOs/SepTokenResponse.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Application.Features.Payment.DTOs;
-
-public class SepTokenResponse
-{
- public int status { get; set; }
-
- public string? token { get; set; }
-
- public string? errorCode { get; set; }
-
- public string? errorDesc { get; set; }
-}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs b/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs
new file mode 100644
index 0000000..7fbbec7
--- /dev/null
+++ b/Application/Features/Payment/DTOs/ZarinPal/CreateZarinPalPaymentDto.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace Application.Features.Payment.DTOs.ZarinPal;
+
+public class CreateZarinPalPaymentDto
+{
+ [JsonPropertyName("amount")]
+ public long Amount { get; set; }
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+ [JsonPropertyName("referrer_id")]
+ public string? ReferrerId { get; set; }
+
+ [JsonPropertyName("matadata")]
+ public ZarinPalMataData ZarinPalMataData { get; set; }
+}
+
+public class ZarinPalMataData
+{
+ [JsonPropertyName("mobile")]
+ public string? Mobile { get; set; }
+ [JsonPropertyName("email")]
+ public string? Email { get; set; }
+ [JsonPropertyName("order_id")]
+ public string? OrderId { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/ZarinPal/ZarinPalCallBackResponse.cs b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalCallBackResponse.cs
new file mode 100644
index 0000000..858d2f7
--- /dev/null
+++ b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalCallBackResponse.cs
@@ -0,0 +1,7 @@
+namespace Application.Features.Payment.DTOs.ZarinPal;
+
+public class ZarinPalCallBackResponse
+{
+ public string Authority { get; set; }
+ public string Status { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/ZarinPal/ZarinPalResponse.cs b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalResponse.cs
new file mode 100644
index 0000000..a199e5a
--- /dev/null
+++ b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalResponse.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace Application.Features.Payment.DTOs.ZarinPal;
+
+public class ZarinPalResponse
+{
+ [JsonPropertyName("data")]
+ public ZarinPalData? Data { get; set; }
+ [JsonPropertyName("errors")]
+ public object? Error { get; set; }
+}
+
+public class ZarinPalData
+{
+ [JsonPropertyName("authority")]
+ public string Authority { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs
new file mode 100644
index 0000000..acc3f4e
--- /dev/null
+++ b/Application/Features/Payment/DTOs/ZarinPal/ZarinPalVerifyResponse.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Application.Features.Payment.DTOs.ZarinPal;
+
+public class ZarinPalVerifyResponse
+{
+ public ZarinPalVerifyData Data { get; set; }
+}
+
+public class ZarinPalVerifyData
+{
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+ [JsonPropertyName("card_pan")]
+ public string CardPan { get; set; }
+ [JsonPropertyName("ref_id")]
+ public int RefId { get; set; }
+ [JsonPropertyName("card_hash")]
+ public string CardHash { get; set; }
+ [JsonPropertyName("fee")]
+ public int Fee { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs b/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs
new file mode 100644
index 0000000..714dd7f
--- /dev/null
+++ b/Application/Features/Payment/Interfaces/PaymentGatewayProviderContract.cs
@@ -0,0 +1,14 @@
+using Application.Features.Payment.DTOs;
+using Domain.Entities;
+using Microsoft.AspNetCore.Http;
+
+namespace Application.Features.Payment.Interfaces;
+
+public interface PaymentGatewayProviderContract
+{
+ public PaymentGateway Gateway { get;}
+
+ Task RequestPaymentAsync(Domain.Entities.Payment payment, CreatePaymentDto dto);
+
+ Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto);
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/Interfaces/PaymentGatewayRequestResult.cs b/Application/Features/Payment/Interfaces/PaymentGatewayRequestResult.cs
new file mode 100644
index 0000000..fb3ff83
--- /dev/null
+++ b/Application/Features/Payment/Interfaces/PaymentGatewayRequestResult.cs
@@ -0,0 +1,26 @@
+namespace Application.Features.Payment.Interfaces;
+
+public class PaymentGatewayRequestResult
+{
+ public bool IsSuccess { get; private set; }
+ public string? GatewayToken { get; private set; }
+ public string? PaymentUrl { get; private set; }
+ public string? ErrorCode { get; private set; }
+ public string? ErrorMessage { get; private set; }
+
+ public static PaymentGatewayRequestResult Success(string gatewayToken, string paymentUrl)
+ => new()
+ {
+ IsSuccess = true,
+ GatewayToken = gatewayToken,
+ PaymentUrl = paymentUrl
+ };
+
+ public static PaymentGatewayRequestResult Failed(string? errorCode, string errorMessage)
+ => new()
+ {
+ IsSuccess = false,
+ ErrorCode = errorCode,
+ ErrorMessage = errorMessage
+ };
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/Interfaces/PaymentGatewayResolverContract.cs b/Application/Features/Payment/Interfaces/PaymentGatewayResolverContract.cs
new file mode 100644
index 0000000..3a9316b
--- /dev/null
+++ b/Application/Features/Payment/Interfaces/PaymentGatewayResolverContract.cs
@@ -0,0 +1,8 @@
+using Domain.Entities;
+
+namespace Application.Features.Payment.Interfaces;
+
+public interface PaymentGatewayResolverContract
+{
+ PaymentGatewayProviderContract Resolve(PaymentGateway gateway);
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/Interfaces/PaymentRepositoryContract.cs b/Application/Features/Payment/Interfaces/PaymentRepositoryContract.cs
new file mode 100644
index 0000000..660596e
--- /dev/null
+++ b/Application/Features/Payment/Interfaces/PaymentRepositoryContract.cs
@@ -0,0 +1,12 @@
+using Application.Features.Payment.DTOs;
+
+namespace Application.Features.Payment.Interfaces;
+
+public interface PaymentRepositoryContract
+{
+ Task CreatePaymentAsync(Domain.Entities.Payment payment);
+ Task SaveAsync();
+ Task GetPaymentByResNumAsync(string resNum);
+ Task GetPaymentByAuthorityAsync(string authority);
+ Task IsExistByRefNum(string refNum);
+}
\ No newline at end of file
diff --git a/Application/Features/Payment/Interfaces/PaymentServiceContract.cs b/Application/Features/Payment/Interfaces/PaymentServiceContract.cs
index 0d17493..776b744 100644
--- a/Application/Features/Payment/Interfaces/PaymentServiceContract.cs
+++ b/Application/Features/Payment/Interfaces/PaymentServiceContract.cs
@@ -1,6 +1,13 @@
+using Application.Features.Payment.DTOs;
+using Application.Features.Payment.DTOs.ZarinPal;
+using Domain.Entities;
+using Microsoft.AspNetCore.Http;
+
namespace Application.Features.Payment.Interfaces;
public interface PaymentServiceContract
{
- Task GenerateResNum();
+ Task CreatePaymentAsync(CreatePaymentDto dto);
+ Task HandleCallBackAsync(PaymentGateway gateway,SandBoxCallBackDto dto);
+ Task VerifyTransaction(string RefNum);
}
\ No newline at end of file
diff --git a/Application/Features/Payment/Services/PaymentService.cs b/Application/Features/Payment/Services/PaymentService.cs
index 74d3034..258a0c2 100644
--- a/Application/Features/Payment/Services/PaymentService.cs
+++ b/Application/Features/Payment/Services/PaymentService.cs
@@ -1,12 +1,93 @@
+using System.Net.Http.Json;
using System.Security.Cryptography;
+using Application.Features.Payment.DTOs;
+using Application.Features.Payment.DTOs.ZarinPal;
using Application.Features.Payment.Interfaces;
+using Domain.Entities;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
namespace Application.Features.Payment.Services;
-public class PaymentService:PaymentServiceContract
+public class PaymentService : PaymentServiceContract
{
- public async Task GenerateResNum()
+ private readonly PaymentRepositoryContract _paymentRepository;
+ private readonly PaymentGatewayResolverContract _gatewayResolver;
+ private readonly IConfiguration _configuration;
+ private readonly HttpClient _httpClient;
+
+ public PaymentService(PaymentRepositoryContract paymentRepository, IConfiguration configuration,
+ HttpClient httpClient, PaymentGatewayResolverContract gatewayResolver)
{
- return $"{Guid.NewGuid():N}";
+ _paymentRepository = paymentRepository;
+ _configuration = configuration;
+ _httpClient = httpClient;
+ _gatewayResolver = gatewayResolver;
}
+
+ // public async Task CreatePaymentAsync(CreatePaymentDto dto)
+ // {
+ // var payment = new Domain.Entities.Payment(dto.TenantId, dto.AppointmentId, dto.Amount, dto.Gateway);
+ //
+ // var provider = _gatewayResolver.Resolve(dto.Gateway);
+ //
+ // var requestResult=await provider.RequestPaymentAsync(payment,dto);
+ //
+ // if (!requestResult.IsSuccess)
+ // {
+ // payment.MarkAsFailed();
+ // await _paymentRepository.SaveAsync();
+ //
+ // throw new InvalidOperationException(requestResult.ErrorMessage);
+ // }
+ // await _paymentRepository.CreatePaymentAsync(payment);
+ // await _paymentRepository.SaveAsync();
+ // return requestResult.PaymentUrl;
+ public async Task CreatePaymentAsync(CreatePaymentDto dto)
+ {
+ var payment = new Domain.Entities.Payment(dto.TenantId, dto.AppointmentId, dto.Amount, dto.Gateway);
+
+ var provider = _gatewayResolver.Resolve(dto.Gateway);
+
+ var requestResult = await provider.RequestPaymentAsync(payment, dto);
+
+ if (!requestResult.IsSuccess)
+ {
+ payment.MarkAsFailed();
+ await _paymentRepository.SaveAsync();
+
+ throw new InvalidOperationException(requestResult.ErrorMessage);
+ }
+ payment.Authority=requestResult.GatewayToken;
+ await _paymentRepository.CreatePaymentAsync(payment);
+ await _paymentRepository.SaveAsync();
+ return requestResult.PaymentUrl;
+ }
+
+ public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto)
+ {
+ var provider = _gatewayResolver.Resolve(gateway);
+ var result= await provider.HandleCallBackAsync(gateway, dto);
+
+ return result;
+ }
+
+ public async Task VerifyTransaction(string RefNum)
+ {
+ var request = new
+ {
+ refNum = RefNum,
+ TerminalNumber = _configuration["Payment:TerminalId"]
+ };
+ var verifyUrl = _configuration["Payment:VerifyTransactionUrl"];
+ var response = await _httpClient.PostAsJsonAsync(verifyUrl, request);
+
+ if (!response.IsSuccessStatusCode)
+ return false;
+
+ var result = await response.Content.ReadAsStringAsync();
+ Console.WriteLine(result);
+ return result is not null;
+ }
+
}
\ No newline at end of file
diff --git a/Application/Features/Service/DTOs/CreateService.cs b/Application/Features/Service/DTOs/CreateServiceDto.cs
similarity index 65%
rename from Application/Features/Service/DTOs/CreateService.cs
rename to Application/Features/Service/DTOs/CreateServiceDto.cs
index f5c2c72..498fbd7 100644
--- a/Application/Features/Service/DTOs/CreateService.cs
+++ b/Application/Features/Service/DTOs/CreateServiceDto.cs
@@ -1,7 +1,8 @@
namespace Application.Features.Service.DTOs;
-public class CreateService
+public class CreateServiceDto
{
public string Title { get; set; }
public int DurationMinutes { get; set; }
+ public Guid TenantId { get; set; }
}
\ No newline at end of file
diff --git a/Application/Features/Service/Services/IServiceAppService.cs b/Application/Features/Service/Services/IServiceAppService.cs
index 915c0bc..87a1ea5 100644
--- a/Application/Features/Service/Services/IServiceAppService.cs
+++ b/Application/Features/Service/Services/IServiceAppService.cs
@@ -4,7 +4,7 @@ namespace Application.Features.Service.Services;
public interface IServiceAppService
{
- Task CreateServiceAsync(CreateService createService);
+ Task CreateServiceAsync(CreateServiceDto createServiceDto);
Task EditServiceAsync(Guid serviceId,EditService editService);
Task DeleteServiceAsync(Guid serviceId);
Task> ViewAllServicesAsync();
diff --git a/Application/Features/Service/Services/ServiceAppService.cs b/Application/Features/Service/Services/ServiceAppService.cs
index 5f9094e..6e850c3 100644
--- a/Application/Features/Service/Services/ServiceAppService.cs
+++ b/Application/Features/Service/Services/ServiceAppService.cs
@@ -21,10 +21,10 @@ private void EnsureSuperAdmin()
if (_userContext.Role != UserRole.SuperAdmin)
throw new ForbiddenAccessException("Only Admin Access");
}
- public async Task CreateServiceAsync(CreateService createService)
+ public async Task CreateServiceAsync(CreateServiceDto createServiceDto)
{
EnsureSuperAdmin();
- var service=new Domain.Entities.Service(createService.Title,createService.DurationMinutes);
+ var service=new Domain.Entities.Service(createServiceDto.Title,createServiceDto.DurationMinutes,createServiceDto.TenantId);
await _repository.CreatServiceAsync(service);
return $"{service.Title} created";
}
diff --git a/Application/Validators/Service/CreateServiceValidator.cs b/Application/Validators/Service/CreateServiceValidator.cs
index 077be06..b842c9b 100644
--- a/Application/Validators/Service/CreateServiceValidator.cs
+++ b/Application/Validators/Service/CreateServiceValidator.cs
@@ -3,7 +3,7 @@
namespace Application.Validators.Service;
-public class CreateServiceValidator:AbstractValidator
+public class CreateServiceValidator:AbstractValidator
{
public CreateServiceValidator()
{
diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj
index d3f8bd4..877932b 100644
--- a/Domain/Domain.csproj
+++ b/Domain/Domain.csproj
@@ -7,4 +7,8 @@
Linux
+
+
+
+
diff --git a/Domain/Entities/Appointment.cs b/Domain/Entities/Appointment.cs
index b1103c7..da15ea9 100644
--- a/Domain/Entities/Appointment.cs
+++ b/Domain/Entities/Appointment.cs
@@ -14,12 +14,12 @@ public class Appointment:TenantEntityContract
public AppointmentStatus Status { get; private set; }
public Guid TenantId { get; set; }
public Tenant Tenant { get; set; }
-
+ public ICollection Payments { get; set; }
public ICollection AppointmentServices { get; private set; } = new List();
private Appointment() { } // For EF
- public Appointment(Guid userId, DateTime startTime, DateTime endTime, string title)
+ public Appointment(Guid userId, DateTime startTime, DateTime endTime, string title,Guid tenantId)
{
if (startTime == default || endTime == default)
throw new ArgumentException("StartTime and EndTime are required");
@@ -31,6 +31,7 @@ public Appointment(Guid userId, DateTime startTime, DateTime endTime, string tit
StartTime = startTime;
EndTime = endTime;
Title = title;
+ TenantId = tenantId;
Status = AppointmentStatus.Reserved;
}
diff --git a/Domain/Entities/Payment.cs b/Domain/Entities/Payment.cs
new file mode 100644
index 0000000..b887ac5
--- /dev/null
+++ b/Domain/Entities/Payment.cs
@@ -0,0 +1,90 @@
+using System.Runtime.CompilerServices;
+using Microsoft.EntityFrameworkCore;
+
+namespace Domain.Entities;
+
+public class Payment
+{
+ public Guid Id { get; set; }
+ public Guid TenantId { get; set; }
+ public Guid AppointmentId { get; set; }
+ public Appointment apppointment { get; set; }
+ public string? State { get; set; }
+ public long Amount { get; set; }
+ public string? Wage { get; set; }
+ public string ResNum { get; set; }
+ public string Description { get; set; }
+ public string? RefNum { get; set; }
+
+ public string? TraceNo { get; set; }
+
+ public string? RRN { get; set; }
+
+ public string? CardNumber { get; set; }
+ public string? Authority { get; set; }
+
+ public PaymentStatus PaymentStatus{ get; set; }
+ public string? PaymentGatewayStatus { get; set; }
+ public PaymentGateway Gateway { get; set; }
+ public string? SecurePan { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+
+ public DateTime? PaidAt { get; set; }
+
+ public Payment(Guid tenantId, Guid appointmentId, long amount, PaymentGateway gateway)
+ {
+ Id = Guid.NewGuid();
+ TenantId = tenantId;
+ AppointmentId = appointmentId;
+ Amount = amount;
+ Gateway=gateway;
+ PaymentStatus = PaymentStatus.pending;
+ CreatedAt = DateTime.UtcNow;
+ }
+
+ public void Edit(string? paymentGatewayStatus, string? state, string? _RRN, string? refNum, string? resNum, string? traceNo, string? wage)
+ {
+ PaymentGatewayStatus= paymentGatewayStatus;
+ State=state;
+ RRN=_RRN;
+ RefNum=refNum;
+ ResNum=resNum;
+ TraceNo=traceNo;
+ Wage=wage;
+ }
+ public void Edit(string? paymentGatewayStatus, int? refNum, string? securePan, int? fee)
+ {
+ PaymentGatewayStatus= paymentGatewayStatus;
+ RefNum= refNum.ToString();
+ SecurePan=securePan;
+ Wage=fee.ToString();
+ }
+ void SetPaidAt()
+ {
+ PaidAt = DateTime.UtcNow;
+ }
+ public void MarkAsFailed()
+ {
+ PaymentStatus = PaymentStatus.Failed;
+ }
+ public void MarkAsSuccess()
+ {
+ PaymentStatus = PaymentStatus.Success;
+ }
+
+ public void GenerateOrderNumber()
+ {
+ string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
+ string shortGuid = Guid.NewGuid().ToString("N").Substring(0, 8);
+ ResNum= $"{timestamp}{shortGuid}";
+ }
+}
+
+public enum PaymentStatus
+{
+ pending,
+ Success,
+ Failed,
+ Expired
+}
\ No newline at end of file
diff --git a/Domain/Entities/PaymentGateway.cs b/Domain/Entities/PaymentGateway.cs
new file mode 100644
index 0000000..d775e57
--- /dev/null
+++ b/Domain/Entities/PaymentGateway.cs
@@ -0,0 +1,7 @@
+namespace Domain.Entities;
+
+public enum PaymentGateway
+{
+ Saman,
+ ZarinPal
+}
\ No newline at end of file
diff --git a/Domain/Entities/Service.cs b/Domain/Entities/Service.cs
index cd458fc..f14d01a 100644
--- a/Domain/Entities/Service.cs
+++ b/Domain/Entities/Service.cs
@@ -13,7 +13,7 @@ public class Service:TenantEntityContract
private Service() { }
- public Service(string title, int durationMinutes)
+ public Service(string title, int durationMinutes, Guid tenantId)
{
if (durationMinutes <= 0)
throw new ArgumentException("Invalid duration");
@@ -21,6 +21,7 @@ public Service(string title, int durationMinutes)
Id = Guid.NewGuid();
Title = title;
DurationMinutes = durationMinutes;
+ TenantId= tenantId;
}
public void Edit(int durationMinutes,string title)
diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj
index b0a2d27..61868b6 100644
--- a/Infrastructure/Infrastructure.csproj
+++ b/Infrastructure/Infrastructure.csproj
@@ -25,4 +25,8 @@
+
+
+
+
diff --git a/Infrastructure/InfrastructureServices.cs b/Infrastructure/InfrastructureServices.cs
index 20c64df..9f9e380 100644
--- a/Infrastructure/InfrastructureServices.cs
+++ b/Infrastructure/InfrastructureServices.cs
@@ -4,6 +4,7 @@
using Application.Features.Appointments.Interfaces;
using Application.Features.AppointmentServiceLink.Interfaces;
using Application.Features.Auth.Interfaces;
+using Application.Features.Payment.Interfaces;
using Application.Features.Service.Interfaces;
using Application.Features.Tenant.Interfaces;
using Infrastructure.Persistence;
@@ -12,6 +13,7 @@
using Infrastructure.Security.Context;
using Infrastructure.Security.Hashing;
using Infrastructure.Security.Jwt;
+using Infrastructure.Services.Implement;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -36,6 +38,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
return services;
}
diff --git a/Infrastructure/Migrations/20260603120331_Payment.Designer.cs b/Infrastructure/Migrations/20260603120331_Payment.Designer.cs
new file mode 100644
index 0000000..477c7d1
--- /dev/null
+++ b/Infrastructure/Migrations/20260603120331_Payment.Designer.cs
@@ -0,0 +1,391 @@
+//
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260603120331_Payment")]
+ partial class Payment
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Property("AppointmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId");
+
+ b.HasIndex("TenantId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Appointments", t =>
+ {
+ t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ServiceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId", "ServiceId");
+
+ b.HasIndex("ServiceId");
+
+ b.ToTable("AppointmentServiceLinks", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Amount")
+ .HasColumnType("bigint");
+
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CardNumber")
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaidAt")
+ .HasColumnType("datetime2");
+
+ b.Property("RRN")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RefNum")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ResNum")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TraceNo")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentId");
+
+ b.HasIndex("ResNum")
+ .IsUnique();
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Payments", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DurationMinutes")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Service", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DateJoined")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpireSubscriptionDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(true)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SubscriptionStatus")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tenants", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PasswordResetAttemptsCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("PasswordResetCodeExpireAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PasswordResetCodeHash")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Users", null, t =>
+ {
+ t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0,1,2,3)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Appointments")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("Appointments")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "Appointment")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.Service", "Service")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("ServiceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Appointment");
+
+ b.Navigation("Service");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "apppointment")
+ .WithMany("Payments")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("apppointment");
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("RefreshTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Services")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Navigation("AppointmentServices");
+
+ b.Navigation("Payments");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Navigation("AppointmentServices");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("Services");
+
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("RefreshTokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260603120331_Payment.cs b/Infrastructure/Migrations/20260603120331_Payment.cs
new file mode 100644
index 0000000..61799a1
--- /dev/null
+++ b/Infrastructure/Migrations/20260603120331_Payment.cs
@@ -0,0 +1,66 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class Payment : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Payments",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false),
+ TenantId = table.Column(type: "uniqueidentifier", nullable: false),
+ AppointmentId = table.Column(type: "uniqueidentifier", nullable: false),
+ Amount = table.Column(type: "bigint", nullable: false),
+ ResNum = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
+ RefNum = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ TraceNo = table.Column(type: "nvarchar(max)", nullable: false),
+ RRN = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
+ CardNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true),
+ Status = table.Column(type: "int", nullable: false),
+ CreatedAt = table.Column(type: "datetime2", nullable: false),
+ PaidAt = table.Column(type: "datetime2", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Payments", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Payments_Appointments_AppointmentId",
+ column: x => x.AppointmentId,
+ principalTable: "Appointments",
+ principalColumn: "AppointmentId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Payments_AppointmentId",
+ table: "Payments",
+ column: "AppointmentId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Payments_ResNum",
+ table: "Payments",
+ column: "ResNum",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Payments_TenantId",
+ table: "Payments",
+ column: "TenantId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Payments");
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260606121723_TraceNoNullable.Designer.cs b/Infrastructure/Migrations/20260606121723_TraceNoNullable.Designer.cs
new file mode 100644
index 0000000..8bc69e3
--- /dev/null
+++ b/Infrastructure/Migrations/20260606121723_TraceNoNullable.Designer.cs
@@ -0,0 +1,390 @@
+//
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260606121723_TraceNoNullable")]
+ partial class TraceNoNullable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Property("AppointmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId");
+
+ b.HasIndex("TenantId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Appointments", t =>
+ {
+ t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ServiceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId", "ServiceId");
+
+ b.HasIndex("ServiceId");
+
+ b.ToTable("AppointmentServiceLinks", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Amount")
+ .HasColumnType("bigint");
+
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CardNumber")
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaidAt")
+ .HasColumnType("datetime2");
+
+ b.Property("RRN")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RefNum")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ResNum")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TraceNo")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentId");
+
+ b.HasIndex("ResNum")
+ .IsUnique();
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Payments", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DurationMinutes")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Service", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DateJoined")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpireSubscriptionDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(true)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SubscriptionStatus")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tenants", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PasswordResetAttemptsCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("PasswordResetCodeExpireAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PasswordResetCodeHash")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Users", null, t =>
+ {
+ t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0,1,2,3)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Appointments")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("Appointments")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "Appointment")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.Service", "Service")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("ServiceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Appointment");
+
+ b.Navigation("Service");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "apppointment")
+ .WithMany("Payments")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("apppointment");
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("RefreshTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Services")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Navigation("AppointmentServices");
+
+ b.Navigation("Payments");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Navigation("AppointmentServices");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("Services");
+
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("RefreshTokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260606121723_TraceNoNullable.cs b/Infrastructure/Migrations/20260606121723_TraceNoNullable.cs
new file mode 100644
index 0000000..f83994a
--- /dev/null
+++ b/Infrastructure/Migrations/20260606121723_TraceNoNullable.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class TraceNoNullable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "TraceNo",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "nvarchar(max)");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "TraceNo",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: false,
+ defaultValue: "",
+ oldClrType: typeof(string),
+ oldType: "nvarchar(max)",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260606222719_FixingPaymentTable.Designer.cs b/Infrastructure/Migrations/20260606222719_FixingPaymentTable.Designer.cs
new file mode 100644
index 0000000..c1c0374
--- /dev/null
+++ b/Infrastructure/Migrations/20260606222719_FixingPaymentTable.Designer.cs
@@ -0,0 +1,398 @@
+//
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260606222719_FixingPaymentTable")]
+ partial class FixingPaymentTable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Property("AppointmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId");
+
+ b.HasIndex("TenantId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Appointments", t =>
+ {
+ t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ServiceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId", "ServiceId");
+
+ b.HasIndex("ServiceId");
+
+ b.ToTable("AppointmentServiceLinks", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Amount")
+ .HasColumnType("bigint");
+
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CardNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaidAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaymentGatewayStatus")
+ .HasColumnType("int");
+
+ b.Property("PaymentStatus")
+ .HasColumnType("int");
+
+ b.Property("RRN")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RefNum")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ResNum")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("State")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TraceNo")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Wage")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentId");
+
+ b.HasIndex("ResNum")
+ .IsUnique();
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Payments", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DurationMinutes")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Service", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DateJoined")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpireSubscriptionDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(true)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SubscriptionStatus")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tenants", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PasswordResetAttemptsCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("PasswordResetCodeExpireAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PasswordResetCodeHash")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Users", null, t =>
+ {
+ t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0,1,2,3)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Appointments")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("Appointments")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "Appointment")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.Service", "Service")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("ServiceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Appointment");
+
+ b.Navigation("Service");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "apppointment")
+ .WithMany("Payments")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("apppointment");
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("RefreshTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Services")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Navigation("AppointmentServices");
+
+ b.Navigation("Payments");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Navigation("AppointmentServices");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("Services");
+
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("RefreshTokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260606222719_FixingPaymentTable.cs b/Infrastructure/Migrations/20260606222719_FixingPaymentTable.cs
new file mode 100644
index 0000000..4799925
--- /dev/null
+++ b/Infrastructure/Migrations/20260606222719_FixingPaymentTable.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class FixingPaymentTable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "Status",
+ table: "Payments",
+ newName: "PaymentStatus");
+
+ migrationBuilder.AlterColumn(
+ name: "CardNumber",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "nvarchar(20)",
+ oldMaxLength: 20,
+ oldNullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "PaymentGatewayStatus",
+ table: "Payments",
+ type: "int",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "State",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Wage",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "PaymentGatewayStatus",
+ table: "Payments");
+
+ migrationBuilder.DropColumn(
+ name: "State",
+ table: "Payments");
+
+ migrationBuilder.DropColumn(
+ name: "Wage",
+ table: "Payments");
+
+ migrationBuilder.RenameColumn(
+ name: "PaymentStatus",
+ table: "Payments",
+ newName: "Status");
+
+ migrationBuilder.AlterColumn(
+ name: "CardNumber",
+ table: "Payments",
+ type: "nvarchar(20)",
+ maxLength: 20,
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "nvarchar(max)",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.Designer.cs b/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.Designer.cs
new file mode 100644
index 0000000..af88294
--- /dev/null
+++ b/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.Designer.cs
@@ -0,0 +1,401 @@
+//
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260617155320_SecurePanAddedToTable")]
+ partial class SecurePanAddedToTable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Property("AppointmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId");
+
+ b.HasIndex("TenantId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Appointments", t =>
+ {
+ t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ServiceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("AppointmentId", "ServiceId");
+
+ b.HasIndex("ServiceId");
+
+ b.ToTable("AppointmentServiceLinks", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Amount")
+ .HasColumnType("bigint");
+
+ b.Property("AppointmentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CardNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaidAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaymentGatewayStatus")
+ .HasColumnType("int");
+
+ b.Property("PaymentStatus")
+ .HasColumnType("int");
+
+ b.Property("RRN")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RefNum")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ResNum")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("SecurePan")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("State")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TraceNo")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Wage")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppointmentId");
+
+ b.HasIndex("ResNum")
+ .IsUnique();
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Payments", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DurationMinutes")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Service", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DateJoined")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpireSubscriptionDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(true)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SubscriptionStatus")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tenants", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PasswordResetAttemptsCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("PasswordResetCodeExpireAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PasswordResetCodeHash")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Users", null, t =>
+ {
+ t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0,1,2,3)");
+ });
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Appointments")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("Appointments")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "Appointment")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Domain.Entities.Service", "Service")
+ .WithMany("AppointmentServices")
+ .HasForeignKey("ServiceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Appointment");
+
+ b.Navigation("Service");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Payment", b =>
+ {
+ b.HasOne("Domain.Entities.Appointment", "apppointment")
+ .WithMany("Payments")
+ .HasForeignKey("AppointmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("apppointment");
+ });
+
+ modelBuilder.Entity("Domain.Entities.RefreshToken", b =>
+ {
+ b.HasOne("Domain.Entities.User", "User")
+ .WithMany("RefreshTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Services")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.HasOne("Domain.Entities.Tenant", "Tenant")
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Tenant");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Navigation("AppointmentServices");
+
+ b.Navigation("Payments");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Service", b =>
+ {
+ b.Navigation("AppointmentServices");
+ });
+
+ modelBuilder.Entity("Domain.Entities.Tenant", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("Services");
+
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("Domain.Entities.User", b =>
+ {
+ b.Navigation("Appointments");
+
+ b.Navigation("RefreshTokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.cs b/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.cs
new file mode 100644
index 0000000..111c0ea
--- /dev/null
+++ b/Infrastructure/Migrations/20260617155320_SecurePanAddedToTable.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class SecurePanAddedToTable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "SecurePan",
+ table: "Payments",
+ type: "nvarchar(max)",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "SecurePan",
+ table: "Payments");
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20260627153204_GatewayAddedToPayment.Designer.cs b/Infrastructure/Migrations/20260627153204_GatewayAddedToPayment.Designer.cs
new file mode 100644
index 0000000..719ce2f
--- /dev/null
+++ b/Infrastructure/Migrations/20260627153204_GatewayAddedToPayment.Designer.cs
@@ -0,0 +1,404 @@
+//
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260627153204_GatewayAddedToPayment")]
+ partial class GatewayAddedToPayment
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Domain.Entities.Appointment", b =>
+ {
+ b.Property("AppointmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property