نبدأ نعمل الـ Payment Module فأنا عايز أعمل Service هعمل فيها الـ Functionality بتاع الـ Payment Module
Contract
فأنا هروح أعمل الـ Service Contract الأول في الـ API Project جوا الـ Services.Contract
هيبقا فيها Method واحدة بس اللي هنعمل بيها الـ Intent
هترجع Basket مع حاجتين: Payment Intent Id, Client Secret
ليه Update: لما باجي أعمل payment Intent مع Options or Configs ومن ضمنها الـ Amount اللي هو هيدفع كام
فلو هو زود حاجة او نقص حاجة في الباسكت فأنا لازم اعملها Update
انا محتاج أخد الـ CustomerBasket
وأعدل جواها
بس لازم الاتنين يبقوا Nullable عشان وانا بعمل Basket أصلا ميطلبوش وميطلعليش Error
public interface IPaymentService
{
Task<CustomerBasket?> CreateOrUpdatePaymentIntent(string basketId);
}
// CustomerBasket
public string? PaymentIntentId {get; set;}
public string? ClientSecret {get; set;}
SCA:
- اليوزر هيكلم الـ API عشان يعمل Customer Basket فهتعملها بالـ Products وتحفظها في الـ Redis
- ترد انها Created للـ User
- الـ User بيكلم الـ API تاني عشان يعمل Payment Intent نية دفع
- كـ API هكلم الـ Stripe (محتاج أعمل الفانكشناليتي Payment Intend)
- الـ Stripe هيعمل Create Payment Intent وكمان هيرجع Client Secret
- الـ API هياخدهم ويحطهم في الـ Customer Basket وأرجعه للـ End user
- نعمل Create للـ Order من خلال الـ API هنعمل (Refactor)
- المفرض الـ User يدفع من خلال الـ Front end ومينفعش من خلال البوستمان وبيكون من خلال المعلومتين اللي معانا Client Secret و Payment Intent Id
Service
نروح بقا في بروجكت الـ Services ونعمل New Service
محتاجين نعمل Install لباكدج الـ Stripe في بروجكت الـ Services واسمها Stripe.net
using Product = Shary.Core.Entities.Product;
public class PaymentService : IPaymentService
{
// Inject IConfiguration, BasketRepo
private readonly IConfiguration _configuration;
private readonly IConfiguration _basketRepo;
private readonly IUnitOfWork _unitOfWork;
public PaymentService(
IConfiguration configuration,
IBasketRepository basketRepo,
IUnitOfWork _unitOfWork)
{
_configuration = configuration;
_basketRepo = basketRepo;
_unitOfWork = unitOfWork;
}
public Task<CustomerBasket?> CreateOrUpdatePaymentIntent(string basketId)
{
// Set Secret key
StripeConfiguration.ApiKey = _configuration["StripeSettings:SecretKey"];
// Get amount from basket
var basket = await _basketRepo.GetBasketAsync(baketId);
if(basket is null) return null // No payment intent
// 2. Shipping price
var shippingPrice = 0m;
if(basket.DeliveryMethodId.HasValue)
{
var deliveryMethod = await _unitOfWork.Repository<DeliveryMethod>().GetByIdAsync(basket.DeliveryMethodId.Value);
basket.ShippingPrice = deliveryMethod.Cost;
shippingPrice = deliveryMethod.Cost;
}
// 1.Check if the price, We need productRepo
if(basket?.Items.Count()>0)
{
foreach(var item in basket.Items)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(item.Id);
if(item.Price != product.Price)
item.Price = product.Price;
}
}
// Create payment intent (Create or Update)
PaymentIntentService paymentIntentService = new();
PaymentIntent paymentIntent;
if(string.IsNullOrEmpty(basket.PaymentIntentId)) // Create new
{
var createOptions = new PaymentIntentCreateOptions()
{ // Object Initializer
Amount = (long) basket.Item.Sum(item => item.Price * 100 * item.qountity) + (long) shippingPrice * 100,
// Dollar to cent because amount is long
Currency = "usd", // العملة
PaymentMethodTypes = new List<string> () {"card"}
};
paymentIntent = await paymentIntentService.CreateAsync(createOptions);
// Save paymentintent + Client Sercret in basket
basket.PaymentIntentId = paymentIntent.Id;
basket.ClientSecret = paymentIntent.ClientSecret;
}
else // Update existing payment intent
{
var updateOptions = new PaymentIntentUpdateOptions()
{
Amount = (long) basket.Item.Sum(item => item.Price * 100 * item.Countity) + (long) shippingPrice * 100
};
await paymentIntentService.UpdateAsync(basket.PaymentIntentId, updateOptions);
// No need to save payment intent and client secret, it generated already
}
// Update Basket
await _basketRepo.UpdateBasketAsync(basket);
return basket;
}
}
// Customer Basket
public int? DeliveryMethodId {get; set;}
public decimal ShippingPrice {get; set;}
طبعا هيقولي ايه الـ Product دا لأن Stripe فيها Product برضو فهو محتار
فاحنا المفروض نعمل Alias name من خلال ctrl + .
بالنسبة للـ Shipping price هنزود في الـ Customer Basket حاجتين جداد
Endpoint
هنحتاج نستخدمها من خلال Endpoint
لأن الـ User بيكمل الـ API عشان يعمل Intent
new controller: PaymentsController
[Authorize] // Any endpoint here should be authorized
public class PaymentsController : BaseApiController
{
private readonly IPaymentService _paymentService;
public PaymentsController(IPaymentService paymentService)
{
_paymentService = paymentService;
}
[ProducesResponseType(typeof(CustomerBasket), StatusCodes.Status200Ok)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
[HttpPost("{basketId}")] // POST : /api/Payments/basketId
public async Task<ActionResult<CustomerBasket>> CreateOrUpdatePaymentIntent(string basketId)
{
var basket = await _paymentService.CreateOrUpdatePaymentIntent(basketId);
if(basket is null) return BadRequest(new ApiResponse(400, "An Error with your basket"));
return Ok(basket);
}
}
// Allow DI
// Api -> Program -> ApplicationServicesExtension
services.AddScoped(typeof(IPaymentService), typeof(PaymentService));
متنساش الحاجات اللي انت ضيفتها في الـ CustomerBasket
تضيفهم في الـ CustomerBasketDto
بس فيه حاجتين المفروض يكونوا Nullable وهما الـ Payment Intent Id و Client Secret لأني وانا بعمل Create ليهم المفروض مبعتهمش عادي