Blazor 5x - قسمت 30 - برنامهی Blazor WASM - افزودن پرداخت آنلاین توسط درگاه مجازی پرباد
نویسنده: وحید نصیری
تاریخ: ۱۴۰۰/۰۱/۰۸ ۱۶:۳۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Parbad.AspNetCore" Version="1.1.0" />
<PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
</ItemGroup>
</Project> <Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
</ItemGroup>
</Project> namespace BlazorWasm.WebApi
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
var connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddParbad()
.ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore())
.ConfigureGateways(gatewayBuilder =>
{
gatewayBuilder
.AddParbadVirtual()
.WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway");
})
.ConfigureStorage(storageBuilder =>
{
storageBuilder.UseEfCore(efCoreOptions =>
{
var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name;
efCoreOptions.ConfigureDbContext = db =>
db.UseSqlServer(
connectionString,
sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName)
);
});
})
.ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1)
.ConfigureOptions(parbadOptions =>
{
// parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE";
});
// ...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
if (env.IsDevelopment())
{
app.UseParbadVirtualGatewayWhenDeveloping();
}
else
{
app.UseParbadVirtualGateway();
}
}
}
} using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlazorServer.Entities
{
public class RoomOrderDetail
{
// ...
[Required]
public long ParbadTrackingNumber { get; set; }
public bool IsPaymentSuccessful { get; set; }
public string Status { get; set; }
}
} dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
namespace BlazorWasm.WebApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class ParbadPaymentController : Controller
{
private readonly IConfiguration _configuration;
private readonly IOnlinePayment _onlinePayment;
private readonly IRoomOrderDetailsService _roomOrderService;
public ParbadPaymentController(
IConfiguration configuration,
IOnlinePayment onlinePayment,
IRoomOrderDetailsService roomOrderService)
{
_configuration = configuration;
_onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment));
_roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
}
[HttpGet]
public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
{
var verifyUrl = Url.Action(
action: nameof(ParbadPaymentController.VerifyRoomOrderPayment),
controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty),
values: null, protocol: Request.Scheme);
var result = await _onlinePayment.RequestAsync(invoiceBuilder =>
invoiceBuilder.UseAutoIncrementTrackingNumber()
.SetAmount(amount)
.SetCallbackUrl(verifyUrl)
.UseParbadVirtual()
);
if (result.IsSucceed)
{
await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber);
// It will redirect the client to the gateway.
return result.GatewayTransporter.TransportToGateway();
}
else
{
return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message));
}
}
[HttpGet, HttpPost]
public async Task<IActionResult> VerifyRoomOrderPayment()
{
var invoice = await _onlinePayment.FetchAsync();
var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber);
if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed)
{
return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed."));
}
var verifyResult = await _onlinePayment.VerifyAsync(invoice);
if (verifyResult.Status == PaymentVerifyResultStatus.Succeed)
{
var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount);
if (result == null)
{
return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful"));
}
return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
}
return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
}
private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
{
var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
return new Uri(new Uri(clientBaseUrl),
$"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
}
}
} [HttpGet] public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
namespace BlazorServer.Services
{
public class RoomOrderDetailsService : IRoomOrderDetailsService
{
// ...
public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber)
{
var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId);
if (order == null)
{
return;
}
order.ParbadTrackingNumber = trackingNumber;
_dbContext.RoomOrderDetails.Update(order);
await _dbContext.SaveChangesAsync();
}
}
} [HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment()
namespace BlazorServer.Services
{
public class RoomOrderDetailsService : IRoomOrderDetailsService
{
// ...
public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
{
var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
.Include(u => u.HotelRoom)
.ThenInclude(x => x.HotelRoomImages)
.ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
.FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);
roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
return roomOrderDetailsDTO;
}
}
} namespace BlazorServer.Services
{
public class RoomOrderDetailsService : IRoomOrderDetailsService
{
// ...
public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount)
{
var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber);
if (order?.IsPaymentSuccessful != false || order.TotalCost != amount)
{
return null;
}
order.IsPaymentSuccessful = true;
order.Status = BookingStatus.Booked;
var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order);
await _dbContext.SaveChangesAsync();
return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity);
}
}
} private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
{
var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
return new Uri(new Uri(clientBaseUrl),
$"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
} {
"Client_URL": "https://localhost:5002/"
} @page "/hotel-room-details/{Id:int}"
@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService
@inject IClientRoomOrderDetailsService RoomOrderDetailsService
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient
// ...
@code {
// ...
private async Task HandleCheckout()
{
if (!await HandleValidation())
{
return;
}
try
{
HotelBooking.OrderDetails.ParbadTrackingNumber = -1;
HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id;
HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount;
var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails);
await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved);
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
.AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
.AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
.Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
}
catch (Exception e)
{
await JsRuntime.ToastrError(e.Message);
}
}
// ...
} namespace BlazorWasm.WebApi.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")]
public class RoomOrderController : Controller
{
private readonly IRoomOrderDetailsService _roomOrderService;
public RoomOrderController(IRoomOrderDetailsService roomOrderService)
{
_roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details)
{
var result = await _roomOrderService.CreateAsync(details);
return Ok(result);
}
[HttpGet]
public async Task<IActionResult> GetOrderDetail(int trackingNumber)
{
var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber);
return Ok(result);
}
}
} namespace BlazorServer.Services
{
public class RoomOrderDetailsService : IRoomOrderDetailsService
{
// ...
public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
{
var roomOrder = _mapper.Map<RoomOrderDetail>(details);
roomOrder.Status = BookingStatus.Pending;
var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
await _dbContext.SaveChangesAsync();
return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
}
public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
{
var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
.Include(u => u.HotelRoom)
.ThenInclude(x => x.HotelRoomImages)
.ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
.FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);
roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
return roomOrderDetailsDTO;
}
// ...
}
} namespace BlazorWasm.Client.Services
{
public interface IClientRoomOrderDetailsService
{
Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber);
}
}
namespace BlazorWasm.Client.Services
{
public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
{
private readonly HttpClient _httpClient;
public ClientRoomOrderDetailsService(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber)
{
// How to url-encode query-string parameters properly
var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail"))
.AddParameter("trackingNumber", trackingNumber.ToString())
.Uri;
return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri);
}
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
{
details.UserId = "unknown user!";
var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent);
}
else
{
//var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent);
throw new InvalidOperationException(responseContent);
}
}
}
} namespace BlazorWasm.Client
{
public class Program
{
public static async Task Main(string[] args)
{
// ...
builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>(); var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
.AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
.AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
.Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true); @page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
@inject ILocalStorageService LocalStorage
@inject IClientRoomOrderDetailsService RoomOrderDetailService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@if (IsLoading)
{
<div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
<img src="images/ajax-loader.gif" />
</div>
}
else
{
<div class="container">
<div class="row mt-4 pt-4">
<div class="col-10 offset-1 text-center">
@if(IsPaymentSuccessful)
{
<h2 class="text-success">Booking Confirmed!</h2>
<p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p>
}
else
{
<h2 class="text-warning">Booking Failed!</h2>
<p>@Message</p>
}
<a class="btn btn-primary" href="hotel-rooms">Back to rooms</a>
</div>
</div>
</div>
}
@code
{
private bool IsLoading;
private bool IsPaymentSuccessful;
[Parameter] public int OrderId { set; get; }
[Parameter] public long TrackingNumber { set; get; }
[Parameter] public string Message { set; get; }
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber);
var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails);
if(finalOrderDetail is not null &&
finalOrderDetail.IsPaymentSuccessful &&
finalOrderDetail.Status == BookingStatus.Booked &&
localOrderDetail is not null &&
localOrderDetail.TotalCost == finalOrderDetail.TotalCost)
{
IsPaymentSuccessful = true;
await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails);
await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking);
}
else
{
IsPaymentSuccessful = false;
}
}
catch(Exception ex)
{
await JsRuntime.ToastrError(ex.Message);
}
finally
{
IsLoading = false;
}
}
} @page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
public class HotelRoomDTO
{
public bool IsBooked { get; set; }
// ...
} namespace BlazorServer.Services
{
public class HotelRoomService : IHotelRoomService
{
// ...
public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr)
{
var hotelRooms = await _dbContext.HotelRooms
.Include(x => x.HotelRoomImages)
.Include(x => x.RoomOrderDetails)
.ProjectTo<HotelRoomDTO>(_mapperConfiguration)
.ToListAsync();
foreach (var hotelRoom in hotelRooms)
{
hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr);
}
return hotelRooms;
}
public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate)
{
var hotelRoom = await _dbContext.HotelRooms
.Include(x => x.HotelRoomImages)
.Include(x => x.RoomOrderDetails)
.ProjectTo<HotelRoomDTO>(_mapperConfiguration)
.FirstOrDefaultAsync(x => x.Id == roomId);
hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate);
return hotelRoom;
}
private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate)
{
if (checkInDate == null || checkOutDate == null)
{
return false;
}
return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful &&
//check if checkin date that user wants does not fall in between any dates for room that is booked
((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate)
//check if checkout date that user wants does not fall in between any dates for room that is booked
|| (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date))
);
}
}
} @if (room.IsBooked)
{
<button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
<a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
} @if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked)
{
<button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
<button type="submit" class="btn btn-success form-control">Checkout Now</button>
}