import { Body, Controller, Get, HttpStatus, Post, Req, Res, UseFilters } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Throttle } from '@nestjs/throttler'; import type { Request, Response } from 'express'; import { throttleProfiles } from '../../../config/throttleProfiles'; import type { Config } from '../../../types/Config'; import { paymentMethodIconUrl } from '../../../consts/paymentMethodIconUrl'; import { paymentMethodLabel } from '../../../consts/paymentMethodLabel'; 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 { StorefrontShopViewService } from '../../storefrontCore/services/StorefrontShopViewService'; import { safeInternalShopRedirectPath } from '../../../utils/safeInternalShopRedirectPath'; import { AddToCartDto } from '../dto/AddToCartDto'; import { ApplyDiscountCodeDto } from '../dto/ApplyDiscountCodeDto'; import { RemoveDiscountCodeDto } from '../dto/RemoveDiscountCodeDto'; import { RemoveFromCartDto } from '../dto/RemoveFromCartDto'; import { UpdateCartQtyDto } from '../dto/UpdateCartQtyDto'; import { StorefrontCartService } from '../services/StorefrontCartService'; import { StorefrontDiscountService } from '../services/StorefrontDiscountService'; import type { CartCookieMutation } from '../types/CartCookieMutation'; import type { DiscountCookieMutation } from '../types/DiscountCookieMutation'; @Controller() @UseFilters(StorefrontExceptionFilter) export class StorefrontCartController { constructor( private readonly cartService: StorefrontCartService, private readonly discountService: StorefrontDiscountService, private readonly shopViewService: StorefrontShopViewService, private readonly cartCookieService: StorefrontCartCookieService, private readonly discountCookieService: StorefrontDiscountCookieService, private readonly feedbackCookieService: StorefrontFeedbackCookieService, private readonly checkoutSessionCookieService: StorefrontCheckoutSessionCookieService, private readonly captchaService: StorefrontCaptchaService, private readonly captchaCookieService: StorefrontCaptchaCookieService, private readonly configService: ConfigService ) {} @Get('shop/cart') @Throttle(throttleProfiles.cartPage) async cartSummary(@Req() req: Request, @Res() res: Response) { const checkoutSessionId = this.checkoutSessionCookieService.getSessionId(req, res); if (checkoutSessionId) { res.redirect(HttpStatus.FOUND, '/shop/checkout'); return; } const cart = this.cartCookieService.getCart(req, res); const discountCodes = this.discountCookieService.getDiscountCodes(req, res); const [summary, shopLocals] = await Promise.all([ this.cartService.getCartSummary(cart, discountCodes), this.shopViewService.buildShopRenderLocals(req, res, { title: 'Cart', metaDescription: 'Review your cart at {shopName}.' }) ]); const { svg: captchaSvg, encryptedAnswer } = this.captchaService.create(); this.captchaCookieService.setAnswer(req, res, encryptedAnswer); const { enabledPaymentMethods } = this.configService.get('shopSettings') as Config['shopSettings']; const paymentMethods = enabledPaymentMethods.map(paymentMethod => ({ paymentMethod, paymentMethodLabel: paymentMethodLabel[paymentMethod], iconUrl: paymentMethodIconUrl[paymentMethod] })); return res.render('cart-summary', { ...summary, ...shopLocals, paymentMethods, captchaSvg }); } @Post('shop/cart/product') async addToCart( @Req() req: Request, @Res() res: Response, @Body() { variantId, qty }: AddToCartDto ): Promise { const cart = this.cartCookieService.getCart(req, res); const cartMutation = await this.cartService.addToCart(cart, variantId, qty); this.applyCartCookieMutation(req, res, cartMutation); this.feedbackCookieService.setFeedback(req, res, { type: 'success', text: 'Added to cart.' }); res.redirect(HttpStatus.FOUND, this.buildAddToCartRedirectPath(req, variantId)); } @Post('shop/cart/product/update') async updateCartQty( @Req() req: Request, @Res() res: Response, @Body() { variantId, qty }: UpdateCartQtyDto ): Promise { const cart = this.cartCookieService.getCart(req, res); const cartMutation = await this.cartService.updateCartQty(cart, variantId, qty); this.applyCartCookieMutation(req, res, cartMutation); this.feedbackCookieService.setFeedback(req, res, { type: 'success', text: 'Cart updated.' }); res.redirect(HttpStatus.FOUND, '/shop/cart'); } @Post('shop/cart/product/remove') removeFromCart(@Req() req: Request, @Res() res: Response, @Body() { variantId }: RemoveFromCartDto): void { const cart = this.cartCookieService.getCart(req, res); const cartMutation = this.cartService.removeFromCart(cart, variantId); this.applyCartCookieMutation(req, res, cartMutation); this.feedbackCookieService.setFeedback(req, res, { type: 'success', text: 'Item removed.' }); res.redirect(HttpStatus.FOUND, '/shop/cart'); } @Post('shop/cart/discount') @Throttle(throttleProfiles.cartDiscount) async applyDiscount( @Req() req: Request, @Res() res: Response, @Body() { code }: ApplyDiscountCodeDto ): Promise { const cart = this.cartCookieService.getCart(req, res); const discountCodes = this.discountCookieService.getDiscountCodes(req, res); const nextDiscountCodes = await this.cartService.applyDiscountCode(cart, discountCodes, code); this.applyDiscountCookieMutation(req, res, nextDiscountCodes); this.feedbackCookieService.setFeedback(req, res, { type: 'success', text: 'Discount code applied.' }); res.redirect(HttpStatus.FOUND, '/shop/cart'); } @Post('shop/cart/discount/remove') removeDiscount(@Req() req: Request, @Res() res: Response, @Body() { code }: RemoveDiscountCodeDto): void { const discountCodes = this.discountCookieService.getDiscountCodes(req, res); const discountMutation = this.discountService.removeDiscountCode(discountCodes, code); this.applyDiscountCookieMutation(req, res, discountMutation); this.feedbackCookieService.setFeedback(req, res, { type: 'success', text: 'Discount code removed.' }); res.redirect(HttpStatus.FOUND, '/shop/cart'); } private applyDiscountCookieMutation(req: Request, res: Response, discountMutation: DiscountCookieMutation): void { if (discountMutation === null) { this.discountCookieService.clearDiscount(req, res); } else { this.discountCookieService.setDiscountCodes(req, res, discountMutation); } } private applyCartCookieMutation(req: Request, res: Response, cartMutation: CartCookieMutation): void { if (cartMutation === null) { this.cartCookieService.clearCart(req, res); this.discountCookieService.clearDiscount(req, res); } else { this.cartCookieService.setCart(req, res, cartMutation); } } private buildAddToCartRedirectPath(req: Request, variantId: string): string { const redirectPath = safeInternalShopRedirectPath(req); const pathname = redirectPath.split('?')[0] ?? redirectPath; const isProductsIndexPath = pathname === '/' || pathname.startsWith('/shop/categories/'); if (!isProductsIndexPath) { return redirectPath; } const anchor = `product-card-${variantId}`; return `${redirectPath}#${anchor}`; } }