Completed Stripe Payments

This commit is contained in:
Charles Showalter 2022-05-31 11:38:23 -07:00
parent ea66486e7e
commit dcc6396b4d
11 changed files with 98 additions and 17 deletions

View File

@ -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<IPaymentService> _logger;
public PaymentsController(IPaymentService paymentService, ILogger<IPaymentService> 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<ActionResult> 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();
}
}
}
}

View File

@ -1,9 +1,12 @@
using Core.Entities;
using Core.Entities.OrderAggregate;
namespace Core.Interfaces
{
public interface IPaymentService
{
Task<CustomerBasket> CreateOrUpdatePaymentIntent(string basketId);
Task<Order> UpdateOrderPaymentSucceeeded(string paymentIntentId);
Task<Order> UpdateOrderPaymentFailed(string paymentIntentId);
}
}

View File

@ -2,9 +2,9 @@ using Core.Entities.OrderAggregate;
namespace Core.Specifications
{
public class OrderByPaymentIntentIdWithItemsSpecification : BaseSpecification<Order>
public class OrderByPaymentIntentIdSpecification : BaseSpecification<Order>
{
public OrderByPaymentIntentIdWithItemsSpecification(string paymentIntentId) : base(o => o.PaymentIntentId == paymentIntentId)
public OrderByPaymentIntentIdSpecification(string paymentIntentId) : base(o => o.PaymentIntentId == paymentIntentId)
{
}
}

View File

@ -31,7 +31,7 @@ namespace Infrastructure.Services
}
var deliveryMethod = await _unitOfWork.Repository<DeliveryMethod>().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<Order>().GetEntityWithSpec(spec);
if(existingOrder != null)
{

View File

@ -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<Core.Entities.OrderAggregate.Order> UpdateOrderPaymentFailed(string paymentIntentId)
{
var spec = new OrderByPaymentIntentIdSpecification(paymentIntentId);
var order = await _unitOfWork.Repository<Order>().GetEntityWithSpec(spec);
if(order == null) return null;
order.Status = OrderStatus.PaymentFailed;
await _unitOfWork.Complete();
return order;
}
public async Task<Core.Entities.OrderAggregate.Order> UpdateOrderPaymentSucceeeded(string paymentIntentId)
{
var spec = new OrderByPaymentIntentIdSpecification(paymentIntentId);
var order = await _unitOfWork.Repository<Order>().GetEntityWithSpec(spec);
if(order == null) return null;
order.Status = OrderStatus.PaymentReceived;
_unitOfWork.Repository<Order>().Update(order);
await _unitOfWork.Complete();
return order;
}
}
}

View File

@ -33,7 +33,7 @@
<button class="btn btn-outline-primary" routerLink="/basket">
<i class="fa fa-angle-left"></i> Back to Shopping Cart
</button>
<button class="btn btn-primary" cdkStepperNext>
<button [disabled]="checkoutForm.get('addressForm').invalid" class="btn btn-primary" cdkStepperNext>
Delivery Options <i class="fa fa-angle-right"></i>
</button>
</div>

View File

@ -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);
},

View File

@ -21,7 +21,7 @@
<button class="btn btn-outline-primary" cdkStepperPrevious>
<i class="fa fa-angle-left"></i> Back to Address
</button>
<button class="btn btn-primary" cdkStepperNext>
<button [disabled]="checkoutForm.get('deliveryForm').invalid" class="btn btn-primary" cdkStepperNext>
Review Order <i class="fa fa-angle-right"></i>
</button>
</div>

View File

@ -23,7 +23,12 @@
<button class="btn btn-outline-primary" cdkStepperPrevious>
<i class="fa fa-angle-left"></i> Back to Review
</button>
<button [disabled]="loading" class="btn btn-primary" (click)="submitOrder()">
<button [disabled]="loading
|| checkoutForm.get('paymentForm').invalid
|| !cardNumberValid
|| !cardExpiryValid
|| !cardCvcValid"
class="btn btn-primary" (click)="submitOrder()">
Complete Checkout <i class="fa fa-angle-right"></i>
<i *ngIf="loading" class="fa fa-spinner fa-spin"></i>
</button>

View File

@ -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 {

View File

@ -2,6 +2,7 @@
<ul class="nav nav-pills nav-justified">
<li class="nav-item" *ngFor="let step of steps; let i = index">
<button
[disabled]="true"
(click)="onClick(i)"
[class.active]="selectedIndex === i"
class="nav-link py-3 text-uppercase font-weight-bold btn-block">