Stripe Payments Continued
This commit is contained in:
parent
9763ef9c25
commit
a5e3cb84b7
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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){
|
||||
|
@ -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();
|
||||
})
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
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();
|
||||
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: createdOrder};
|
||||
this.router.navigate(['checkout/success'], navigationExtras);
|
||||
} else {
|
||||
this.toastr.error(paymentResult.error.message);
|
||||
}
|
||||
this.loading = false;
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
submitOrder(){
|
||||
const basket = this.basketService.getCurrentBasketValue();
|
||||
const orderToCreate = this.getOrderToCreate(basket);
|
||||
this.checkoutService.createOrder(orderToCreate).subscribe({
|
||||
next: (order: IOrder) => {
|
||||
this.toastr.success('Order created successfully');
|
||||
this.basketService.deleteLocalBasket(basket.id);
|
||||
const navigationExtras: NavigationExtras = {state: order};
|
||||
this.router.navigate(['checkout/success'], navigationExtras);
|
||||
},
|
||||
error: (e: any) => {
|
||||
this.toastr.error(e.message);
|
||||
console.log(e);
|
||||
},
|
||||
complete: () => { console.log('completed') }
|
||||
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,
|
||||
|
@ -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>
|
||||
|
@ -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'); }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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(()=> {
|
||||
|
@ -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 {
|
||||
|
@ -9,5 +9,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user