Stripe Payments Continued

This commit is contained in:
Charles Showalter 2022-05-30 13:25:27 -07:00
parent 9763ef9c25
commit a5e3cb84b7
17 changed files with 189 additions and 33 deletions

View File

@ -1,3 +1,4 @@
using API.Errors;
using Core.Entities;
using Core.Interfaces;
using Microsoft.AspNetCore.Authorization;
@ -17,7 +18,9 @@ namespace API.Controllers
[HttpPost("{basketId}")]
public async Task<ActionResult<CustomerBasket>> CreateOrUpdatePaymentIntent(string basketId)
{
return await _paymentService.CreateOrUpdatePaymentIntent(basketId);
var basket = await _paymentService.CreateOrUpdatePaymentIntent(basketId);
if(basket == null) return BadRequest(new ApiResponse(400, "Problem with your basket"));
return basket;
}
}
}

View File

@ -10,5 +10,7 @@ namespace API.Dtos
public int? DeliveryMethodId { get; set; }
public string ClientSecret { get; set; }
public string PaymentItentId { get; set; }
public decimal ShippingPrice { get; set; }
}
}

View File

@ -21,5 +21,6 @@ namespace Core.Entities
public int? DeliveryMethodId { get; set; }
public string ClientSecret { get; set; }
public string PaymentItentId { get; set; }
public decimal ShippingPrice { get; set; }
}
}

View File

@ -6,13 +6,14 @@ namespace Core.Entities.OrderAggregate
{
}
public Order(IReadOnlyList<OrderItem> orderItems, string buyerEmail, Address shipToAddress, DeliveryMethod deliveryMethod, decimal subtotal)
public Order(IReadOnlyList<OrderItem> orderItems, string buyerEmail, Address shipToAddress, DeliveryMethod deliveryMethod, decimal subtotal, string paymentIntentId)
{
BuyerEmail = buyerEmail;
ShipToAddress = shipToAddress;
DeliveryMethod = deliveryMethod;
OrderItems = orderItems;
Subtotal = subtotal;
PaymentIntentId = paymentIntentId;
}
public string BuyerEmail { get; set; }

View File

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

View File

@ -9,9 +9,11 @@ namespace Infrastructure.Services
{
private readonly IBasketRepository _basketRepo;
private readonly IUnitOfWork _unitOfWork;
private readonly IPaymentService _paymentService;
public OrderService(IBasketRepository basketRepo, IUnitOfWork unitOfWork)
public OrderService(IBasketRepository basketRepo, IUnitOfWork unitOfWork, IPaymentService paymentService)
{
_paymentService = paymentService;
_unitOfWork = unitOfWork;
_basketRepo = basketRepo;
}
@ -29,11 +31,19 @@ namespace Infrastructure.Services
}
var deliveryMethod = await _unitOfWork.Repository<DeliveryMethod>().GetByIdAsync(deliverMethodId);
var subtotal = items.Sum(item => item.Price * item.Quantity);
var order = new Order(items, buyerEmail, shippingAddress, deliveryMethod, subtotal);
var spec = new OrderByPaymentIntentIdWithItemsSpecification(basket.PaymentItentId);
var existingOrder = await _unitOfWork.Repository<Order>().GetEntityWithSpec(spec);
if(existingOrder != null)
{
_unitOfWork.Repository<Order>().Delete(existingOrder);
await _paymentService.CreateOrUpdatePaymentIntent(basket.PaymentItentId);
}
var order = new Order(items, buyerEmail, shippingAddress, deliveryMethod, subtotal, basket.PaymentItentId);
_unitOfWork.Repository<Order>().Add(order);
var result = await _unitOfWork.Complete();
if(result <= 0) return null;
await _basketRepo.DeleteBasketAsysnc(basketId);
return order;
}

View File

@ -23,6 +23,7 @@ namespace Infrastructure.Services
{
StripeConfiguration.ApiKey = _config["StripeSettings:SecretKey"];
var basket = await _basketRepository.GetBasketAsync(basketId);
if(basket == null) return null;
var shippingPrice = 0m;
if(basket.DeliveryMethodId.HasValue){

View File

@ -19,15 +19,28 @@ export class BasketService {
constructor(private http: HttpClient) { }
createPaymentIntent(){
return this.http.post(this.baseUrl + 'payments/' + this.getCurrentBasketValue().id, {}).pipe(
map((basket: IBasket) => {
this.basketSource.next(basket);
})
);
}
setShippingPrice(deliveryMethod: IDeliveryMethods){
this.shipping = deliveryMethod.price;
const basket = this.getCurrentBasketValue();
basket.deliveryMethodId = deliveryMethod.id;
basket.shippingPrice = deliveryMethod.price;
this.calculateTotals();
this.setBasket(basket);
}
getBasket(id: string){
return this.http.get(this.baseUrl + 'basket?id=' + id).pipe(
map((basket: IBasket) => {
this.basketSource.next(basket);
this.shipping = basket.shippingPrice;
this.calculateTotals();
})
);

View File

@ -1,9 +1,30 @@
<p>checkout-payments works!</p>
<div class="mt-5 pb-3" [formGroup]="checkoutForm">
<div class="row">
<div class="form-group col-12" formGroupName="paymentForm">
<app-text-inputs [label]="'Name on card'" formControlName="nameOnCard"></app-text-inputs>
</div>
<div class="form-group col-6">
<div class="form-control py-3" #cardNumber></div>
<ng-container *ngIf="cardErrors">
<span class="text-warning">
{{cardErrors}}
</span>
</ng-container>
</div>
<div class="form-group col-3">
<div class="form-control py-3" #cardExpiry></div>
</div>
<div class="form-group col-3">
<div class="form-control py-3" #cardCvc></div>
</div>
</div>
</div>
<div class="float-none d-flex justify-content-between flex-column flex-lg-row mb-5">
<button class="btn btn-outline-primary" cdkStepperPrevious>
<i class="fa fa-angle-left"></i> Back to Review
</button>
<button class="btn btn-primary" (click)="submitOrder()">
<button [disabled]="loading" 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>
</div>

View File

@ -1,43 +1,106 @@
import { Component, Input, OnInit } from '@angular/core';
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { NavigationExtras, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { lastValueFrom } from 'rxjs';
import { BasketService } from 'src/app/basket/basket.service';
import { IBasket } from 'src/app/shared/models/baskset';
import { IOrder } from 'src/app/shared/models/order';
import { CheckoutService } from '../checkout.service';
declare var Stripe;
@Component({
selector: 'app-checkout-payments',
templateUrl: './checkout-payments.component.html',
styleUrls: ['./checkout-payments.component.scss']
})
export class CheckoutPaymentsComponent implements OnInit {
export class CheckoutPaymentsComponent implements AfterViewInit, OnDestroy {
@Input() checkoutForm: FormGroup;
@ViewChild('cardNumber', {static: true}) cardNumberElement: ElementRef;
@ViewChild('cardExpiry', {static: true}) cardExpiryElement: ElementRef;
@ViewChild('cardCvc', {static: true}) cardCvcElement: ElementRef;
stripe: any;
cardNumber: any;
cardExpiry: any;
cardCvc: any;
cardErrors: any;
cardHandler = this.onChange.bind(this);
loading = false;
constructor(private basketService: BasketService, private checkoutService: CheckoutService, private toastr: ToastrService, private router: Router) { }
ngOnInit(): void {
ngAfterViewInit(): void {
this.stripe = Stripe('pk_test_51L4B2fKnYx7tVZlJDJxQ56Wrjip0xv6ieenwhyzoM8hPu2iHRg581JBJAwOpMoQLlsWg7uSs3izk7xQv0xpT5sK000HDZNIA1l');
const elements = this.stripe.elements();
this.cardNumber = elements.create('cardNumber');
this.cardNumber.mount(this.cardNumberElement.nativeElement);
this.cardNumber.addEventListener('change', this.cardHandler);
this.cardExpiry = elements.create('cardExpiry');
this.cardExpiry.mount(this.cardExpiryElement.nativeElement);
this.cardExpiry.addEventListener('change', this.cardHandler);
this.cardCvc = elements.create('cardCvc');
this.cardCvc.mount(this.cardCvcElement.nativeElement);
this.cardCvc.addEventListener('change', this.cardHandler);
}
submitOrder(){
ngOnDestroy(): void {
this.cardNumber.destroy();
this.cardExpiry.destroy();
this.cardCvc.destroy();
}
onChange({error}) {
if(error)
{
this.cardErrors = error.message;
} else
{
this.cardErrors = null;
}
}
async submitOrder(){
this.loading = true;
const basket = this.basketService.getCurrentBasketValue();
const orderToCreate = this.getOrderToCreate(basket);
this.checkoutService.createOrder(orderToCreate).subscribe({
next: (order: IOrder) => {
this.toastr.success('Order created successfully');
try {
const createdOrder = await this.createOrder(basket);
const paymentResult = await this.confirmPaymentWithStripe(basket);
if(paymentResult.paymentIntent)
{
this.basketService.deleteLocalBasket(basket.id);
const navigationExtras: NavigationExtras = {state: order};
const navigationExtras: NavigationExtras = {state: createdOrder};
this.router.navigate(['checkout/success'], navigationExtras);
},
error: (e: any) => {
this.toastr.error(e.message);
console.log(e);
},
complete: () => { console.log('completed') }
} else {
this.toastr.error(paymentResult.error.message);
}
this.loading = false;
} catch(error) {
console.log(error);
this.loading = false;
}
}
private async confirmPaymentWithStripe(basket: IBasket) {
return this.stripe.confirmCardPayment(basket.clientSecret, {
payment_method: {
card: this.cardNumber,
billing_details: {
name: this.checkoutForm.get('paymentForm').get('nameOnCard').value
}
}
});
}
private async createOrder(basket: IBasket) {
const orderToCreate = this.getOrderToCreate(basket);
return lastValueFrom(this.checkoutService.createOrder(orderToCreate));
}
private getOrderToCreate(basket: IBasket) {
return {
basketId: basket.id,

View File

@ -8,7 +8,7 @@
<button class="btn btn-outline-primary" cdkStepperPrevious>
<i class="fa fa-angle-left"></i> Back to Delivery
</button>
<button class="btn btn-primary" cdkStepperNext>
<button class="btn btn-primary" (click)="createPaymentIntent()">
Payment Options <i class="fa fa-angle-right"></i>
</button>
</div>

View File

@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { CdkStepper } from '@angular/cdk/stepper';
import { Component, Input, OnInit } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { BasketService } from 'src/app/basket/basket.service';
import { IBasket } from 'src/app/shared/models/baskset';
@ -9,12 +11,23 @@ import { IBasket } from 'src/app/shared/models/baskset';
styleUrls: ['./checkout-review.component.scss']
})
export class CheckoutReviewComponent implements OnInit {
@Input() appStepper: CdkStepper;
basket$: Observable<IBasket>;
constructor(private basketService: BasketService) { }
constructor(private basketService: BasketService, private toastrService: ToastrService) { }
ngOnInit(): void {
this.basket$ = this.basketService.basket$;
}
createPaymentIntent(){
return this.basketService.createPaymentIntent().subscribe({
next: (response: any) => {
this.appStepper.next();
},
error: (e: any ) => { console.log(e); },
complete: () => { console.log('completed'); }
})
}
}

View File

@ -1,7 +1,7 @@
<div class="container mt-5">
<div class="row">
<div class="col-8">
<app-stepper [linearModeSelected]="false" #appStepper>
<app-stepper [linearModeSelected]="true" #appStepper>
<cdk-step [label]="'Address'" [completed]="(checkoutForm.get('addressForm')).valid">
<app-checkout-address [checkoutForm]="checkoutForm"></app-checkout-address>
</cdk-step>
@ -9,7 +9,7 @@
<app-checkout-delivery [checkoutForm]="checkoutForm"></app-checkout-delivery>
</cdk-step>
<cdk-step [label]="'Review'">
<app-checkout-review></app-checkout-review>
<app-checkout-review [appStepper]="appStepper"></app-checkout-review>
</cdk-step>
<cdk-step [label]="'Payment'">
<app-checkout-payments [checkoutForm]="checkoutForm"></app-checkout-payments>

View File

@ -20,6 +20,7 @@ export class CheckoutComponent implements OnInit {
ngOnInit(): void {
this.createCheckoutForm();
this.getAddressFormValues();
this.getDeliveryMethodValue();
this.basketTotals$ = this.basketService.basketTotal$;
}
@ -36,7 +37,7 @@ export class CheckoutComponent implements OnInit {
deliveryForm: this.fb.group({
deliveryMethod: [null, Validators.required]
}),
paymenForm: this.fb.group({
paymentForm: this.fb.group({
nameOnCard: [null, Validators.required]
})
});
@ -54,4 +55,11 @@ export class CheckoutComponent implements OnInit {
});
}
getDeliveryMethodValue(){
const basket = this.basketService.getCurrentBasketValue();
if(basket.deliveryMethodId !== null){
this.checkoutForm.get('deliveryForm').get('deliveryMethod').patchValue(basket.deliveryMethodId.toString());
}
}
}

View File

@ -8,10 +8,14 @@ export class LoadingInterceptor implements HttpInterceptor {
constructor(private busyService: BusyService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if(!req.url.includes('emailexists')){
this.busyService.busy();
if(req.method === 'POST' && req.url.includes('orders')){
return next.handle(req);
}
if(req.url.includes('emailexists')){
return next.handle(req);
}
this.busyService.busy();
return next.handle(req).pipe(
delay(1000),
finalize(()=> {

View File

@ -13,6 +13,10 @@ export interface IBasketItem {
export interface IBasket {
id: string;
items: IBasketItem[];
clientSecret?: string;
paymentIntentId?: string;
deliveryMethodId?: number;
shippingPrice?: number;
}
export class Basket implements IBasket {

View File

@ -9,5 +9,6 @@
</head>
<body>
<app-root></app-root>
<script src="https://js.stripe.com/v3/"></script>
</body>
</html>