207 lines
8.6 KiB
TypeScript
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}`;
|
|
}
|
|
}
|