Completed Stripe Payments
This commit is contained in:
parent
ea66486e7e
commit
dcc6396b4d
@ -3,14 +3,19 @@ using Core.Entities;
|
|||||||
using Core.Interfaces;
|
using Core.Interfaces;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Stripe;
|
||||||
|
using Order = Core.Entities.OrderAggregate.Order;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
public class PaymentsController : BaseApiController
|
public class PaymentsController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IPaymentService _paymentService;
|
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;
|
_paymentService = paymentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,5 +27,33 @@ namespace API.Controllers
|
|||||||
if(basket == null) return BadRequest(new ApiResponse(400, "Problem with your basket"));
|
if(basket == null) return BadRequest(new ApiResponse(400, "Problem with your basket"));
|
||||||
return 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;
|
||||||
|
using Core.Entities.OrderAggregate;
|
||||||
|
|
||||||
namespace Core.Interfaces
|
namespace Core.Interfaces
|
||||||
{
|
{
|
||||||
public interface IPaymentService
|
public interface IPaymentService
|
||||||
{
|
{
|
||||||
Task<CustomerBasket> CreateOrUpdatePaymentIntent(string basketId);
|
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
|
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 deliveryMethod = await _unitOfWork.Repository<DeliveryMethod>().GetByIdAsync(deliverMethodId);
|
||||||
var subtotal = items.Sum(item => item.Price * item.Quantity);
|
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);
|
var existingOrder = await _unitOfWork.Repository<Order>().GetEntityWithSpec(spec);
|
||||||
if(existingOrder != null)
|
if(existingOrder != null)
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using Core.Entities;
|
using Core.Entities;
|
||||||
using Core.Entities.OrderAggregate;
|
using Core.Entities.OrderAggregate;
|
||||||
using Core.Interfaces;
|
using Core.Interfaces;
|
||||||
|
using Core.Specifications;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
using Order = Core.Entities.OrderAggregate.Order;
|
||||||
using Product = Core.Entities.Product;
|
using Product = Core.Entities.Product;
|
||||||
|
|
||||||
namespace Infrastructure.Services
|
namespace Infrastructure.Services
|
||||||
@ -63,5 +65,30 @@ namespace Infrastructure.Services
|
|||||||
await _basketRepository.UpdateBasketAsync(basket);
|
await _basketRepository.UpdateBasketAsync(basket);
|
||||||
return 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">
|
<button class="btn btn-outline-primary" routerLink="/basket">
|
||||||
<i class="fa fa-angle-left"></i> Back to Shopping Cart
|
<i class="fa fa-angle-left"></i> Back to Shopping Cart
|
||||||
</button>
|
</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>
|
Delivery Options <i class="fa fa-angle-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { AccountService } from 'src/app/account/account.service';
|
import { AccountService } from 'src/app/account/account.service';
|
||||||
|
import { IAddress } from 'src/app/shared/models/address';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-checkout-address',
|
selector: 'app-checkout-address',
|
||||||
@ -18,7 +19,10 @@ export class CheckoutAddressComponent implements OnInit {
|
|||||||
|
|
||||||
saveUserAddress(){
|
saveUserAddress(){
|
||||||
this.accountService.updateUserAddress(this.checkoutForm.get('addressForm').value).subscribe({
|
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) => {
|
error: (e: any) => {
|
||||||
this.toaster.error(e.message);
|
this.toaster.error(e.message);
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
||||||
<i class="fa fa-angle-left"></i> Back to Address
|
<i class="fa fa-angle-left"></i> Back to Address
|
||||||
</button>
|
</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>
|
Review Order <i class="fa fa-angle-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +23,12 @@
|
|||||||
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
||||||
<i class="fa fa-angle-left"></i> Back to Review
|
<i class="fa fa-angle-left"></i> Back to Review
|
||||||
</button>
|
</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>
|
Complete Checkout <i class="fa fa-angle-right"></i>
|
||||||
<i *ngIf="loading" class="fa fa-spinner fa-spin"></i>
|
<i *ngIf="loading" class="fa fa-spinner fa-spin"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -27,6 +27,9 @@ export class CheckoutPaymentsComponent implements AfterViewInit, OnDestroy {
|
|||||||
cardErrors: any;
|
cardErrors: any;
|
||||||
cardHandler = this.onChange.bind(this);
|
cardHandler = this.onChange.bind(this);
|
||||||
loading = false;
|
loading = false;
|
||||||
|
cardNumberValid = false;
|
||||||
|
cardExpiryValid = false;
|
||||||
|
cardCvcValid = false;
|
||||||
|
|
||||||
constructor(private basketService: BasketService, private checkoutService: CheckoutService, private toastr: ToastrService, private router: Router) { }
|
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();
|
this.cardCvc.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange({error}) {
|
onChange(event) {
|
||||||
if(error)
|
if(event.error) { this.cardErrors = event.error.message; } else { this.cardErrors = null; }
|
||||||
{
|
switch (event.elementType) {
|
||||||
this.cardErrors = error.message;
|
case 'cardNumber':
|
||||||
} else
|
this.cardNumberValid = event.complete;
|
||||||
{
|
break;
|
||||||
this.cardErrors = null;
|
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)
|
if(paymentResult.paymentIntent)
|
||||||
{
|
{
|
||||||
this.basketService.deleteLocalBasket(basket.id);
|
this.basketService.deleteBasket(basket);
|
||||||
const navigationExtras: NavigationExtras = {state: createdOrder};
|
const navigationExtras: NavigationExtras = {state: createdOrder};
|
||||||
this.router.navigate(['checkout/success'], navigationExtras);
|
this.router.navigate(['checkout/success'], navigationExtras);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<ul class="nav nav-pills nav-justified">
|
<ul class="nav nav-pills nav-justified">
|
||||||
<li class="nav-item" *ngFor="let step of steps; let i = index">
|
<li class="nav-item" *ngFor="let step of steps; let i = index">
|
||||||
<button
|
<button
|
||||||
|
[disabled]="true"
|
||||||
(click)="onClick(i)"
|
(click)="onClick(i)"
|
||||||
[class.active]="selectedIndex === i"
|
[class.active]="selectedIndex === i"
|
||||||
class="nav-link py-3 text-uppercase font-weight-bold btn-block">
|
class="nav-link py-3 text-uppercase font-weight-bold btn-block">
|
||||||
|
Loading…
Reference in New Issue
Block a user