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("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("Gateway") + .HasColumnType("int"); + + 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/20260627153204_GatewayAddedToPayment.cs b/Infrastructure/Migrations/20260627153204_GatewayAddedToPayment.cs new file mode 100644 index 0000000..7932fe8 --- /dev/null +++ b/Infrastructure/Migrations/20260627153204_GatewayAddedToPayment.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class GatewayAddedToPayment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Gateway", + table: "Payments", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Gateway", + table: "Payments"); + } + } +} diff --git a/Infrastructure/Migrations/20260628154520_AuthorityAddedToPayments.Designer.cs b/Infrastructure/Migrations/20260628154520_AuthorityAddedToPayments.Designer.cs new file mode 100644 index 0000000..a6a4eec --- /dev/null +++ b/Infrastructure/Migrations/20260628154520_AuthorityAddedToPayments.Designer.cs @@ -0,0 +1,407 @@ +// +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("20260628154520_AuthorityAddedToPayments")] + partial class AuthorityAddedToPayments + { + /// + 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("Authority") + .HasColumnType("nvarchar(max)"); + + b.Property("CardNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Gateway") + .HasColumnType("int"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("PaymentGatewayStatus") + .HasColumnType("nvarchar(max)"); + + 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/20260628154520_AuthorityAddedToPayments.cs b/Infrastructure/Migrations/20260628154520_AuthorityAddedToPayments.cs new file mode 100644 index 0000000..55682c0 --- /dev/null +++ b/Infrastructure/Migrations/20260628154520_AuthorityAddedToPayments.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class AuthorityAddedToPayments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "PaymentGatewayStatus", + table: "Payments", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Authority", + table: "Payments", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Authority", + table: "Payments"); + + migrationBuilder.AlterColumn( + name: "PaymentGatewayStatus", + table: "Payments", + type: "int", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/Infrastructure/Migrations/20260628195138_DescriptionAddedToPayments.Designer.cs b/Infrastructure/Migrations/20260628195138_DescriptionAddedToPayments.Designer.cs new file mode 100644 index 0000000..8a92d88 --- /dev/null +++ b/Infrastructure/Migrations/20260628195138_DescriptionAddedToPayments.Designer.cs @@ -0,0 +1,411 @@ +// +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("20260628195138_DescriptionAddedToPayments")] + partial class DescriptionAddedToPayments + { + /// + 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("Authority") + .HasColumnType("nvarchar(max)"); + + b.Property("CardNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gateway") + .HasColumnType("int"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("PaymentGatewayStatus") + .HasColumnType("nvarchar(max)"); + + 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/20260628195138_DescriptionAddedToPayments.cs b/Infrastructure/Migrations/20260628195138_DescriptionAddedToPayments.cs new file mode 100644 index 0000000..8fb49c0 --- /dev/null +++ b/Infrastructure/Migrations/20260628195138_DescriptionAddedToPayments.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class DescriptionAddedToPayments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + table: "Payments", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + table: "Payments"); + } + } +} diff --git a/Infrastructure/Persistence/AppDbContext.cs b/Infrastructure/Persistence/AppDbContext.cs index 5d72d3b..60b5964 100644 --- a/Infrastructure/Persistence/AppDbContext.cs +++ b/Infrastructure/Persistence/AppDbContext.cs @@ -11,6 +11,7 @@ public class AppDbContext:DbContext public DbSet Services =>Set(); public DbSet Users =>Set(); public DbSet RefreshTokens =>Set(); + public DbSet Payments { get; set; } public DbSet AppointmentServiceLinks =>Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/Infrastructure/Persistence/Configurations/PaymentConfiguration.cs b/Infrastructure/Persistence/Configurations/PaymentConfiguration.cs new file mode 100644 index 0000000..af90e56 --- /dev/null +++ b/Infrastructure/Persistence/Configurations/PaymentConfiguration.cs @@ -0,0 +1,79 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class PaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Payments"); + + builder.HasKey(p => p.Id); + + builder.Property(p => p.TenantId).IsRequired(); + + builder.Property(p => p.AppointmentId).IsRequired(); + + builder + .Property(p => p.Amount) + .IsRequired(); + + builder + .Property(p => p.Description) + .IsRequired(); + + builder.Property(p=>p.ResNum) + .HasMaxLength(100) + .IsRequired(); + + builder.Property(p => p.RefNum) + .HasMaxLength(100); + + builder + .Property(p => p.TraceNo); + + builder + .Property(p=>p.RRN) + .HasMaxLength(100); + + builder + .Property(p=>p.PaymentStatus) + .IsRequired(); + + builder + .Property(p => p.SecurePan); + + builder + .Property(p=>p.PaymentGatewayStatus); + + builder + .Property(p => p.CreatedAt) + .IsRequired(); + + builder + .Property(p=>p.PaidAt) + .IsRequired(false); + + builder.HasOne(p => p.apppointment) + .WithMany(p => p.Payments) + .HasForeignKey(p => p.AppointmentId); + + builder + .HasIndex(p=>p.ResNum) + .IsUnique(); + + builder + .HasIndex(p => p.AppointmentId); + + builder + .HasIndex(p => p.TenantId); + + builder + .Property(p => p.Gateway); + + builder + .Property(p => p.Authority); + } +} \ No newline at end of file diff --git a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index d8f7931..81bd5c9 100644 --- a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -75,6 +75,83 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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("Authority") + .HasColumnType("nvarchar(max)"); + + b.Property("CardNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gateway") + .HasColumnType("int"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("PaymentGatewayStatus") + .HasColumnType("nvarchar(max)"); + + 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") @@ -255,6 +332,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -290,6 +378,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Appointment", b => { b.Navigation("AppointmentServices"); + + b.Navigation("Payments"); }); modelBuilder.Entity("Domain.Entities.Service", b => diff --git a/Infrastructure/Persistence/Repositories/PaymentRepository.cs b/Infrastructure/Persistence/Repositories/PaymentRepository.cs new file mode 100644 index 0000000..48ba08a --- /dev/null +++ b/Infrastructure/Persistence/Repositories/PaymentRepository.cs @@ -0,0 +1,48 @@ +using Application.Features.Payment.Interfaces; +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Persistence.Repositories; + +public class PaymentRepository:PaymentRepositoryContract +{ + private readonly AppDbContext _dbContext; + + public PaymentRepository(AppDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task CreatePaymentAsync(Payment payment) + { + await _dbContext + .Payments + .AddAsync(payment); + } + + public async Task SaveAsync() + { + await _dbContext.SaveChangesAsync(); + } + + public async Task GetPaymentByResNumAsync(string resNum) + { + return await _dbContext + .Payments + .Where(p => p.ResNum == resNum) + .FirstOrDefaultAsync(); + } + + public async Task GetPaymentByAuthorityAsync(string authority) + { + return await _dbContext + .Payments + .Where(p => p.Authority == authority) + .FirstOrDefaultAsync(); + } + + public Task IsExistByRefNum(string refNum) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Infrastructure/Services/Implement/PaymentGatewayResolver.cs b/Infrastructure/Services/Implement/PaymentGatewayResolver.cs new file mode 100644 index 0000000..bec1cc6 --- /dev/null +++ b/Infrastructure/Services/Implement/PaymentGatewayResolver.cs @@ -0,0 +1,24 @@ +using Application.Features.Payment.Interfaces; +using Domain.Entities; + +namespace Infrastructure.Services.Implement; + +public class PaymentGatewayResolver:PaymentGatewayResolverContract +{ + private readonly IEnumerable _providers; + + public PaymentGatewayResolver(IEnumerable providers) + { + _providers = providers; + } + + public PaymentGatewayProviderContract Resolve(PaymentGateway gateway) + { + var provider = _providers.FirstOrDefault(x => x.Gateway == gateway); + + if (provider is null) + throw new InvalidOperationException($"no provider found for gateway : {gateway}"); + + return provider; + } +} \ No newline at end of file diff --git a/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs b/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs new file mode 100644 index 0000000..491f565 --- /dev/null +++ b/Infrastructure/Services/Implement/SamanPaymentGatewayProvider.cs @@ -0,0 +1,93 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Application.Features.Payment.DTOs; +using Application.Features.Payment.DTOs.Saman; +using Application.Features.Payment.Interfaces; +using Domain.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +namespace Infrastructure.Services.Implement; + +public class SamanPaymentGatewayProvider : PaymentGatewayProviderContract +{ + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly PaymentRepositoryContract _paymentRepositoryContract; + + public SamanPaymentGatewayProvider(HttpClient httpClient, IConfiguration configuration, PaymentRepositoryContract paymentRepositoryContract) + { + _httpClient = httpClient; + _configuration = configuration; + _paymentRepositoryContract = paymentRepositoryContract; + } + + public PaymentGateway Gateway => PaymentGateway.Saman; + + public async Task RequestPaymentAsync(Payment payment, CreatePaymentDto dto) + { + var paymentPageUrl = "https://sandbox.banktest.ir/saman/sep.shaparak.ir/OnlinePG/SendToken"; + payment.GenerateOrderNumber(); + var dtoRequest = new SamanTokenRequestDto() + { + Action = "token", + Amount = dto.Amount, + ResNum = payment.ResNum, + }; + var request = new + { + dtoRequest.Action, + TerminalId = _configuration["Payment:TerminalId"]!, + dtoRequest.Amount, + dtoRequest.ResNum, + RedirectUrl = _configuration["Payment:SamanRedirectUrl"], + dtoRequest.CellNumber + }; + using var response = await _httpClient.PostAsJsonAsync( + "https://sandbox.banktest.ir/saman/sep.shaparak.ir/OnlinePG/OnlinePG", + request); + + var raw = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + return PaymentGatewayRequestResult.Failed( + ((int)response.StatusCode).ToString(), + "خطا در ارتباط با درگاه سامان"); + } + + var result = JsonSerializer.Deserialize(raw); + + if (result is null || string.IsNullOrWhiteSpace(result.Token)) + { + return PaymentGatewayRequestResult.Failed( + "INVALID_RESPONSE", + "پاسخ نامعتبر از درگاه سامان"); + } + + var paymentUrl = $"{paymentPageUrl}?token={result.Token}"; + + return PaymentGatewayRequestResult.Success(result.Token, paymentUrl); + } + + public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) + { + var payment=await _paymentRepositoryContract.GetPaymentByAuthorityAsync(dto.Authority); + var request = new + { + RefNum = payment.RefNum, + TerminalNumber = _configuration["Payment:TerminalId"] + }; + var verifyUrl = _configuration["Payment:VerifyTransactionUrl"]; + + var response = await _httpClient.PostAsJsonAsync(verifyUrl, request); + + var result=await response.Content.ReadFromJsonAsync(); + + if (!response.IsSuccessStatusCode) + return "پرداخت ناموفق"; + + return result.ResultDescription; + } + +} \ No newline at end of file diff --git a/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs b/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs new file mode 100644 index 0000000..35ff3dc --- /dev/null +++ b/Infrastructure/Services/Implement/ZarinPalPaymentGatewayProvider.cs @@ -0,0 +1,99 @@ +using System.Net.Http.Json; +using System.Text.Json; +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 Infrastructure.Services.Implement; + +public class ZarinPalPaymentGatewayProvider : PaymentGatewayProviderContract +{ + private readonly IConfiguration _config; + private readonly HttpClient _httpClient; + private readonly PaymentRepositoryContract _repositoryContract; + + public ZarinPalPaymentGatewayProvider(IConfiguration config, HttpClient httpClient, PaymentRepositoryContract repositoryContract) + { + _config = config; + _httpClient = httpClient; + _repositoryContract = repositoryContract; + } + + public PaymentGateway Gateway => PaymentGateway.ZarinPal; + + public async Task RequestPaymentAsync(Payment payment, CreatePaymentDto dto) + { + payment.GenerateOrderNumber(); + var requestDto = new CreateZarinPalPaymentDto + { + Amount = dto.Amount, + Description = dto.Description, + ReferrerId = dto.ReferrerId, + ZarinPalMataData = new ZarinPalMataData + { + Mobile = dto.Mobile, + Email = dto.Email, + OrderId = payment.ResNum + } + }; + var request = new + { + merchant_id = _config["Payment:MerchantIdZarinPal"], + amount = requestDto.Amount, + description = requestDto.Description, + callback_url = _config["Payment:ZarinPalCallBackUrl"], + matadata = requestDto.ZarinPalMataData + }; + + using var response = + await _httpClient.PostAsJsonAsync("https://sandbox.zarinpal.com/pg/v4/payment/request.json", request); + + var raw = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + return PaymentGatewayRequestResult.Failed( + ((int)response.StatusCode).ToString(), + "خطا در ارتباط با درگاه زرین پال"); + } + + var result = JsonSerializer.Deserialize(raw); + if (result is null || string.IsNullOrWhiteSpace(result.Data.Authority)) + { + return PaymentGatewayRequestResult.Failed( + "INVALID_RESPONSE", + "پاسخ نامعتبر از درگاه زرین پال"); + } + + var paymentUrl = "https://sandbox.zarinpal.com/pg/StartPay/" + result?.Data.Authority; + return PaymentGatewayRequestResult.Success(result.Data.Authority, paymentUrl); + } + + public async Task HandleCallBackAsync(PaymentGateway gateway, SandBoxCallBackDto dto) + { + if (dto.Status != "OK") + { + return "پرداخت ناموفق"; + } + var payment=await _repositoryContract.GetPaymentByAuthorityAsync(dto.Authority); + var verifyRequest = new + { + merchant_id = _config["Payment:MerchantIdZarinPal"], + amount = payment.Amount, + authority = dto.Authority + }; + var verifyResponse = + await _httpClient.PostAsJsonAsync("https://sandbox.zarinpal.com/pg/v4/payment/verify.json", verifyRequest); + + var result = await verifyResponse.Content.ReadFromJsonAsync(); + + if (result.Data.Code != 100 && result.Data.Code != 101) + return "پرداخت ناموفق ."; + payment.Edit(dto.Status, result.Data.RefId, result.Data.CardPan, result.Data.Fee); + await _repositoryContract.SaveAsync(); + return "پرداخت با موفقیت انجام شد شماره پیگیری " + result.Data.RefId; + } +} \ No newline at end of file diff --git a/api/Controllers/PaymentController.cs b/api/Controllers/PaymentController.cs index 497fdc9..ea04e9c 100644 --- a/api/Controllers/PaymentController.cs +++ b/api/Controllers/PaymentController.cs @@ -1,7 +1,10 @@ using System.Text; using System.Text.Json; using Application.Features.Payment.DTOs; +using Application.Features.Payment.DTOs.Saman; +using Application.Features.Payment.DTOs.ZarinPal; using Application.Features.Payment.Interfaces; +using Domain.Entities; using Microsoft.AspNetCore.Mvc; namespace api.Controllers; @@ -14,44 +17,60 @@ public class PaymentController : ControllerBase private readonly IConfiguration _config; private readonly PaymentServiceContract _paymentServiceContract; - public PaymentController(HttpClient httpClient, IConfiguration config, PaymentServiceContract paymentServiceContract) + public PaymentController(HttpClient httpClient, IConfiguration config, + PaymentServiceContract paymentServiceContract) { _httpClient = httpClient; _config = config; _paymentServiceContract = paymentServiceContract; } - [HttpPost] - public async Task RequestToken([FromBody]CreatePaymentDto dto) + public async Task GetPaymentUrl([FromBody] CreatePaymentDto dto) + { + var paymentUrl = await _paymentServiceContract.CreatePaymentAsync(dto); + return Ok(paymentUrl); + } + private Dictionary GetAllValues() { - var resNum = await _paymentServiceContract.GenerateResNum(); - var requestBody = new + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var q in Request.Query) + dict[q.Key] = q.Value.FirstOrDefault(); + + if (Request.HasFormContentType) { - action="token", - TerminalId=_config["Payment:TerminalId"], - Amount=dto.Amount, - ResNum=resNum, - RedirectUrl=_config["Payment:RedirectUrl"], - CellNumber=dto.PhoneNumber - }; - - var content=new StringContent( - JsonSerializer.Serialize(requestBody), - Encoding.UTF8, - "application/json"); - - var response=await _httpClient.PostAsync("https://sandbox.banktest.ir/saman/sep.shaparak.ir/OnlinePG/OnlinePG",content); - - var result = await response.Content.ReadFromJsonAsync(); - - if (result is null || result.status != 1) - return BadRequest(result); - - return Ok(new CreatePaymentResponse + foreach (var f in Request.Form) + dict[f.Key] = f.Value.FirstOrDefault(); + } + + return dict; + } + [HttpPost("{gateway}")] + [HttpGet("{gateway}")] + public async Task CallBack([FromRoute] PaymentGateway gateway) + { + var values = GetAllValues(); + + values.TryGetValue("Amount", out var amountStr); + + var dto = new SandBoxCallBackDto { - PayUrl = $"https://sandbox.banktest.ir/saman/sep.shaparak.ir/OnlinePG/SendToken?token={result.token}" - }); + Authority = values.GetValueOrDefault("Authority"), + Status = values.GetValueOrDefault("Status"), + State = values.GetValueOrDefault("State"), + Amount = long.TryParse(amountStr, out var amount) ? amount : null, + RRN = values.GetValueOrDefault("RRN"), + RefNum = values.GetValueOrDefault("RefNum"), + ResNum = values.GetValueOrDefault("ResNum"), + TraceNo = values.GetValueOrDefault("TraceNo"), + Wage = values.GetValueOrDefault("Wage"), + SecurePan = values.GetValueOrDefault("SecurePan"), + CID = values.GetValueOrDefault("CID"), + Token = values.GetValueOrDefault("Token") + }; + + var result = await _paymentServiceContract.HandleCallBackAsync(gateway, dto); + + return Ok(result); } - [HttpPost] - public } \ No newline at end of file diff --git a/api/Controllers/ServiceController.cs b/api/Controllers/ServiceController.cs index d77250d..fa47401 100644 --- a/api/Controllers/ServiceController.cs +++ b/api/Controllers/ServiceController.cs @@ -17,9 +17,9 @@ public ServiceController(IServiceAppService serviceAppService) [HttpPost] [Authorize(Roles = "SuperAdmin")] - public async Task Create([FromBody] CreateService service) + public async Task Create([FromBody]CreateServiceDto serviceDto) { - var res=await _serviceAppService.CreateServiceAsync(service); + var res=await _serviceAppService.CreateServiceAsync(serviceDto); return Ok(res); } diff --git a/api/Program.cs b/api/Program.cs index e13f1fe..6d33533 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,3 +1,4 @@ +using System.Reflection.Metadata; using System.Security.Claims; using System.Text; using api.Middlewares;