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 { 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 { 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'); } }