Completed Stripe Payments
This commit is contained in:
parent
ea66486e7e
commit
dcc6396b4d
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user