From bcb6bc8dd213880988d4f860da6125ef5407abc8 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Thu, 18 Jun 2026 22:25:00 +0200 Subject: [PATCH 1/6] fix herbatika lint --- apps/herbatika/package.json | 2 +- .../storefront-auth/register/register-flow.ts | 185 ++++++++++++++++++ .../app/api/storefront-auth/register/route.ts | 166 ++++------------ .../auth/register-account-type-field.tsx | 2 +- .../src/components/auth/register-form.tsx | 5 +- .../components/auth/use-auth-controller.ts | 4 +- .../components/checkout/checkout.constants.ts | 13 +- .../reviews/product-review-errors.ts | 66 +++++-- .../src/lib/auth/auth-form-validators.ts | 8 +- .../components/molecules/_herbatika-toast.css | 6 +- docker/development/herbatika/Dockerfile | 6 +- 11 files changed, 287 insertions(+), 176 deletions(-) create mode 100644 apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts diff --git a/apps/herbatika/package.json b/apps/herbatika/package.json index 02ed2135e..f1e7cee69 100644 --- a/apps/herbatika/package.json +++ b/apps/herbatika/package.json @@ -1,5 +1,5 @@ { - "name": "herbatica", + "name": "herbatika", "version": "0.1.0", "private": true, "scripts": { diff --git a/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts b/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts new file mode 100644 index 000000000..7c7ffe7b2 --- /dev/null +++ b/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts @@ -0,0 +1,185 @@ +import type { HttpTypes } from "@medusajs/types" +import { + buildErrorResponse, + buildMedusaUrl, + conflict, + getPublishableHeaders, + isConflictStatus, + parseResponseJson, + serverError, +} from "../_lib" +import { + createWholesaleCompanyRequest, + type ParsedWholesaleRegistration, +} from "./wholesale" + +export type ParsedRegisterPayload = { + email: string + password: string + firstName?: string + lastName?: string + wholesale: ParsedWholesaleRegistration | null +} + +export const refreshCustomerToken = async (loginToken: string) => { + const refreshResponse = await fetch(buildMedusaUrl("/auth/token/refresh"), { + method: "POST", + headers: { + authorization: `Bearer ${loginToken}`, + }, + cache: "no-store", + }) + + if (!refreshResponse.ok) { + return loginToken + } + + const refreshPayload = await parseResponseJson(refreshResponse) + return refreshPayload && typeof refreshPayload.token === "string" + ? refreshPayload.token + : loginToken +} + +export const createCustomerIdentity = async ({ + email, + password, + wholesale, +}: Pick) => { + const registerResponse = await fetch( + buildMedusaUrl("/auth/customer/emailpass/register"), + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + email, + password, + }), + cache: "no-store", + } + ) + + const registerConflict = isConflictStatus(registerResponse.status) + if (!(registerResponse.ok || registerConflict)) { + return buildErrorResponse(registerResponse) + } + + if (registerConflict && wholesale) { + return conflict( + "Účet s týmto e-mailom už existuje. Prihláste sa a požiadajte o VO účet cez podporu." + ) + } + + return null +} + +export const loginCustomerIdentity = async ({ + email, + password, +}: Pick) => { + const loginResponse = await fetch( + buildMedusaUrl("/auth/customer/emailpass"), + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + email, + password, + }), + cache: "no-store", + } + ) + + if (!loginResponse.ok) { + return { + error: await buildErrorResponse(loginResponse), + token: null, + } + } + + const loginPayload = await parseResponseJson(loginResponse) + const loginToken = + loginPayload && typeof loginPayload.token === "string" + ? loginPayload.token + : null + + if (!loginToken) { + return { + error: serverError( + "Prihlásenie zákazníka prebehlo úspešne, ale token nebol vrátený." + ), + token: null, + } + } + + return { + error: null, + token: loginToken, + } +} + +const buildCustomerProfile = ({ + email, + firstName, + lastName, + wholesale, +}: Omit): HttpTypes.StoreCreateCustomer => ({ + email, + first_name: firstName, + last_name: lastName, + ...(wholesale + ? { + company_name: wholesale.companyName, + metadata: { + company_identifier: wholesale.companyIdentifier, + }, + } + : {}), +}) + +export const createCustomerProfile = async ({ + loginToken, + payload, +}: { + loginToken: string + payload: Omit +}) => { + const createCustomerResponse = await fetch( + buildMedusaUrl("/store/customers"), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${loginToken}`, + ...getPublishableHeaders(), + }, + body: JSON.stringify(buildCustomerProfile(payload)), + cache: "no-store", + } + ) + + const customerConflict = isConflictStatus(createCustomerResponse.status) + return createCustomerResponse.ok || customerConflict + ? null + : buildErrorResponse(createCustomerResponse) +} + +export const createWholesaleProfile = ({ + email, + sessionToken, + wholesale, +}: { + email: string + sessionToken: string + wholesale: ParsedWholesaleRegistration | null +}) => + wholesale + ? createWholesaleCompanyRequest({ + email, + token: sessionToken, + wholesale, + }) + : null diff --git a/apps/herbatika/src/app/api/storefront-auth/register/route.ts b/apps/herbatika/src/app/api/storefront-auth/register/route.ts index a1254af5f..0a890bf51 100644 --- a/apps/herbatika/src/app/api/storefront-auth/register/route.ts +++ b/apps/herbatika/src/app/api/storefront-auth/register/route.ts @@ -1,21 +1,14 @@ -import type { HttpTypes } from "@medusajs/types" import { NextResponse } from "next/server" -import { - badRequest, - buildErrorResponse, - buildMedusaUrl, - conflict, - getPublishableHeaders, - isConflictStatus, - parseResponseJson, - serverError, - setSessionTokenCookie, -} from "../_lib" +import { badRequest, serverError, setSessionTokenCookie } from "../_lib" import { asRecordOrUndefined, asStringOrUndefined } from "./parse-utils" import { - createWholesaleCompanyRequest, - parseWholesaleRegistration, -} from "./wholesale" + createCustomerIdentity, + createCustomerProfile, + createWholesaleProfile, + loginCustomerIdentity, + refreshCustomerToken, +} from "./register-flow" +import { parseWholesaleRegistration } from "./wholesale" type RegisterBody = { email?: string @@ -83,25 +76,6 @@ const parseRegisterBody = async (request: Request) => { } } -const refreshCustomerToken = async (loginToken: string) => { - const refreshResponse = await fetch(buildMedusaUrl("/auth/token/refresh"), { - method: "POST", - headers: { - authorization: `Bearer ${loginToken}`, - }, - cache: "no-store", - }) - - if (!refreshResponse.ok) { - return loginToken - } - - const refreshPayload = await parseResponseJson(refreshResponse) - return refreshPayload && typeof refreshPayload.token === "string" - ? refreshPayload.token - : loginToken -} - export async function POST(request: Request) { try { const parsedBody = await parseRegisterBody(request) @@ -109,110 +83,42 @@ export async function POST(request: Request) { return parsedBody.error } - const { email, firstName, lastName, password, wholesale } = - parsedBody.value - const registerResponse = await fetch( - buildMedusaUrl("/auth/customer/emailpass/register"), - { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - email, - password, - }), - cache: "no-store", - } - ) - - const registerConflict = isConflictStatus(registerResponse.status) - if (!(registerResponse.ok || registerConflict)) { - return buildErrorResponse(registerResponse) - } - - if (registerConflict && wholesale) { - return conflict( - "Účet s týmto e-mailom už existuje. Prihláste sa a požiadajte o VO účet cez podporu." - ) - } - - const loginResponse = await fetch( - buildMedusaUrl("/auth/customer/emailpass"), - { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - email, - password, - }), - cache: "no-store", - } - ) - - if (!loginResponse.ok) { - return buildErrorResponse(loginResponse) - } - - const loginPayload = await parseResponseJson(loginResponse) - const loginToken = - loginPayload && typeof loginPayload.token === "string" - ? loginPayload.token - : null - - if (!loginToken) { - return serverError( - "Prihlásenie zákazníka prebehlo úspešne, ale token nebol vrátený." - ) - } - - const customerProfile: HttpTypes.StoreCreateCustomer = { + const { email, firstName, lastName, password, wholesale } = parsedBody.value + const registerError = await createCustomerIdentity({ email, - first_name: firstName, - last_name: lastName, - ...(wholesale - ? { - company_name: wholesale.companyName, - metadata: { - company_identifier: wholesale.companyIdentifier, - }, - } - : {}), + password, + wholesale, + }) + if (registerError) { + return registerError } - const createCustomerResponse = await fetch( - buildMedusaUrl("/store/customers"), - { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${loginToken}`, - ...getPublishableHeaders(), - }, - body: JSON.stringify(customerProfile), - cache: "no-store", - } - ) - - const customerConflict = isConflictStatus(createCustomerResponse.status) - if (!(createCustomerResponse.ok || customerConflict)) { - return buildErrorResponse(createCustomerResponse) + const loginResult = await loginCustomerIdentity({ email, password }) + if (loginResult.error) { + return loginResult.error } - const sessionToken = await refreshCustomerToken(loginToken) - - if (wholesale) { - const companyError = await createWholesaleCompanyRequest({ + const customerError = await createCustomerProfile({ + loginToken: loginResult.token, + payload: { email, - token: sessionToken, + firstName, + lastName, wholesale, - }) + }, + }) + if (customerError) { + return customerError + } - if (companyError) { - return companyError - } + const sessionToken = await refreshCustomerToken(loginResult.token) + const companyError = await createWholesaleProfile({ + email, + sessionToken, + wholesale, + }) + if (companyError) { + return companyError } return createRegisterResponse(sessionToken) diff --git a/apps/herbatika/src/components/auth/register-account-type-field.tsx b/apps/herbatika/src/components/auth/register-account-type-field.tsx index 536dec969..f7abf64fb 100644 --- a/apps/herbatika/src/components/auth/register-account-type-field.tsx +++ b/apps/herbatika/src/components/auth/register-account-type-field.tsx @@ -35,6 +35,7 @@ export function RegisterAccountTypeField({ > {(field) => ( )} diff --git a/apps/herbatika/src/components/auth/register-form.tsx b/apps/herbatika/src/components/auth/register-form.tsx index 931d410b8..2449bfad1 100644 --- a/apps/herbatika/src/components/auth/register-form.tsx +++ b/apps/herbatika/src/components/auth/register-form.tsx @@ -104,10 +104,7 @@ export const RegisterForm = ({ {isWholesaleAccount ? ( - + ) : null} diff --git a/apps/herbatika/src/components/auth/use-auth-controller.ts b/apps/herbatika/src/components/auth/use-auth-controller.ts index 40ef8da32..caf79f186 100644 --- a/apps/herbatika/src/components/auth/use-auth-controller.ts +++ b/apps/herbatika/src/components/auth/use-auth-controller.ts @@ -12,9 +12,9 @@ import { } from "@/components/auth/auth-helpers" import { useRegisterCountryItems } from "@/components/auth/use-register-country-items" import { + isWholesaleRegistration, type LoginFormValues, type RegisterFormValues, - isWholesaleRegistration, resolveLoginSubmitError, resolveRegisterSubmitError, } from "@/lib/auth/auth-form-validators" @@ -26,8 +26,8 @@ import { useTransferCart, } from "@/lib/storefront/cart" import { cartStorage } from "@/lib/storefront/cart-storage" -import { resolveRegionCurrency } from "@/lib/storefront/region-selection" import { resolveErrorMessage } from "@/lib/storefront/error-utils" +import { resolveRegionCurrency } from "@/lib/storefront/region-selection" import { useLogoutAction } from "@/lib/storefront/use-logout-action" type AuthControlsMode = "login" | "register" diff --git a/apps/herbatika/src/components/checkout/checkout.constants.ts b/apps/herbatika/src/components/checkout/checkout.constants.ts index 198dfadc3..11002c6bb 100644 --- a/apps/herbatika/src/components/checkout/checkout.constants.ts +++ b/apps/herbatika/src/components/checkout/checkout.constants.ts @@ -1,11 +1,14 @@ import type { CheckoutAddressValues } from "@/lib/forms/checkout/address.form" - -export { - COUNTRY_SELECT_ITEMS, - isCountryAvailableForRegion as isCheckoutCountryAvailableForRegion, - resolveCountryItemsForRegion as resolveCheckoutCountryItemsForRegion, +import { + COUNTRY_SELECT_ITEMS as FORM_COUNTRY_SELECT_ITEMS, + isCountryAvailableForRegion, + resolveCountryItemsForRegion, } from "@/lib/forms/country-options" +export const COUNTRY_SELECT_ITEMS = FORM_COUNTRY_SELECT_ITEMS +export const isCheckoutCountryAvailableForRegion = isCountryAvailableForRegion +export const resolveCheckoutCountryItemsForRegion = resolveCountryItemsForRegion + export type AddressFormState = CheckoutAddressValues export const CHECKOUT_STEPS = [ diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index 96e69860f..52e12732c 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -3,6 +3,7 @@ const GENERIC_REVIEW_SUBMIT_ERROR = "Recenziu sa nepodarilo odoslať. Skúste to prosím znova." const PURCHASE_REQUIRED_REVIEW_ERROR = "Na napísanie recenzie musíte mať tento produkt zakúpený." +const BAD_REQUEST_REVIEW_STATUSES = new Set([400, 422]) const hasErrorShape = ( error: unknown @@ -53,30 +54,32 @@ const isPurchaseRequiredReviewMessage = (normalizedMessage: string) => { ) } -export const resolveProductReviewSubmitErrorMessage = (error: unknown) => { - const message = extractErrorMessage(error) - const status = - hasErrorShape(error) && typeof error.status === "number" - ? error.status - : undefined - const normalizedMessage = message.toLowerCase() - - if (!message && status === undefined) { - return GENERIC_REVIEW_SUBMIT_ERROR - } - +const isDuplicateReviewMessage = (normalizedMessage: string) => + normalizedMessage.includes("already") || + normalizedMessage.includes("duplicate") || + normalizedMessage.includes("exist") || + normalizedMessage.includes("reviewed") + +const isDuplicateReviewError = ( + status: number | undefined, + normalizedMessage: string +) => status === 409 || isDuplicateReviewMessage(normalizedMessage) + +const resolveKnownReviewErrorMessage = ({ + message, + normalizedMessage, + status, +}: { + message: string + normalizedMessage: string + status: number | undefined +}) => { const tokenMessage = resolveTokenMessage(normalizedMessage) if (tokenMessage) { return tokenMessage } - if ( - status === 409 || - normalizedMessage.includes("already") || - normalizedMessage.includes("duplicate") || - normalizedMessage.includes("exist") || - normalizedMessage.includes("reviewed") - ) { + if (isDuplicateReviewError(status, normalizedMessage)) { return "Tento produkt ste už hodnotili." } @@ -92,7 +95,7 @@ export const resolveProductReviewSubmitErrorMessage = (error: unknown) => { return "Recenziu pre tento produkt momentálne nemôžete odoslať." } - if (status === 400 || status === 422) { + if (status && BAD_REQUEST_REVIEW_STATUSES.has(status)) { return message || "Skontrolujte prosím hodnotenie a text recenzie." } @@ -100,5 +103,26 @@ export const resolveProductReviewSubmitErrorMessage = (error: unknown) => { return GENERIC_REVIEW_SUBMIT_ERROR } - return message || GENERIC_REVIEW_SUBMIT_ERROR + return null +} + +export const resolveProductReviewSubmitErrorMessage = (error: unknown) => { + const message = extractErrorMessage(error) + const status = + hasErrorShape(error) && typeof error.status === "number" + ? error.status + : undefined + const normalizedMessage = message.toLowerCase() + + if (!message && status === undefined) { + return GENERIC_REVIEW_SUBMIT_ERROR + } + + const knownMessage = resolveKnownReviewErrorMessage({ + message, + normalizedMessage, + status, + }) + + return knownMessage || message || GENERIC_REVIEW_SUBMIT_ERROR } diff --git a/apps/herbatika/src/lib/auth/auth-form-validators.ts b/apps/herbatika/src/lib/auth/auth-form-validators.ts index 6422efc50..6082e0a90 100644 --- a/apps/herbatika/src/lib/auth/auth-form-validators.ts +++ b/apps/herbatika/src/lib/auth/auth-form-validators.ts @@ -1,6 +1,4 @@ -import { - checkoutAddressFieldValidators, -} from "@/lib/forms/checkout/address-validators" +import { checkoutAddressFieldValidators } from "@/lib/forms/checkout/address-validators" import { createChangeBlurContextualFieldValidators, createChangeBlurFieldValidators, @@ -60,9 +58,7 @@ export const isWholesaleRegistration = (values: RegisterFormValues) => values.account_type === "wholesale" const validateRegisterAccountType = (value: string) => - value === "retail" || value === "wholesale" - ? undefined - : "Vyberte typ účtu." + value === "retail" || value === "wholesale" ? undefined : "Vyberte typ účtu." const createWholesaleFieldValidators = ( validator: (value: string) => string | undefined diff --git a/apps/herbatika/src/styles/tokens/components/molecules/_herbatika-toast.css b/apps/herbatika/src/styles/tokens/components/molecules/_herbatika-toast.css index 4e3c63e1a..1dc1f5a8f 100644 --- a/apps/herbatika/src/styles/tokens/components/molecules/_herbatika-toast.css +++ b/apps/herbatika/src/styles/tokens/components/molecules/_herbatika-toast.css @@ -1,4 +1,4 @@ @theme static { - --color-toast-fg: white; - --color-toast-close-fg: white; -} \ No newline at end of file + --color-toast-fg: white; + --color-toast-close-fg: white; +} diff --git a/docker/development/herbatika/Dockerfile b/docker/development/herbatika/Dockerfile index 512fde550..ec006ff46 100644 --- a/docker/development/herbatika/Dockerfile +++ b/docker/development/herbatika/Dockerfile @@ -22,7 +22,7 @@ COPY libs/storefront-data/package.json libs/storefront-data/package.json RUN --mount=type=cache,id=pnpm-store-herbatika,target=/pnpm/store \ corepack install && \ - pnpm --filter=herbatica... fetch --store-dir=/pnpm/store --frozen-lockfile + pnpm --filter=herbatika... fetch --store-dir=/pnpm/store --frozen-lockfile COPY tsconfig.base.json tsconfig.json nx.json ./ COPY apps/herbatika ./apps/herbatika @@ -54,13 +54,13 @@ ENV MEILISEARCH_CATEGORIES_INDEX=${MEILISEARCH_CATEGORIES_INDEX} ENV MEILISEARCH_PRODUCERS_INDEX=${MEILISEARCH_PRODUCERS_INDEX} RUN --mount=type=cache,id=pnpm-store-herbatika,target=/pnpm/store \ - pnpm install --store-dir=/pnpm/store --prefer-offline --frozen-lockfile --ignore-scripts --filter=herbatica... + pnpm install --store-dir=/pnpm/store --prefer-offline --frozen-lockfile --ignore-scripts --filter=herbatika... RUN pnpm --filter=@techsio/storefront-data build RUN pnpm --filter=@techsio/ui-kit build RUN --mount=type=cache,id=next-cache-herbatika,target=/var/www/apps/herbatika/.next/cache \ - pnpm --filter=herbatica exec next build --webpack && \ + pnpm --filter=herbatika exec next build --webpack && \ test -f apps/herbatika/.next/BUILD_ID FROM node:22-slim AS prod From 046018b7962ce3e3e55b8dd4259998719f3aa968 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Fri, 19 Jun 2026 12:04:59 +0200 Subject: [PATCH 2/6] address herbatika review comments --- .../storefront-auth/register/register-flow.ts | 2 +- .../reviews/product-review-errors.ts | 45 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts b/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts index 7c7ffe7b2..db7b40047 100644 --- a/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts +++ b/apps/herbatika/src/app/api/storefront-auth/register/register-flow.ts @@ -167,7 +167,7 @@ export const createCustomerProfile = async ({ : buildErrorResponse(createCustomerResponse) } -export const createWholesaleProfile = ({ +export const createWholesaleProfile = async ({ email, sessionToken, wholesale, diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index 52e12732c..8b7d2fbfe 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -3,7 +3,35 @@ const GENERIC_REVIEW_SUBMIT_ERROR = "Recenziu sa nepodarilo odoslať. Skúste to prosím znova." const PURCHASE_REQUIRED_REVIEW_ERROR = "Na napísanie recenzie musíte mať tento produkt zakúpený." +const REVIEW_VALIDATION_ERROR = + "Skontrolujte prosím hodnotenie a text recenzie." const BAD_REQUEST_REVIEW_STATUSES = new Set([400, 422]) +const DUPLICATE_REVIEW_MESSAGE_PATTERNS = [ + "already reviewed", + "already submitted", + "already exists", + "duplicate review", + "review already exists", + "reviewed this product", +] as const +const REVIEW_VALIDATION_MESSAGE_RULES = [ + { + patterns: ["rating"], + message: "Vyberte prosím hodnotenie.", + }, + { + patterns: ["content"], + message: "Napíšte prosím text recenzie.", + }, + { + patterns: ["text"], + message: "Napíšte prosím text recenzie.", + }, + { + patterns: ["title"], + message: "Skontrolujte prosím nadpis recenzie.", + }, +] as const const hasErrorShape = ( error: unknown @@ -55,22 +83,24 @@ const isPurchaseRequiredReviewMessage = (normalizedMessage: string) => { } const isDuplicateReviewMessage = (normalizedMessage: string) => - normalizedMessage.includes("already") || - normalizedMessage.includes("duplicate") || - normalizedMessage.includes("exist") || - normalizedMessage.includes("reviewed") + DUPLICATE_REVIEW_MESSAGE_PATTERNS.some((pattern) => + normalizedMessage.includes(pattern) + ) const isDuplicateReviewError = ( status: number | undefined, normalizedMessage: string ) => status === 409 || isDuplicateReviewMessage(normalizedMessage) +const resolveReviewValidationMessage = (normalizedMessage: string) => + REVIEW_VALIDATION_MESSAGE_RULES.find(({ patterns }) => + patterns.every((pattern) => normalizedMessage.includes(pattern)) + )?.message ?? REVIEW_VALIDATION_ERROR + const resolveKnownReviewErrorMessage = ({ - message, normalizedMessage, status, }: { - message: string normalizedMessage: string status: number | undefined }) => { @@ -96,7 +126,7 @@ const resolveKnownReviewErrorMessage = ({ } if (status && BAD_REQUEST_REVIEW_STATUSES.has(status)) { - return message || "Skontrolujte prosím hodnotenie a text recenzie." + return resolveReviewValidationMessage(normalizedMessage) } if (status && status >= 500) { @@ -119,7 +149,6 @@ export const resolveProductReviewSubmitErrorMessage = (error: unknown) => { } const knownMessage = resolveKnownReviewErrorMessage({ - message, normalizedMessage, status, }) From 4460d6a5e66d4a9d823c15e0ffa4f6953f7da646 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Fri, 19 Jun 2026 12:20:39 +0200 Subject: [PATCH 3/6] address review error edge cases --- .../src/components/reviews/product-review-errors.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index 8b7d2fbfe..aa1b04127 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -8,10 +8,14 @@ const REVIEW_VALIDATION_ERROR = const BAD_REQUEST_REVIEW_STATUSES = new Set([400, 422]) const DUPLICATE_REVIEW_MESSAGE_PATTERNS = [ "already reviewed", + "already rated", "already submitted", "already exists", + "duplicate entry", "duplicate review", "review already exists", + "review already submitted", + "review exists", "reviewed this product", ] as const const REVIEW_VALIDATION_MESSAGE_RULES = [ @@ -113,14 +117,14 @@ const resolveKnownReviewErrorMessage = ({ return "Tento produkt ste už hodnotili." } - if (status === 401) { - return "Pre odoslanie recenzie sa prosím prihláste." - } - if (isPurchaseRequiredReviewMessage(normalizedMessage)) { return PURCHASE_REQUIRED_REVIEW_ERROR } + if (status === 401) { + return "Pre odoslanie recenzie sa prosím prihláste." + } + if (status === 403) { return "Recenziu pre tento produkt momentálne nemôžete odoslať." } From 886df131475672f1ffba7c4f21349d9d1d0fab10 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Fri, 19 Jun 2026 16:44:50 +0200 Subject: [PATCH 4/6] clarify review error matching --- .../reviews/product-review-errors.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index aa1b04127..d4869e1db 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -6,17 +6,14 @@ const PURCHASE_REQUIRED_REVIEW_ERROR = const REVIEW_VALIDATION_ERROR = "Skontrolujte prosím hodnotenie a text recenzie." const BAD_REQUEST_REVIEW_STATUSES = new Set([400, 422]) -const DUPLICATE_REVIEW_MESSAGE_PATTERNS = [ - "already reviewed", - "already rated", - "already submitted", - "already exists", - "duplicate entry", - "duplicate review", - "review already exists", - "review already submitted", - "review exists", - "reviewed this product", +const DUPLICATE_REVIEW_MESSAGE_RULES = [ + ["already", "review"], + ["already", "rated"], + ["already", "submitted"], + ["already", "exists"], + ["duplicate"], + ["review", "exists"], + ["reviewed", "product"], ] as const const REVIEW_VALIDATION_MESSAGE_RULES = [ { @@ -87,8 +84,8 @@ const isPurchaseRequiredReviewMessage = (normalizedMessage: string) => { } const isDuplicateReviewMessage = (normalizedMessage: string) => - DUPLICATE_REVIEW_MESSAGE_PATTERNS.some((pattern) => - normalizedMessage.includes(pattern) + DUPLICATE_REVIEW_MESSAGE_RULES.some((patterns) => + patterns.every((pattern) => normalizedMessage.includes(pattern)) ) const isDuplicateReviewError = ( @@ -96,6 +93,7 @@ const isDuplicateReviewError = ( normalizedMessage: string ) => status === 409 || isDuplicateReviewMessage(normalizedMessage) +// Multi-pattern validation rules intentionally use AND semantics. const resolveReviewValidationMessage = (normalizedMessage: string) => REVIEW_VALIDATION_MESSAGE_RULES.find(({ patterns }) => patterns.every((pattern) => normalizedMessage.includes(pattern)) @@ -117,6 +115,7 @@ const resolveKnownReviewErrorMessage = ({ return "Tento produkt ste už hodnotili." } + // Preserve the original resolver precedence for backend purchase-required copy. if (isPurchaseRequiredReviewMessage(normalizedMessage)) { return PURCHASE_REQUIRED_REVIEW_ERROR } From 98c47f2cb496955bdaa78d36f279f92c10ad4043 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Fri, 19 Jun 2026 17:05:18 +0200 Subject: [PATCH 5/6] align review error precedence --- .../reviews/product-review-errors.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index d4869e1db..806e576f0 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -6,6 +6,13 @@ const PURCHASE_REQUIRED_REVIEW_ERROR = const REVIEW_VALIDATION_ERROR = "Skontrolujte prosím hodnotenie a text recenzie." const BAD_REQUEST_REVIEW_STATUSES = new Set([400, 422]) +// Broad duplicate keywords are skipped for validation statuses below. +const BROAD_DUPLICATE_REVIEW_MESSAGE_PATTERNS = [ + "already", + "duplicate", + "exist", + "reviewed", +] as const const DUPLICATE_REVIEW_MESSAGE_RULES = [ ["already", "review"], ["already", "rated"], @@ -83,15 +90,30 @@ const isPurchaseRequiredReviewMessage = (normalizedMessage: string) => { ) } -const isDuplicateReviewMessage = (normalizedMessage: string) => +const isSpecificDuplicateReviewMessage = (normalizedMessage: string) => DUPLICATE_REVIEW_MESSAGE_RULES.some((patterns) => patterns.every((pattern) => normalizedMessage.includes(pattern)) ) +const isBroadDuplicateReviewMessage = (normalizedMessage: string) => + BROAD_DUPLICATE_REVIEW_MESSAGE_PATTERNS.some((pattern) => + normalizedMessage.includes(pattern) + ) + const isDuplicateReviewError = ( status: number | undefined, normalizedMessage: string -) => status === 409 || isDuplicateReviewMessage(normalizedMessage) +) => { + if (status === 409 || isSpecificDuplicateReviewMessage(normalizedMessage)) { + return true + } + + if (status && BAD_REQUEST_REVIEW_STATUSES.has(status)) { + return false + } + + return isBroadDuplicateReviewMessage(normalizedMessage) +} // Multi-pattern validation rules intentionally use AND semantics. const resolveReviewValidationMessage = (normalizedMessage: string) => @@ -115,15 +137,14 @@ const resolveKnownReviewErrorMessage = ({ return "Tento produkt ste už hodnotili." } - // Preserve the original resolver precedence for backend purchase-required copy. - if (isPurchaseRequiredReviewMessage(normalizedMessage)) { - return PURCHASE_REQUIRED_REVIEW_ERROR - } - if (status === 401) { return "Pre odoslanie recenzie sa prosím prihláste." } + if (isPurchaseRequiredReviewMessage(normalizedMessage)) { + return PURCHASE_REQUIRED_REVIEW_ERROR + } + if (status === 403) { return "Recenziu pre tento produkt momentálne nemôžete odoslať." } From e213b024c81cd3f3f150031b78c633ec44ed8a84 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Fri, 19 Jun 2026 17:13:54 +0200 Subject: [PATCH 6/6] fix review error precedence --- .../src/components/reviews/product-review-errors.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/herbatika/src/components/reviews/product-review-errors.ts b/apps/herbatika/src/components/reviews/product-review-errors.ts index 806e576f0..d19c15856 100644 --- a/apps/herbatika/src/components/reviews/product-review-errors.ts +++ b/apps/herbatika/src/components/reviews/product-review-errors.ts @@ -133,7 +133,7 @@ const resolveKnownReviewErrorMessage = ({ return tokenMessage } - if (isDuplicateReviewError(status, normalizedMessage)) { + if (status === 409) { return "Tento produkt ste už hodnotili." } @@ -141,12 +141,16 @@ const resolveKnownReviewErrorMessage = ({ return "Pre odoslanie recenzie sa prosím prihláste." } + if (status === 403) { + return "Recenziu pre tento produkt momentálne nemôžete odoslať." + } + if (isPurchaseRequiredReviewMessage(normalizedMessage)) { return PURCHASE_REQUIRED_REVIEW_ERROR } - if (status === 403) { - return "Recenziu pre tento produkt momentálne nemôžete odoslať." + if (isDuplicateReviewError(status, normalizedMessage)) { + return "Tento produkt ste už hodnotili." } if (status && BAD_REQUEST_REVIEW_STATUSES.has(status)) {