Files
nullcart/backend/src/modules/storefrontCart/controllers/StorefrontCartController.ts
T

207 lines
8.6 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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}`;
}
}