181 lines
7.3 KiB
TypeScript
181 lines
7.3 KiB
TypeScript
import { Controller, Get, HttpStatus, Post, Req, Res, UseFilters, Body, BadRequestException } from '@nestjs/common';
|
|
import { Throttle } from '@nestjs/throttler';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import type { Request, Response } from 'express';
|
|
import type { Config } from '../../../types/Config';
|
|
import { throttleProfiles } from '../../../config/throttleProfiles';
|
|
import { deriveCheckoutSessionState } from '../../../utils/checkout/deriveCheckoutSessionState';
|
|
import { OrderService } from '../../order/services/OrderService';
|
|
import { StorefrontExceptionFilter } from '../../storefrontCore/filters/StorefrontExceptionFilter';
|
|
import { StorefrontCartCookieService } from '../../storefrontCore/services/StorefrontCartCookieService';
|
|
import { StorefrontCheckoutSessionCookieService } from '../../storefrontCore/services/StorefrontCheckoutSessionCookieService';
|
|
import { StorefrontDiscountCookieService } from '../../storefrontCore/services/StorefrontDiscountCookieService';
|
|
import { StorefrontFeedbackCookieService } from '../../storefrontCore/services/StorefrontFeedbackCookieService';
|
|
import { StorefrontCaptchaCookieService } from '../../storefrontCore/services/StorefrontCaptchaCookieService';
|
|
import { StorefrontCaptchaService } from '../../storefrontCore/services/StorefrontCaptchaService';
|
|
import { StorefrontOrderAuthCookieService } from '../../storefrontCore/services/StorefrontOrderAuthCookieService';
|
|
import { StorefrontShopViewService } from '../../storefrontCore/services/StorefrontShopViewService';
|
|
import { StorefrontCartService } from '../../storefrontCart/services/StorefrontCartService';
|
|
import { CheckoutSessionService } from '../services/CheckoutSessionService';
|
|
import { PayCheckoutDto } from '../dto/PayCheckoutDto';
|
|
import { StorefrontCheckoutViewService } from '../services/StorefrontCheckoutViewService';
|
|
|
|
@Controller()
|
|
@UseFilters(StorefrontExceptionFilter)
|
|
export class StorefrontCheckoutController {
|
|
constructor(
|
|
private readonly cartService: StorefrontCartService,
|
|
private readonly checkoutSessionService: CheckoutSessionService,
|
|
private readonly checkoutViewService: StorefrontCheckoutViewService,
|
|
private readonly shopViewService: StorefrontShopViewService,
|
|
private readonly cartCookieService: StorefrontCartCookieService,
|
|
private readonly discountCookieService: StorefrontDiscountCookieService,
|
|
private readonly checkoutSessionCookieService: StorefrontCheckoutSessionCookieService,
|
|
private readonly feedbackCookieService: StorefrontFeedbackCookieService,
|
|
private readonly orderAuthCookieService: StorefrontOrderAuthCookieService,
|
|
private readonly orderService: OrderService,
|
|
private readonly configService: ConfigService,
|
|
private readonly captchaService: StorefrontCaptchaService,
|
|
private readonly captchaCookieService: StorefrontCaptchaCookieService
|
|
) {}
|
|
|
|
@Post('shop/checkout/pay')
|
|
@Throttle(throttleProfiles.checkoutPay)
|
|
async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha, paymentMethod }: PayCheckoutDto): Promise<void> {
|
|
const sessionId = this.checkoutSessionCookieService.getSessionId(req, res);
|
|
|
|
if (sessionId) {
|
|
res.redirect(HttpStatus.FOUND, '/shop/checkout');
|
|
|
|
return;
|
|
}
|
|
|
|
const encryptedAnswer = this.captchaCookieService.getAnswer(req, res, { consume: true });
|
|
|
|
const isCaptchaValid = this.captchaService.verify(captcha, encryptedAnswer);
|
|
|
|
if (!isCaptchaValid) {
|
|
throw new BadRequestException('Incorrect captcha. Try again.');
|
|
}
|
|
|
|
const cart = this.cartCookieService.getCart(req, res);
|
|
|
|
const discountCodes = this.discountCookieService.getDiscountCodes(req, res);
|
|
|
|
const summary = await this.cartService.getCartSummary(cart, discountCodes);
|
|
|
|
const session = await this.checkoutSessionService.createFromCartSummary(summary, paymentMethod);
|
|
|
|
this.checkoutSessionCookieService.setSessionId(req, res, session.id);
|
|
|
|
res.redirect(HttpStatus.FOUND, '/shop/checkout');
|
|
}
|
|
|
|
@Get('shop/checkout')
|
|
async checkoutPage(@Req() req: Request, @Res() res: Response) {
|
|
const sessionId = this.checkoutSessionCookieService.getSessionId(req, res);
|
|
|
|
if (!sessionId) {
|
|
res.redirect(HttpStatus.FOUND, '/shop/cart');
|
|
|
|
return;
|
|
}
|
|
|
|
const session = await this.checkoutSessionService.findById(sessionId);
|
|
|
|
if (!session) {
|
|
this.checkoutSessionCookieService.clearSession(req, res);
|
|
|
|
this.feedbackCookieService.setFeedback(req, res, {
|
|
type: 'error',
|
|
text: 'Checkout session no longer available.'
|
|
});
|
|
|
|
res.redirect(HttpStatus.FOUND, '/shop/cart');
|
|
|
|
return;
|
|
}
|
|
|
|
const orderId = await this.orderService.findIdByCheckoutSessionId(sessionId);
|
|
|
|
if (orderId) {
|
|
this.checkoutSessionCookieService.clearSession(req, res);
|
|
this.cartCookieService.clearCart(req, res);
|
|
this.discountCookieService.clearDiscount(req, res);
|
|
|
|
this.orderAuthCookieService.grantAccess(req, res, orderId);
|
|
|
|
this.feedbackCookieService.setFeedback(req, res, {
|
|
type: 'success',
|
|
text: 'Order placed successfully.'
|
|
});
|
|
|
|
res.redirect(HttpStatus.FOUND, `/shop/order/${orderId}`);
|
|
|
|
return;
|
|
}
|
|
|
|
const sessionState = deriveCheckoutSessionState(session);
|
|
|
|
if (sessionState.isPastDue) {
|
|
this.checkoutSessionCookieService.clearSession(req, res);
|
|
|
|
this.feedbackCookieService.setFeedback(req, res, {
|
|
type: 'error',
|
|
text: 'Checkout session expired.'
|
|
});
|
|
|
|
res.redirect(HttpStatus.FOUND, '/shop/cart');
|
|
|
|
return;
|
|
}
|
|
|
|
if (sessionState.isCancelled) {
|
|
this.checkoutSessionCookieService.clearSession(req, res);
|
|
|
|
this.feedbackCookieService.setFeedback(req, res, {
|
|
type: 'success',
|
|
text: 'Checkout cancelled.'
|
|
});
|
|
|
|
res.redirect(HttpStatus.FOUND, '/shop/cart');
|
|
|
|
return;
|
|
}
|
|
|
|
const [shopLocals, checkoutView] = await Promise.all([
|
|
this.shopViewService.buildShopRenderLocals(req, res, {
|
|
title: 'Checkout',
|
|
metaDescription: 'Complete your purchase at {shopName}.'
|
|
}),
|
|
this.checkoutViewService.toCheckoutView(session)
|
|
]);
|
|
|
|
const { checkoutStatusRefreshSec } = this.configService.get('order') as Config['order'];
|
|
|
|
return res.render('checkout', {
|
|
checkout: checkoutView,
|
|
refreshSec: checkoutStatusRefreshSec,
|
|
...shopLocals
|
|
});
|
|
}
|
|
|
|
@Post('shop/checkout/cancel')
|
|
async cancelCheckout(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
const sessionId = this.checkoutSessionCookieService.getSessionId(req, res);
|
|
|
|
if (sessionId) {
|
|
await this.checkoutSessionService.cancelSession(sessionId);
|
|
}
|
|
|
|
this.checkoutSessionCookieService.clearSession(req, res);
|
|
|
|
this.feedbackCookieService.setFeedback(req, res, {
|
|
type: 'success',
|
|
text: 'Checkout cancelled.'
|
|
});
|
|
|
|
res.redirect(HttpStatus.FOUND, '/shop/cart');
|
|
}
|
|
}
|