Skip to content

feat(web): add product listing variants pricing and size guide#303

Merged
onerandomdevv merged 1 commit into
devfrom
feat/web-product-listing-3-4
May 21, 2026
Merged

feat(web): add product listing variants pricing and size guide#303
onerandomdevv merged 1 commit into
devfrom
feat/web-product-listing-3-4

Conversation

@onerandomdevv
Copy link
Copy Markdown
Collaborator

@onerandomdevv onerandomdevv commented May 21, 2026

What does this PR do?

This PR implements W-15b: Product Listing Form Screens 3 + 4. It continues the existing product listing wizard from W-15a by adding variant generation, pricing, stock input, volume tiers, not-sold-individually rules, physical-store dropship gating, size guide preview, custom size rows, model measurements, and final Publish Now / Save as Draft submission. The payload helper maps the form data to the current B-11/B-12 backend product contract and submits through POST /products.

Type of change

  • New feature
  • Bug fix
  • Refactor / cleanup
  • Database migration included
  • Chore / maintenance
  • Documentation

Area affected

  • Backend
  • Web
  • WhatsApp
  • Shared package
  • Database / Prisma
  • GitHub / CI / infrastructure

How to test this

  1. Checkout the branch:

    git checkout feat/web-product-listing-3-4```
    
  2. Run web checks:
    cd apps/web
    pnpm run lint
    npx tsc --noEmit
    pnpm run build

  3. Test the product listing wizard:

  • Open /store/products/new
  • Complete Screens 1 and 2 from the existing W-15a flow
  • Continue to Screen 3
  • Toggle variants on/off
  • Add Dimension 1 and Dimension 2 values
  • Confirm variant combinations auto-generate
  • Enter prices and stock values
  • Use “Pre-fill all prices”
  • Add volume pricing tiers
  • Toggle “Not sold individually”
  • Confirm dropship settings only show for physical-store context
  • Continue to Screen 4
  • Confirm size guide preview renders from selected subcategory
  • Toggle custom sizing
  • Add model measurements
  • Test Publish Now and Save as Draft

Expected result: Screens 3 and 4 work inside the existing product listing wizard, generated variants map correctly to stock/pricing payloads, money values are sent as integer kobo, no fabricated stock defaults are used, and successful publish/draft redirects to /store/products.

Notes for reviewer

Implemented:

  • Screen 3 variants, pricing, stock, volume tiers, not-sold-individually, and physical-store dropship gating
  • Screen 4 size guide preview, custom size rows, model measurements, Publish Now, and Save as Draft
  • Payload helper for the current B-11/B-12 backend contract
  • Submission through POST /products
  • Internal GET /stores/me usage only to decide whether to show dropship settings

No backend contract mismatch was found.

No backend or schema changes were needed.

Summary by CodeRabbit

  • New Features
    • Product creation workflow expanded to include variants and pricing configuration
    • Support for volume-based pricing tiers and discount management
    • Custom sizing options with validation for clothing and footwear measurements
    • Improved form validation and inline error feedback throughout product setup

Review Change Stack

@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented May 21, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR completes the product creation wizard (steps 2–4) by implementing a variants/pricing screen, a size guide customization and publish screen, full payload validation and building, and orchestrating the multi-step flow in the main page component.

Changes

Product Creation Wizard

Layer / File(s) Summary
Product listing type system and parsing helpers
apps/web/src/lib/product-listing.ts
Extended ProductListingDraft with pricing and sizeGuide fields; introduced dimension/variant/tier/payload types (DimensionOneType, VariantDraftRow, ProductListingPricing, CustomSizeRow, ProductCreatePayload, etc.); added export helpers emptyPricing()/emptySizeGuide(); added numeric conversion utilities nairaToKoboString(), parseWholeNumber(), calculateDiscountPercent().
Variants and pricing configuration screen
apps/web/src/app/(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx
Implemented ScreenThreeVariantsPricing component to configure dimensions, variants, retail/compare-at pricing, stock, volume tiers, and optional dropship sourcing. Includes DimensionBuilder subcomponent for adding/removing dimension values, syncVariantRows() to regenerate variant grid from dimensions, and validatePricing() to enforce pricing ordering, variant price/stock rules, volume tier constraints, and dropship floor-price rules.
Size guide customization and publish screen
apps/web/src/app/(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx
Implemented ScreenFourSizeGuidePublish component to select sizing guide, toggle between standard and custom sizing modes, add/remove/edit custom size rows with footwear or clothing measurement fields, and validate all size fields. Includes getSizeOptions() to derive available sizes from pricing variants or hardcoded ranges, validateCustomSizes() and validateModelMeasurements() for field validation, and helper components.
Product payload building and API submission
apps/web/src/lib/product-listing.ts
Implemented buildProductCreatePayload(draft, publishNow) to validate all required fields, convert prices to kobo via BigInt, filter approved/sensitive photos with Cloudinary URLs, construct conditional payload sections for SKU, compare-at, variants, volume tiers, not-sold-individually rules, and dropship options, and return validation result. Added createProduct(payload) to POST to /products endpoint. Includes internal helpers for variant overrides and size guide configuration building.
Wizard page state and navigation
apps/web/src/app/(store)/store/products/new/page.tsx
Updated NewProductPage to manage draft state across all steps (details, pricing, sizeGuide), implement photo lifecycle with URL revocation on unmount, handle step transitions via onBack/onNext, and wire screen components with onChange callbacks. Implemented handleSubmit(publishNow) to validate payload, call createProduct, navigate to /store/products on success, and display error messages on failure.

Sequence Diagram

sequenceDiagram
  participant User as User
  participant Page as NewProductPage
  participant Screen3 as ScreenThreeVariantsPricing
  participant Screen4 as ScreenFourSizeGuidePublish
  participant Builder as buildProductCreatePayload
  participant API as createProduct
  participant Server as /products
  
  User->>Page: load product wizard
  Page->>Screen3: render with pricing draft
  User->>Screen3: configure dimensions, variants, pricing
  Screen3->>Screen3: syncVariantRows() on dimension change
  Screen3->>Screen3: validatePricing() on continue
  Screen3->>Page: onNext() if valid
  Page->>Screen4: render with sizeGuide draft
  User->>Screen4: choose sizing guide, add custom sizes
  Screen4->>Screen4: validateCustomSizes() on field edit
  User->>Screen4: click Publish Now
  Screen4->>Page: onSubmit(true)
  Page->>Builder: buildProductCreatePayload(draft, true)
  Builder->>Builder: convert prices to kobo, normalize sizes
  Builder-->>Page: {payload, errors}
  Page->>API: createProduct(payload)
  API->>Server: POST /products
  Server-->>API: {id, ...}
  API-->>User: redirect to /store/products
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • coded-devs/twizrr#299: Backend PR that introduces variantOptions, variantOverrides, and sizeGuideConfig DTO fields; directly consumed by the main PR's buildProductCreatePayload() payload construction.
  • coded-devs/twizrr#300: Earlier PR that establishes the product listing wizard foundation (Screens 1–2, draft/photos/details types, uploadProductImage); main PR extends that same draft model with pricing and size guide fields.

Poem

🐰 The wizard hops forth, step by step so true,
Variants dance, pricing pays what is due,
Sizes are measured, both custom and neat,
From kobo conversions to pushes complete—
A product takes shape, the magic is sweet! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main addition: product listing variants pricing and size guide features (Screens 3-4).
Description check ✅ Passed The PR description is comprehensive and well-structured, covering what was done, type of change, affected areas, detailed testing instructions, and important implementation notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-product-listing-3-4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
apps/web/src/app/(store)/store/products/new/page.tsx (1)

76-90: ⚡ Quick win

Prefer TanStack Query useMutation for the submit flow.

This keeps server-state behavior centralized and aligned with the project’s React Query standard instead of manual async + local loading/error orchestration.

As per coding guidelines, "Use React Query (TanStack Query v5) for server state management with centralized queryKeys".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/`(store)/store/products/new/page.tsx around lines 76 - 90,
The submit flow in handleSubmit currently uses manual async/try-catch and local
loading/error state; refactor it to use TanStack Query's useMutation so
server-state and side effects are centralized. Replace the direct call to
createProduct and local setSubmitting/setSubmitErrors handling by creating a
mutation (using createProduct as the mutation function) and invoke
mutation.mutateAsync with the payload generated by buildProductCreatePayload;
handle success by calling router.push("/store/products") in onSuccess and map
errors to setSubmitErrors or use mutation.error in the UI, while keeping
buildProductCreatePayload, getSubmitErrorMessage, and existing state setters for
compatibility during validation and error display. Ensure the mutation is keyed
appropriately per project conventions and use mutation.isLoading instead of
setSubmitting to reflect publish vs draft status.
apps/web/src/app/(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx (2)

45-60: 🏗️ Heavy lift

Use React Query for /stores/me instead of imperative useEffect fetch.

This fetch should be moved to TanStack Query with centralized query keys per project standard.

As per coding guidelines, "Use React Query (TanStack Query v5) for server state management with centralized queryKeys".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx
around lines 45 - 60, Replace the imperative useEffect/api.get call in the
ScreenThreeVariantsPricing component with a TanStack useQuery that fetches
"/stores/me" using the centralized query key (e.g., queryKeys.store.me); update
the logic that setsSupportsDropship to derive its value from the query result
(setSupportsDropship(data?.storeType === "PHYSICAL" or false) or set local state
from useEffect on query success), handle errors via the query's error state
(defaulting supportsDropship to false), remove the mounted cleanup logic (not
needed with useQuery), and import/use the shared queryKeys and useQuery hook
instead of api.get and useEffect.

95-99: 🏗️ Heavy lift

Migrate pricing form validation to React Hook Form + Zod.

Validation is currently custom and split across handlers/helpers. This step should use RHF + Zod schemas.

As per coding guidelines, "All frontend form validation must use Zod schemas" and "Use React Hook Form with Zod for form validation".

Also applies to: 732-829

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx
around lines 95 - 99, Migrate the pricing validation in handleContinue to React
Hook Form + Zod: remove the call to validatePricing and the setErrors flow in
handleContinue and instead create a Zod schema (covering pricing,
supportsDropship and rules currently enforced by validatePricing), initialize
useForm({ resolver: zodResolver(yourSchema), defaultValues: pricing }) in this
component, replace manual field handling with RHF register/control for the
pricing inputs, wire the submit to handleSubmit(props => onNext()) so onNext is
only called when Zod validation passes, and delete or consolidate the
validatePricing function and errors state usage to rely on formState.errors from
RHF. Ensure symbols referenced are the existing handleContinue, validatePricing,
setErrors, onNext, pricing and supportsDropship so you update the correct logic.
apps/web/src/lib/product-listing.ts (1)

184-243: 🏗️ Heavy lift

Normalize ID and boolean field names to match project TS conventions.

Several newly introduced fields violate the repo naming contract (e.g., id should be ...Id, booleans should start with is/has/can/should). Please align these interfaces/payload shapes for consistency before this propagates further.

As per coding guidelines, "All ID field names must be suffixed with 'Id'" and "All boolean field names must be prefixed with 'is', 'has', 'can', or 'should' (e.g. isActive, hasVariants)".

Also applies to: 281-297

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/product-listing.ts` around lines 184 - 243, Update the
interface field names to follow project TS conventions: rename every plain "id"
field to a suffixed form like "variantId", "tierId", "customSizeId", or
"productId" in the affected interfaces (VariantDraftRow, VolumePricingTier,
CustomSizeRow, ProductCreateResponse) and rename boolean properties to start
with is/has/can/should (e.g., isActive, hasVariants, volumePricingOpen ->
isVolumePricingOpen, notSoldIndividually -> isNotSoldIndividually, allowDropship
-> canAllowDropship or isAllowDropship as your chosen convention); update any
related names in ProductListingPricing and ProductListingSizeGuide
(dimensionOneValues/dimensionTwoValues remain) to match those new identifiers so
types remain consistent across usages. Ensure you also change the corresponding
property names wherever these interfaces are consumed.
apps/web/src/app/(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx (1)

425-481: 🏗️ Heavy lift

Use RHF + Zod for size-guide validation instead of local validators.

This screen’s validation should be schema-based and integrated with React Hook Form per project standard.

As per coding guidelines, "All frontend form validation must use Zod schemas" and "Use React Hook Form with Zod for form validation".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx
around lines 425 - 481, Replace the ad-hoc validators (validateCustomSizes,
validateModelMeasurements, isOptionalNumber) with a Zod schema for
ProductListingSizeGuide and wire it into React Hook Form using the zod resolver:
define a schema that enforces useCustomSizes boolean, validates customSizes as
an array of objects where size is nonempty and numeric fields (bustCm, waistCm,
hipsCm, heightCm) are optional-but-numeric, and conditionally requires
footLengthCm when isFootwear is true; validate
modelHeightCm/modelBustCm/modelWaistCm/modelHipsCm as optional-but-numeric in
the same schema; then remove calls to
validateCustomSizes/validateModelMeasurements and instead pass the Zod schema to
useForm via zodResolver to drive all validation and error messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx:
- Around line 478-480: The inline validator isOptionalNumber currently treats
"0" as valid but the submit payload rejects non-positive values; update
isOptionalNumber to only accept empty strings or whole numbers greater than zero
by calling parseWholeNumber(value) and ensuring the result is not null and > 0
(retain the existing trim check). Reference the isOptionalNumber function and
parseWholeNumber when making this change so the screen-level validation matches
submit-time rules.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx:
- Line 777: The validation currently always runs validateVolumePricing(pricing,
errors) and thus validates pricing.volumeTiers even when volume pricing UI is
closed; update the logic so validateVolumePricing is only invoked when
pricing.volumePricingOpen is true (and similarly guard any other volume-tier
checks between lines handling volumeTiers, e.g. the block around the other
validations at 802-828), so hidden/disabled volume-tier data is skipped and
won't block continuation; locate calls to validateVolumePricing and any direct
checks of pricing.volumeTiers and wrap them with a condition like if
(pricing.volumePricingOpen) before running the validation.

In `@apps/web/src/app/`(store)/store/products/new/page.tsx:
- Around line 106-108: The page heading in the <h1> element in page.tsx is using
the Cabinet font and weight (class names font-cabinet and font-semibold); change
it to use the Syne 700 typography token by replacing font-cabinet and
font-semibold with the Syne font and weight 700 classes (e.g., font-syne and a
700 weight class such as font-[700] or font-extrabold) so the heading uses Syne
(700) per the apps/web typography rule.
- Around line 76-94: The handleSubmit function currently allows concurrent
POSTs; add an in-flight guard so repeated clicks won’t send duplicate
createProduct calls: at the top of handleSubmit check the submitting state (or a
dedicated ref like isSubmittingRef) and return early if a request is in-flight;
set the submitting state (setSubmitting or isSubmittingRef) immediately after
payload validation and before calling createProduct, then proceed with the
try/catch/finally as-is and clear the submitting flag in finally; reference
handleSubmit, setSubmitting (or an isSubmittingRef), buildProductCreatePayload,
createProduct, setSubmitErrors, and router.push when making these changes.

In `@apps/web/src/lib/product-listing.ts`:
- Around line 449-453: The payload builder currently calls
buildVolumePricingTiers and validates volume tiers unconditionally in
buildProductCreatePayload, which causes hidden/stale tiers to block submission;
change the logic to only call buildVolumePricingTiers and add
attributes.volumePricingTiers when the product's volumePricingOpen flag is true
(check the same gating where UI exposes volume pricing), and apply the same
guard in the other similar block around the code at the second occurrence (the
block spanning the area analogous to lines 578-610) so volume tier
parsing/validation is skipped when volumePricingOpen is false.

---

Nitpick comments:
In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx:
- Around line 425-481: Replace the ad-hoc validators (validateCustomSizes,
validateModelMeasurements, isOptionalNumber) with a Zod schema for
ProductListingSizeGuide and wire it into React Hook Form using the zod resolver:
define a schema that enforces useCustomSizes boolean, validates customSizes as
an array of objects where size is nonempty and numeric fields (bustCm, waistCm,
hipsCm, heightCm) are optional-but-numeric, and conditionally requires
footLengthCm when isFootwear is true; validate
modelHeightCm/modelBustCm/modelWaistCm/modelHipsCm as optional-but-numeric in
the same schema; then remove calls to
validateCustomSizes/validateModelMeasurements and instead pass the Zod schema to
useForm via zodResolver to drive all validation and error messages.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx:
- Around line 45-60: Replace the imperative useEffect/api.get call in the
ScreenThreeVariantsPricing component with a TanStack useQuery that fetches
"/stores/me" using the centralized query key (e.g., queryKeys.store.me); update
the logic that setsSupportsDropship to derive its value from the query result
(setSupportsDropship(data?.storeType === "PHYSICAL" or false) or set local state
from useEffect on query success), handle errors via the query's error state
(defaulting supportsDropship to false), remove the mounted cleanup logic (not
needed with useQuery), and import/use the shared queryKeys and useQuery hook
instead of api.get and useEffect.
- Around line 95-99: Migrate the pricing validation in handleContinue to React
Hook Form + Zod: remove the call to validatePricing and the setErrors flow in
handleContinue and instead create a Zod schema (covering pricing,
supportsDropship and rules currently enforced by validatePricing), initialize
useForm({ resolver: zodResolver(yourSchema), defaultValues: pricing }) in this
component, replace manual field handling with RHF register/control for the
pricing inputs, wire the submit to handleSubmit(props => onNext()) so onNext is
only called when Zod validation passes, and delete or consolidate the
validatePricing function and errors state usage to rely on formState.errors from
RHF. Ensure symbols referenced are the existing handleContinue, validatePricing,
setErrors, onNext, pricing and supportsDropship so you update the correct logic.

In `@apps/web/src/app/`(store)/store/products/new/page.tsx:
- Around line 76-90: The submit flow in handleSubmit currently uses manual
async/try-catch and local loading/error state; refactor it to use TanStack
Query's useMutation so server-state and side effects are centralized. Replace
the direct call to createProduct and local setSubmitting/setSubmitErrors
handling by creating a mutation (using createProduct as the mutation function)
and invoke mutation.mutateAsync with the payload generated by
buildProductCreatePayload; handle success by calling
router.push("/store/products") in onSuccess and map errors to setSubmitErrors or
use mutation.error in the UI, while keeping buildProductCreatePayload,
getSubmitErrorMessage, and existing state setters for compatibility during
validation and error display. Ensure the mutation is keyed appropriately per
project conventions and use mutation.isLoading instead of setSubmitting to
reflect publish vs draft status.

In `@apps/web/src/lib/product-listing.ts`:
- Around line 184-243: Update the interface field names to follow project TS
conventions: rename every plain "id" field to a suffixed form like "variantId",
"tierId", "customSizeId", or "productId" in the affected interfaces
(VariantDraftRow, VolumePricingTier, CustomSizeRow, ProductCreateResponse) and
rename boolean properties to start with is/has/can/should (e.g., isActive,
hasVariants, volumePricingOpen -> isVolumePricingOpen, notSoldIndividually ->
isNotSoldIndividually, allowDropship -> canAllowDropship or isAllowDropship as
your chosen convention); update any related names in ProductListingPricing and
ProductListingSizeGuide (dimensionOneValues/dimensionTwoValues remain) to match
those new identifiers so types remain consistent across usages. Ensure you also
change the corresponding property names wherever these interfaces are consumed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d022639e-4680-49d3-93b7-44e93e465876

📥 Commits

Reviewing files that changed from the base of the PR and between 1e19821 and ce86681.

📒 Files selected for processing (4)
  • apps/web/src/app/(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx
  • apps/web/src/app/(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx
  • apps/web/src/app/(store)/store/products/new/page.tsx
  • apps/web/src/lib/product-listing.ts

Comment on lines +478 to +480
function isOptionalNumber(value: string): boolean {
if (!value.trim()) return true;
return parseWholeNumber(value) !== null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inline numeric validation should require positive integers to match submit-time rules.

isOptionalNumber currently accepts 0, but payload construction rejects non-positive values, so users can pass this screen and fail at submit.

Suggested fix
 function isOptionalNumber(value: string): boolean {
   if (!value.trim()) return true;
-  return parseWholeNumber(value) !== null;
+  const parsed = parseWholeNumber(value);
+  return parsed !== null && parsed > 0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isOptionalNumber(value: string): boolean {
if (!value.trim()) return true;
return parseWholeNumber(value) !== null;
function isOptionalNumber(value: string): boolean {
if (!value.trim()) return true;
const parsed = parseWholeNumber(value);
return parsed !== null && parsed > 0;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx
around lines 478 - 480, The inline validator isOptionalNumber currently treats
"0" as valid but the submit payload rejects non-positive values; update
isOptionalNumber to only accept empty strings or whole numbers greater than zero
by calling parseWholeNumber(value) and ensuring the result is not null and > 0
(retain the existing trim check). Reference the isOptionalNumber function and
parseWholeNumber when making this change so the screen-level validation matches
submit-time rules.

errors.push("Stock must be a whole number.");
}

validateVolumePricing(pricing, errors);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip volume-tier validation when volume pricing is disabled.

You currently validate volumeTiers even when pricing.volumePricingOpen is false, which can block continue on hidden data.

Suggested fix
-  validateVolumePricing(pricing, errors);
+  if (pricing.volumePricingOpen) {
+    validateVolumePricing(pricing, errors);
+  }

Also applies to: 802-828

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx
at line 777, The validation currently always runs validateVolumePricing(pricing,
errors) and thus validates pricing.volumeTiers even when volume pricing UI is
closed; update the logic so validateVolumePricing is only invoked when
pricing.volumePricingOpen is true (and similarly guard any other volume-tier
checks between lines handling volumeTiers, e.g. the block around the other
validations at 802-828), so hidden/disabled volume-tier data is skipped and
won't block continuation; locate calls to validateVolumePricing and any direct
checks of pricing.volumeTiers and wrap them with a condition like if
(pricing.volumePricingOpen) before running the validation.

Comment on lines +76 to +94
const handleSubmit = async (publishNow: boolean) => {
setSubmitErrors([]);
const result = buildProductCreatePayload(draft, publishNow);

if (!result.payload) {
setSubmitErrors(result.errors);
return;
}

setSubmitting(publishNow ? "publish" : "draft");
try {
await createProduct(result.payload);
router.push("/store/products");
} catch (error) {
setSubmitErrors([getSubmitErrorMessage(error)]);
} finally {
setSubmitting(null);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an in-flight guard to prevent duplicate product creation requests.

createProduct is a non-idempotent POST. Rapid repeated submits can dispatch duplicate creates before UI disable state settles.

Suggested fix
   const photosRef = useRef(draft.photos);
+  const submitInFlightRef = useRef(false);
   photosRef.current = draft.photos;
@@
   const handleSubmit = async (publishNow: boolean) => {
+    if (submitInFlightRef.current) return;
     setSubmitErrors([]);
     const result = buildProductCreatePayload(draft, publishNow);
@@
+    submitInFlightRef.current = true;
     setSubmitting(publishNow ? "publish" : "draft");
     try {
       await createProduct(result.payload);
       router.push("/store/products");
@@
     } finally {
+      submitInFlightRef.current = false;
       setSubmitting(null);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/`(store)/store/products/new/page.tsx around lines 76 - 94,
The handleSubmit function currently allows concurrent POSTs; add an in-flight
guard so repeated clicks won’t send duplicate createProduct calls: at the top of
handleSubmit check the submitting state (or a dedicated ref like
isSubmittingRef) and return early if a request is in-flight; set the submitting
state (setSubmitting or isSubmittingRef) immediately after payload validation
and before calling createProduct, then proceed with the try/catch/finally as-is
and clear the submitting flag in finally; reference handleSubmit, setSubmitting
(or an isSubmittingRef), buildProductCreatePayload, createProduct,
setSubmitErrors, and router.push when making these changes.

Comment on lines +106 to 108
<h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]">
Add product
</h1>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use Syne (700) for the page heading instead of Cabinet.

This heading violates the typography token rule for apps/web/src.

Suggested fix
-        <h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]">
+        <h1 className="font-syne text-base font-bold text-[var(--color-bianca)]">
           Add product
         </h1>

As per coding guidelines, "Use Syne font (weight 700) for all headings via next/font/google".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]">
Add product
</h1>
<h1 className="font-syne text-base font-bold text-[var(--color-bianca)]">
Add product
</h1>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/`(store)/store/products/new/page.tsx around lines 106 - 108,
The page heading in the <h1> element in page.tsx is using the Cabinet font and
weight (class names font-cabinet and font-semibold); change it to use the Syne
700 typography token by replacing font-cabinet and font-semibold with the Syne
font and weight 700 classes (e.g., font-syne and a 700 weight class such as
font-[700] or font-extrabold) so the heading uses Syne (700) per the apps/web
typography rule.

Comment on lines +449 to +453
const attributes: Record<string, unknown> = {};
const volumePricingTiers = buildVolumePricingTiers(pricing, errors);
if (volumePricingTiers.length > 0) {
attributes.volumePricingTiers = volumePricingTiers;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate volume-tier parsing by volumePricingOpen to avoid false validation failures.

buildProductCreatePayload validates/builds volume tiers even when volume pricing is off, so stale hidden tier values can still block submission.

Suggested fix
-  const volumePricingTiers = buildVolumePricingTiers(pricing, errors);
+  const volumePricingTiers = pricing.volumePricingOpen
+    ? buildVolumePricingTiers(pricing, errors)
+    : [];

Also applies to: 578-610

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/product-listing.ts` around lines 449 - 453, The payload
builder currently calls buildVolumePricingTiers and validates volume tiers
unconditionally in buildProductCreatePayload, which causes hidden/stale tiers to
block submission; change the logic to only call buildVolumePricingTiers and add
attributes.volumePricingTiers when the product's volumePricingOpen flag is true
(check the same gating where UI exposes volume pricing), and apply the same
guard in the other similar block around the code at the second occurrence (the
block spanning the area analogous to lines 578-610) so volume tier
parsing/validation is skipped when volumePricingOpen is false.

@onerandomdevv onerandomdevv merged commit 4306f13 into dev May 21, 2026
8 checks passed
@onerandomdevv onerandomdevv deleted the feat/web-product-listing-3-4 branch May 22, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant