From dcc6396b4db57fd1153a628e2083c120bdc488e6 Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Tue, 31 May 2022 11:38:23 -0700 Subject: [PATCH] Completed Stripe Payments --- API/Controllers/PaymentsController.cs | 37 ++++++++++++++++++- Core/Interfaces/IPaymentService.cs | 3 ++ ...ByPaymentIntentIdWithItemsSpecification.cs | 4 +- Infrastructure/Services/OrderService.cs | 2 +- Infrastructure/Services/PaymentService.cs | 27 ++++++++++++++ .../checkout-address.component.html | 2 +- .../checkout-address.component.ts | 6 ++- .../checkout-delivery.component.html | 2 +- .../checkout-payments.component.html | 7 +++- .../checkout-payments.component.ts | 24 ++++++++---- .../components/stepper/stepper.component.html | 1 + 11 files changed, 98 insertions(+), 17 deletions(-) diff --git a/API/Controllers/PaymentsController.cs b/API/Controllers/PaymentsController.cs index 71de009..a0cbd8b 100644 --- a/API/Controllers/PaymentsController.cs +++ b/API/Controllers/PaymentsController.cs @@ -3,14 +3,19 @@ using Core.Entities; using Core.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Stripe; +using Order = Core.Entities.OrderAggregate.Order; namespace API.Controllers { public class PaymentsController : BaseApiController { private readonly IPaymentService _paymentService; - public PaymentsController(IPaymentService paymentService) + private const string WhSecret = "whsec_231beef81e46f9d27d4543dda6f4fbbd72adcc08605ff5549cc9cb36f38fcf78"; + private readonly ILogger _logger; + public PaymentsController(IPaymentService paymentService, ILogger logger) { + _logger = logger; _paymentService = paymentService; } @@ -22,5 +27,33 @@ namespace API.Controllers if(basket == null) return BadRequest(new ApiResponse(400, "Problem with your basket")); return basket; } + + [HttpPost("webhook")] + public async Task StripeWebhook() + { + var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], WhSecret); + + PaymentIntent intent; + Order order; + + switch (stripeEvent.Type) + { + case "payment_intent.succeeded": + intent = (PaymentIntent) stripeEvent.Data.Object; + _logger.LogInformation("Payment Succeeded ", intent.Id); + order = await _paymentService.UpdateOrderPaymentSucceeeded(intent.Id); + _logger.LogInformation("Order updated to payment received ", order.Id); + break; + case "payment_intent.payment_failed": + intent = (PaymentIntent) stripeEvent.Data.Object; + _logger.LogInformation("Payment Failed ", intent.Id); + order = await _paymentService.UpdateOrderPaymentFailed(intent.Id); + _logger.LogInformation("Payment Failed ", order.Id); + break; + } + + return new EmptyResult(); + } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/Core/Interfaces/IPaymentService.cs b/Core/Interfaces/IPaymentService.cs index 4bf7c33..05bab2c 100644 --- a/Core/Interfaces/IPaymentService.cs +++ b/Core/Interfaces/IPaymentService.cs @@ -1,9 +1,12 @@ using Core.Entities; +using Core.Entities.OrderAggregate; namespace Core.Interfaces { public interface IPaymentService { Task CreateOrUpdatePaymentIntent(string basketId); + Task UpdateOrderPaymentSucceeeded(string paymentIntentId); + Task UpdateOrderPaymentFailed(string paymentIntentId); } } \ No newline at end of file diff --git a/Core/Specifications/OrderByPaymentIntentIdWithItemsSpecification.cs b/Core/Specifications/OrderByPaymentIntentIdWithItemsSpecification.cs index 2425751..a2dd548 100644 --- a/Core/Specifications/OrderByPaymentIntentIdWithItemsSpecification.cs +++ b/Core/Specifications/OrderByPaymentIntentIdWithItemsSpecification.cs @@ -2,9 +2,9 @@ using Core.Entities.OrderAggregate; namespace Core.Specifications { - public class OrderByPaymentIntentIdWithItemsSpecification : BaseSpecification + public class OrderByPaymentIntentIdSpecification : BaseSpecification { - public OrderByPaymentIntentIdWithItemsSpecification(string paymentIntentId) : base(o => o.PaymentIntentId == paymentIntentId) + public OrderByPaymentIntentIdSpecification(string paymentIntentId) : base(o => o.PaymentIntentId == paymentIntentId) { } } diff --git a/Infrastructure/Services/OrderService.cs b/Infrastructure/Services/OrderService.cs index 0c450ad..190b89a 100644 --- a/Infrastructure/Services/OrderService.cs +++ b/Infrastructure/Services/OrderService.cs @@ -31,7 +31,7 @@ namespace Infrastructure.Services } var deliveryMethod = await _unitOfWork.Repository().GetByIdAsync(deliverMethodId); var subtotal = items.Sum(item => item.Price * item.Quantity); - var spec = new OrderByPaymentIntentIdWithItemsSpecification(basket.PaymentItentId); + var spec = new OrderByPaymentIntentIdSpecification(basket.PaymentItentId); var existingOrder = await _unitOfWork.Repository().GetEntityWithSpec(spec); if(existingOrder != null) { diff --git a/Infrastructure/Services/PaymentService.cs b/Infrastructure/Services/PaymentService.cs index 75f6a0a..3081fb0 100644 --- a/Infrastructure/Services/PaymentService.cs +++ b/Infrastructure/Services/PaymentService.cs @@ -1,8 +1,10 @@ using Core.Entities; using Core.Entities.OrderAggregate; using Core.Interfaces; +using Core.Specifications; using Microsoft.Extensions.Configuration; using Stripe; +using Order = Core.Entities.OrderAggregate.Order; using Product = Core.Entities.Product; namespace Infrastructure.Services @@ -63,5 +65,30 @@ namespace Infrastructure.Services await _basketRepository.UpdateBasketAsync(basket); return basket; } + + public async Task UpdateOrderPaymentFailed(string paymentIntentId) + { + var spec = new OrderByPaymentIntentIdSpecification(paymentIntentId); + var order = await _unitOfWork.Repository().GetEntityWithSpec(spec); + + if(order == null) return null; + + order.Status = OrderStatus.PaymentFailed; + await _unitOfWork.Complete(); + return order; + } + + public async Task UpdateOrderPaymentSucceeeded(string paymentIntentId) + { + var spec = new OrderByPaymentIntentIdSpecification(paymentIntentId); + var order = await _unitOfWork.Repository().GetEntityWithSpec(spec); + + if(order == null) return null; + + order.Status = OrderStatus.PaymentReceived; + _unitOfWork.Repository().Update(order); + await _unitOfWork.Complete(); + return order; + } } } \ No newline at end of file diff --git a/client/src/app/checkout/checkout-address/checkout-address.component.html b/client/src/app/checkout/checkout-address/checkout-address.component.html index b79cb7e..96e4981 100644 --- a/client/src/app/checkout/checkout-address/checkout-address.component.html +++ b/client/src/app/checkout/checkout-address/checkout-address.component.html @@ -33,7 +33,7 @@ - diff --git a/client/src/app/checkout/checkout-address/checkout-address.component.ts b/client/src/app/checkout/checkout-address/checkout-address.component.ts index 3c719a2..5dcf1b4 100644 --- a/client/src/app/checkout/checkout-address/checkout-address.component.ts +++ b/client/src/app/checkout/checkout-address/checkout-address.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; import { AccountService } from 'src/app/account/account.service'; +import { IAddress } from 'src/app/shared/models/address'; @Component({ selector: 'app-checkout-address', @@ -18,7 +19,10 @@ export class CheckoutAddressComponent implements OnInit { saveUserAddress(){ this.accountService.updateUserAddress(this.checkoutForm.get('addressForm').value).subscribe({ - next: () => { this.toaster.success('Address Saved'); }, + next: (address: IAddress) => { + this.toaster.success('Address Saved'); + this.checkoutForm.get('addressForm').reset(address); + }, error: (e: any) => { this.toaster.error(e.message); }, diff --git a/client/src/app/checkout/checkout-delivery/checkout-delivery.component.html b/client/src/app/checkout/checkout-delivery/checkout-delivery.component.html index 7cff2e4..747d3dd 100644 --- a/client/src/app/checkout/checkout-delivery/checkout-delivery.component.html +++ b/client/src/app/checkout/checkout-delivery/checkout-delivery.component.html @@ -21,7 +21,7 @@ - diff --git a/client/src/app/checkout/checkout-payments/checkout-payments.component.html b/client/src/app/checkout/checkout-payments/checkout-payments.component.html index aa8bb4c..f952281 100644 --- a/client/src/app/checkout/checkout-payments/checkout-payments.component.html +++ b/client/src/app/checkout/checkout-payments/checkout-payments.component.html @@ -23,7 +23,12 @@ - diff --git a/client/src/app/checkout/checkout-payments/checkout-payments.component.ts b/client/src/app/checkout/checkout-payments/checkout-payments.component.ts index 04d8cbe..39bbd85 100644 --- a/client/src/app/checkout/checkout-payments/checkout-payments.component.ts +++ b/client/src/app/checkout/checkout-payments/checkout-payments.component.ts @@ -27,6 +27,9 @@ export class CheckoutPaymentsComponent implements AfterViewInit, OnDestroy { cardErrors: any; cardHandler = this.onChange.bind(this); loading = false; + cardNumberValid = false; + cardExpiryValid = false; + cardCvcValid = false; constructor(private basketService: BasketService, private checkoutService: CheckoutService, private toastr: ToastrService, private router: Router) { } @@ -53,13 +56,18 @@ export class CheckoutPaymentsComponent implements AfterViewInit, OnDestroy { this.cardCvc.destroy(); } - onChange({error}) { - if(error) - { - this.cardErrors = error.message; - } else - { - this.cardErrors = null; + onChange(event) { + if(event.error) { this.cardErrors = event.error.message; } else { this.cardErrors = null; } + switch (event.elementType) { + case 'cardNumber': + this.cardNumberValid = event.complete; + break; + case 'cardExpiry': + this.cardExpiryValid = event.complete; + break; + case 'cardCvc': + this.cardCvcValid = event.complete; + break; } } @@ -72,7 +80,7 @@ export class CheckoutPaymentsComponent implements AfterViewInit, OnDestroy { if(paymentResult.paymentIntent) { - this.basketService.deleteLocalBasket(basket.id); + this.basketService.deleteBasket(basket); const navigationExtras: NavigationExtras = {state: createdOrder}; this.router.navigate(['checkout/success'], navigationExtras); } else { diff --git a/client/src/app/shared/components/stepper/stepper.component.html b/client/src/app/shared/components/stepper/stepper.component.html index 0fcf3a8..9c8528b 100644 --- a/client/src/app/shared/components/stepper/stepper.component.html +++ b/client/src/app/shared/components/stepper/stepper.component.html @@ -2,6 +2,7 @@