نبدأ نعمل الـ 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:

  1. اليوزر هيكلم الـ API عشان يعمل Customer Basket فهتعملها بالـ Products وتحفظها في الـ Redis
  2. ترد انها Created للـ User
  3. الـ User بيكلم الـ API تاني عشان يعمل Payment Intent نية دفع
  4. كـ API هكلم الـ Stripe (محتاج أعمل الفانكشناليتي Payment Intend)
  5. الـ Stripe هيعمل Create Payment Intent وكمان هيرجع Client Secret
  6. الـ API هياخدهم ويحطهم في الـ Customer Basket وأرجعه للـ End user
  7. نعمل Create للـ Order من خلال الـ API هنعمل (Refactor)
  8. المفرض الـ 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 ليهم المفروض مبعتهمش عادي