Skip to content

feat: new item price tier component#829

Open
tomrndom wants to merge 3 commits intomasterfrom
feature/item-price-tiers
Open

feat: new item price tier component#829
tomrndom wants to merge 3 commits intomasterfrom
feature/item-price-tiers

Conversation

@tomrndom
Copy link

@tomrndom tomrndom commented Mar 18, 2026

  • Update reducers and actions
  • New utils, validations and tests
  • Update editable mui table

ref: https://app.clickup.com/t/86b8ve10b

Summary by CodeRabbit

  • New Features

    • Added a pricing tiers component to manage Early Bird, Standard, and Onsite rates with enable/disable toggles and unified UI.
    • New rate helpers and a nullable decimal validator to support optional rates.
  • Bug Fixes & Improvements

    • Standardized rate formatting and cent conversions across the app.
    • Validation updated to allow null and zero for optional pricing rates; table cells are conditionally editable based on availability.
  • Documentation

    • Added translation keys for pricing tiers and updated validation messages.
  • Tests

    • Added tests for pricing tiers, rate helpers, and nullable decimal validation.

… utils, validations and tests

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

This PR adds rate-specific utilities and tests, introduces an ItemPriceTiers Formik UI component, replaces prior money helpers with rate helpers across actions/reducers/pages, updates validations to support nullable decimals and a renamed i18n key, and makes rate column editability conditional.

Changes

Cohort / File(s) Summary
Rate Utilities & Tests
src/utils/rate-helpers.js, src/utils/__tests__/rate-helpers.test.js
Adds RATE_FIELDS, isRateEnabled, rateFromCents, rateToCents, formatRateFromCents and unit tests; wraps existing money helpers for rate semantics.
Yup Validators & Tests
src/utils/yup.js, src/utils/__tests__/yup.test.js
Adds nullableDecimalValidation(); replaces validation.number_positivevalidation.non_negative in validators; adds tests for nullable decimal behavior.
i18n
src/i18n/en.json
Adds price_tiers keys (early_bird_rate, standard_rate, onsite_rate, available, not_available) and renames validation.number_positivevalidation.non_negative.
New UI Component & Tests
src/components/mui/formik-inputs/item-price-tiers.js, src/components/mui/__tests__/item-price-tiers.test.js
Adds ItemPriceTiers Formik component that manages three rate tiers (enable/toggle + price input) with comprehensive UI tests.
Editable Table Update
src/components/mui/editable-table/mui-table-editable.js
Adds isEditable(col, row) helper; cell click handler now receives full row and uses row.id for editing state.
Action Normalization Changes
src/actions/.../form-template-item-actions.js, src/actions/.../inventory-item-actions.js, src/actions/.../sponsor-forms-actions.js
Replace amountToCents with rateToCents from rate-helpers when normalizing early_bird_rate, standard_rate, onsite_rate.
Rate Rendering Replacements
src/pages/.../add-form-template-item-popup.js, src/pages/.../sponsor-form-add-item-from-inventory-popup.js, src/pages/.../sponsor-form-item-from-inventory.js
Replace currencyAmountFromCents with formatRateFromCents for rate column renderers.
Form Pages: Replace Fields with Component
src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js, src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
Replace three MuiFormikPriceFields with ItemPriceTiers; change validations to nullableDecimalValidation().
Conditional Column Editability
src/pages/sponsors/sponsor-form-item-list-page/index.js, src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
Make editable conditional for rate columns: editable only when value != translation price_tiers.not_available.
Reducers & Tests
src/reducers/sponsors/..., src/reducers/sponsors_inventory/..., src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
Swap money helpers for formatRateFromCents/rateFromCents; default rate fields set from ""0; add RATE_FIELDS exclusion during normalization; update tests/fixtures.
Page Template Validation
src/pages/sponsors-global/page-templates/page-template-popup/index.js
Use validation.non_negative for max_file_size min constraint.
Small imports/usage updates
various files
Updated imports to use new rate-helpers and removed old money utility imports where applicable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • smarcet

Poem

🐇 I nibble cents and tidy tiers so bright,
Three rates now toggle, shown just right,
Helpers hop in, conversions align,
From null to cents the fields now shine,
A tiny rabbit cheers this tidy sign! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a new item price tier component that manages early bird, standard, and onsite rates with enable/disable toggles.

✏️ 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 feature/item-price-tiers
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link

@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: 10

🧹 Nitpick comments (4)
src/utils/__tests__/rate-helpers.test.js (1)

12-66: Add edge-case assertions for undefined and empty string.

Given helper behavior, add explicit tests for isRateEnabled(undefined) and isRateEnabled("") (and optionally rateToCents(undefined)) to prevent regressions on nullable inputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/__tests__/rate-helpers.test.js` around lines 12 - 66, Add edge-case
unit tests for undefined and empty-string inputs: call isRateEnabled(undefined)
and isRateEnabled("") and assert they return false (matching existing null
behavior), and add an optional test calling rateToCents(undefined) asserting it
returns null (matching rateToCents(null)). Place these new assertions alongside
the existing describe blocks for isRateEnabled and rateToCents so they run with
the current test suite.
src/reducers/sponsors_inventory/inventory-item-reducer.js (1)

64-75: Extract duplicated entity normalization/conversion logic.

Line 64–75 and Line 87–98 repeat the same transformation. A shared helper will reduce drift risk between receive/add/update flows.

♻️ Suggested refactor
+const normalizeInventoryItemEntity = (source) => {
+  const entity = { ...source };
+  const rateFieldValues = Object.values(RATE_FIELDS);
+
+  for (const key in entity) {
+    if (
+      Object.prototype.hasOwnProperty.call(entity, key) &&
+      !rateFieldValues.includes(key)
+    ) {
+      entity[key] = entity[key] == null ? "" : entity[key];
+    }
+  }
+
+  entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
+  entity.standard_rate = rateFromCents(entity.standard_rate);
+  entity.onsite_rate = rateFromCents(entity.onsite_rate);
+  return entity;
+};
+
 case RECEIVE_INVENTORY_ITEM: {
-  const entity = { ...payload.response };
-  const rateFieldValues = Object.values(RATE_FIELDS);
-  ...
-  entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
-  entity.standard_rate = rateFromCents(entity.standard_rate);
-  entity.onsite_rate = rateFromCents(entity.onsite_rate);
+  const entity = normalizeInventoryItemEntity(payload.response);
   return {
     ...state,
     entity: {
       ...DEFAULT_ENTITY,
       ...entity
     }
   };
 }
 case INVENTORY_ITEM_ADDED:
 case INVENTORY_ITEM_UPDATED: {
-  const entity = { ...payload.response };
-  const rateFieldValues = Object.values(RATE_FIELDS);
-  ...
-  entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
-  entity.standard_rate = rateFromCents(entity.standard_rate);
-  entity.onsite_rate = rateFromCents(entity.onsite_rate);
+  const entity = normalizeInventoryItemEntity(payload.response);

Also applies to: 87-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/reducers/sponsors_inventory/inventory-item-reducer.js` around lines 64 -
75, Extract the repeated normalization/conversion into a single helper function
(e.g., normalizeInventoryEntity) that takes an entity, iterates over keys and
replaces null/undefined with "" except keys in RATE_FIELDS, and then applies
rateFromCents to early_bird_rate, standard_rate, and onsite_rate; replace the
duplicated blocks in the reducer (both the receive/add/update flows that
reference RATE_FIELDS and call rateFromCents) with a call to this new helper,
and export/import it as needed so all reducer cases share the same
implementation.
src/reducers/sponsors_inventory/form-template-item-reducer.js (1)

65-75: Same duplicated normalization block—consider one helper here too.

Line 65–75 and Line 88–98 duplicate the same logic. Centralizing this transformation would keep both action paths consistent long-term.

Also applies to: 88-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/reducers/sponsors_inventory/form-template-item-reducer.js` around lines
65 - 75, Extract the duplicated normalization logic into a single helper (e.g.,
normalizeFormTemplateItem(entity)) that: 1) iterates over entity keys and
replaces null/undefined with "" for keys not in RATE_FIELDS, and 2) applies
rateFromCents to entity.early_bird_rate, entity.standard_rate, and
entity.onsite_rate; then replace the duplicated blocks in the reducer with a
call to normalizeFormTemplateItem(entity) so both code paths use the same
function (keeping references to RATE_FIELDS and rateFromCents unchanged).
src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js (1)

95-97: Add null-rate coverage for the new nullable tier behavior.

These updated fixtures validate numeric cents, but this suite still misses explicit assertions for null tier inputs in both list and current-item receive paths.

Also applies to: 166-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js`
around lines 95 - 97, Update the tests to cover nullable tier rates by adding
fixtures where early_bird_rate, standard_rate, and onsite_rate are null and
asserting the reducer handles them correctly: for the list receive path (the
test in sponsor-form-items-list-reducer.test that feeds a list of items) assert
the resulting state stores null for those rate fields, and for the current-item
receive path (the test that populates the current item) assert the reducer sets
the current item rates to null; locate the relevant tests in
sponsor-form-items-list-reducer.test.js and modify the fixture objects and
expectations around the existing numeric-rate cases to include and assert
null-rate behavior for both list and current-item handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/mui/__tests__/item-price-tiers.test.js`:
- Around line 8-11: The jest.mock path is wrong so the test doesn’t override the
real import; update the jest.mock call to mock "i18n-react" (not
"i18n-react/dist/i18n-react") and export __esModule: true with a default object
that provides translate: (key) => key (matching how the component uses the
imported module), i.e., modify the existing jest.mock invocation in the test to
target "i18n-react" and return the same default shape.

In `@src/components/mui/formik-inputs/item-price-tiers.js`:
- Around line 24-33: The local enabled state (enabled / setEnabled) in the
component is redundant and can desync from Formik after reinitialization; remove
the enabled useState and any references to setEnabled, derive the toggle state
directly from Formik values by calling isRateEnabled(values[RATE_FIELDS.*])
where needed, and simplify handleToggle to only call setFieldValue(field,
checked ? 0 : null); ensure any UI that read enabled now computes it from values
(or useMemo(values) for perf) and keep references to isRateEnabled, RATE_FIELDS,
handleToggle, setFieldValue, and values to locate the changes.

In `@src/i18n/en.json`:
- Line 92: Update the validation message for the key "validation.non_negative":
it currently reads "Must be a positive number." which is wrong for non-negative
checks; change the string to explicitly state non-negative/zero-allowed (for
example "Must be non-negative" or "Must be 0 or greater") so users understand
the allowed value range and the "validation.non_negative" localization key is
accurate.

In `@src/pages/sponsors-global/page-templates/page-template-popup/index.js`:
- Line 67: The validation call uses .min(BYTES_PER_MB,
T.translate("validation.non_negative")) which mislabels the constraint; update
the message passed to .min so it reflects the actual minimum (BYTES_PER_MB)
instead of "validation.non_negative" — either use the correct translation key
(e.g., "validation.min_bytes") or interpolate BYTES_PER_MB into the translated
string via T.translate, and keep this change where .min(BYTES_PER_MB, ...) is
used so users see a correct message for values like 0 or 500000.

In
`@src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js`:
- Line 95: ItemPriceTiers' local enabled state is initialized only on mount and
becomes stale when Formik reinitializes; add a useEffect inside the
ItemPriceTiers component that subscribes to the Formik value(s) controlling tier
availability (via useFormikContext().values or the prop currently passed from
sponsor-form-item-form) and updates the local enabled state whenever those
Formik values change so the toggle reflects reinitialized form values; ensure
the effect's dependency array contains the exact Formik field(s) (e.g.,
values.priceTiersEnabled or the field name used in your form) to avoid
unnecessary updates.

In
`@src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js`:
- Line 215: The current editable callbacks (e.g., editable: (row) =>
row.early_bird_rate !== "N/A") rely on a hard-coded locale string; replace this
with a locale-independent check by adding a small helper (e.g.,
isRateEditable(value)) that returns false for null/undefined or when the value
cannot be parsed into a finite number (strip currency/spacing and use
Number.isFinite(parsedNumber)), and true otherwise; then update the editable
arrow functions for early_bird_rate and the other rate columns to call
isRateEditable(row.early_bird_rate) (and the analogous fields) instead of
comparing to "N/A".

In `@src/utils/__tests__/yup.test.js`:
- Around line 3-5: The jest mock currently targets "i18n-react/dist/i18n-react"
but the validator imports "i18n-react", so update the jest.mock call to mock
"i18n-react" instead (keeping the same translate: (key) => key export) so the
module name matches the actual import used by the yup validator and avoids
resolver-dependent behavior; locate the jest.mock invocation in the test and
change its module specifier to "i18n-react".

In `@src/utils/rate-helpers.js`:
- Around line 14-15: rateToCents currently passes empty strings into
amountToCents while isRateEnabled treats "" as disabled; modify rateToCents to
mirror isRateEnabled by returning undefined (or null, matching surrounding
conventions) when value is null, undefined, or an empty string (use the same
check: value !== null && value !== undefined && value !== "") so you never call
amountToCents with "" and invalid rates cannot be normalized/persisted; update
the function that calls amountToCents (rateToCents) to early-return when the
value fails that check.

In `@src/utils/yup.js`:
- Around line 163-171: The nullableDecimalValidation schema fails when
Formik/MUI inputs return an empty string because number().nullable() doesn't
accept ""—add a transform to nullableDecimalValidation that converts empty
string (and whitespace-only) original values to null before other validations
run; update the function nullableDecimalValidation to call .transform((value,
originalValue) => (typeof originalValue === "string" && originalValue.trim() ===
"" ? null : value)) so cleared fields validate as null and your existing
.nullable(), .typeError(), .min(), and .test("max-decimals", ...) continue to
work.
- Line 50: Replace the use of .positive(T.translate("validation.non_negative"))
with .min(0, T.translate("validation.non_negative")) in the validators inside
src/utils/yup.js so the rule allows zero (non-negative) instead of requiring >0;
update both occurrences (the one at the shown diff and the other around line 82)
so they match the existing nullableDecimalValidation behavior (which uses
.min(0)) and keep the same translation key.

---

Nitpick comments:
In `@src/reducers/sponsors_inventory/form-template-item-reducer.js`:
- Around line 65-75: Extract the duplicated normalization logic into a single
helper (e.g., normalizeFormTemplateItem(entity)) that: 1) iterates over entity
keys and replaces null/undefined with "" for keys not in RATE_FIELDS, and 2)
applies rateFromCents to entity.early_bird_rate, entity.standard_rate, and
entity.onsite_rate; then replace the duplicated blocks in the reducer with a
call to normalizeFormTemplateItem(entity) so both code paths use the same
function (keeping references to RATE_FIELDS and rateFromCents unchanged).

In `@src/reducers/sponsors_inventory/inventory-item-reducer.js`:
- Around line 64-75: Extract the repeated normalization/conversion into a single
helper function (e.g., normalizeInventoryEntity) that takes an entity, iterates
over keys and replaces null/undefined with "" except keys in RATE_FIELDS, and
then applies rateFromCents to early_bird_rate, standard_rate, and onsite_rate;
replace the duplicated blocks in the reducer (both the receive/add/update flows
that reference RATE_FIELDS and call rateFromCents) with a call to this new
helper, and export/import it as needed so all reducer cases share the same
implementation.

In `@src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js`:
- Around line 95-97: Update the tests to cover nullable tier rates by adding
fixtures where early_bird_rate, standard_rate, and onsite_rate are null and
asserting the reducer handles them correctly: for the list receive path (the
test in sponsor-form-items-list-reducer.test that feeds a list of items) assert
the resulting state stores null for those rate fields, and for the current-item
receive path (the test that populates the current item) assert the reducer sets
the current item rates to null; locate the relevant tests in
sponsor-form-items-list-reducer.test.js and modify the fixture objects and
expectations around the existing numeric-rate cases to include and assert
null-rate behavior for both list and current-item handlers.

In `@src/utils/__tests__/rate-helpers.test.js`:
- Around line 12-66: Add edge-case unit tests for undefined and empty-string
inputs: call isRateEnabled(undefined) and isRateEnabled("") and assert they
return false (matching existing null behavior), and add an optional test calling
rateToCents(undefined) asserting it returns null (matching rateToCents(null)).
Place these new assertions alongside the existing describe blocks for
isRateEnabled and rateToCents so they run with the current test suite.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b233ec2-0b58-4f57-a414-5edc4f714930

📥 Commits

Reviewing files that changed from the base of the PR and between 6152c8e and 22d6c70.

📒 Files selected for processing (24)
  • src/actions/form-template-item-actions.js
  • src/actions/inventory-item-actions.js
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/__tests__/item-price-tiers.test.js
  • src/components/mui/editable-table/mui-table-editable.js
  • src/components/mui/formik-inputs/item-price-tiers.js
  • src/i18n/en.json
  • src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
  • src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
  • src/pages/sponsors-global/page-templates/page-template-popup/index.js
  • src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
  • src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
  • src/pages/sponsors/sponsor-form-item-list-page/index.js
  • src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
  • src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
  • src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
  • src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
  • src/reducers/sponsors/sponsor-form-items-list-reducer.js
  • src/reducers/sponsors_inventory/form-template-item-reducer.js
  • src/reducers/sponsors_inventory/inventory-item-reducer.js
  • src/utils/__tests__/rate-helpers.test.js
  • src/utils/__tests__/yup.test.js
  • src/utils/rate-helpers.js
  • src/utils/yup.js

Comment on lines +24 to +33
const [enabled, setEnabled] = useState({
[RATE_FIELDS.EARLY_BIRD]: isRateEnabled(values[RATE_FIELDS.EARLY_BIRD]),
[RATE_FIELDS.STANDARD]: isRateEnabled(values[RATE_FIELDS.STANDARD]),
[RATE_FIELDS.ONSITE]: isRateEnabled(values[RATE_FIELDS.ONSITE])
});

const handleToggle = (field, checked) => {
setEnabled((prev) => ({ ...prev, [field]: checked }));
setFieldValue(field, checked ? 0 : null);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

enabled state can desync from Formik values after reinitialization.

The local state is only seeded once. If Formik values change later, toggle UI can be stale.

💡 Suggested fix (derive from Formik values, remove duplicate state)
-import React, { useState } from "react";
+import React from "react";
@@
-  const [enabled, setEnabled] = useState({
+  const enabled = {
     [RATE_FIELDS.EARLY_BIRD]: isRateEnabled(values[RATE_FIELDS.EARLY_BIRD]),
     [RATE_FIELDS.STANDARD]: isRateEnabled(values[RATE_FIELDS.STANDARD]),
     [RATE_FIELDS.ONSITE]: isRateEnabled(values[RATE_FIELDS.ONSITE])
-  });
+  };
 
   const handleToggle = (field, checked) => {
-    setEnabled((prev) => ({ ...prev, [field]: checked }));
     setFieldValue(field, checked ? 0 : null);
   };
📝 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
const [enabled, setEnabled] = useState({
[RATE_FIELDS.EARLY_BIRD]: isRateEnabled(values[RATE_FIELDS.EARLY_BIRD]),
[RATE_FIELDS.STANDARD]: isRateEnabled(values[RATE_FIELDS.STANDARD]),
[RATE_FIELDS.ONSITE]: isRateEnabled(values[RATE_FIELDS.ONSITE])
});
const handleToggle = (field, checked) => {
setEnabled((prev) => ({ ...prev, [field]: checked }));
setFieldValue(field, checked ? 0 : null);
};
const enabled = {
[RATE_FIELDS.EARLY_BIRD]: isRateEnabled(values[RATE_FIELDS.EARLY_BIRD]),
[RATE_FIELDS.STANDARD]: isRateEnabled(values[RATE_FIELDS.STANDARD]),
[RATE_FIELDS.ONSITE]: isRateEnabled(values[RATE_FIELDS.ONSITE])
};
const handleToggle = (field, checked) => {
setFieldValue(field, checked ? 0 : null);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/formik-inputs/item-price-tiers.js` around lines 24 - 33,
The local enabled state (enabled / setEnabled) in the component is redundant and
can desync from Formik after reinitialization; remove the enabled useState and
any references to setEnabled, derive the toggle state directly from Formik
values by calling isRateEnabled(values[RATE_FIELDS.*]) where needed, and
simplify handleToggle to only call setFieldValue(field, checked ? 0 : null);
ensure any UI that read enabled now computes it from values (or useMemo(values)
for perf) and keep references to isRateEnabled, RATE_FIELDS, handleToggle,
setFieldValue, and values to locate the changes.

"string": "Must be a string.",
"number": "Must be a number.",
"number_positive": "Must be a positive number.",
"non_negative": "Must be a positive number.",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

validation.non_negative message text is semantically incorrect

Line 92 currently says “Must be a positive number.” For non-negative validation (>= 0), this should mention non-negative (or at least “0 or greater”) to avoid misleading users.

💡 Proposed fix
-    "non_negative": "Must be a positive number.",
+    "non_negative": "Must be a non-negative number.",
📝 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
"non_negative": "Must be a positive number.",
"non_negative": "Must be a non-negative number.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/en.json` at line 92, Update the validation message for the key
"validation.non_negative": it currently reads "Must be a positive number." which
is wrong for non-negative checks; change the string to explicitly state
non-negative/zero-allowed (for example "Must be non-negative" or "Must be 0 or
greater") so users understand the allowed value range and the
"validation.non_negative" localization key is accurate.

then: (schema) =>
schema
.min(BYTES_PER_MB, T.translate("validation.number_positive"))
.min(BYTES_PER_MB, T.translate("validation.non_negative"))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validation message does not match the actual minimum constraint

Line 67 enforces a minimum of BYTES_PER_MB (1 MiB), but the message key is validation.non_negative. Users get misleading feedback for values like 0 or 500000.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors-global/page-templates/page-template-popup/index.js` at
line 67, The validation call uses .min(BYTES_PER_MB,
T.translate("validation.non_negative")) which mislabels the constraint; update
the message passed to .min so it reflects the actual minimum (BYTES_PER_MB)
instead of "validation.non_negative" — either use the correct translation key
(e.g., "validation.min_bytes") or interpolate BYTES_PER_MB into the translated
string via T.translate, and keep this change where .min(BYTES_PER_MB, ...) is
used so users see a correct message for values like 0 or 500000.

required
/>
</Grid2>
<ItemPriceTiers />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify reinitialize + local enabled state initialization pattern
rg -n -C3 'enableReinitialize|ItemPriceTiers' src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
rg -n -C5 'const \[enabled, setEnabled\] = useState|useFormikContext|handleToggle|useEffect' src/components/mui/formik-inputs/item-price-tiers.js

Repository: fntechgit/summit-admin

Length of output: 2428


Add useEffect to re-sync ItemPriceTiers local state when form reinitializes.

The enabled state initializes once on mount but doesn't track subsequent Formik value changes. When enableReinitialize: true causes the form to reinitialize (e.g., when switching items), the local state becomes stale while Formik values update, leading to toggles displaying incorrect availability and potentially overwriting rates with outdated state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js`
at line 95, ItemPriceTiers' local enabled state is initialized only on mount and
becomes stale when Formik reinitializes; add a useEffect inside the
ItemPriceTiers component that subscribes to the Formik value(s) controlling tier
availability (via useFormikContext().values or the prop currently passed from
sponsor-form-item-form) and updates the local enabled state whenever those
Formik values change so the toggle reflects reinitialized form values; ensure
the effect's dependency array contains the exact Formik field(s) (e.g.,
values.priceTiersEnabled or the field name used in your form) to avoid
unnecessary updates.

),
sortable: false,
editable: true,
editable: (row) => row.early_bird_rate !== "N/A",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hard-coding "N/A" for rate editability checks.

These checks are tied to one locale string. In non-English locales, cells can become incorrectly editable/non-editable.

💡 Suggested fix
+  const notAvailableLabel = T.translate("price_tiers.not_available");
+  const isRateCellEditable = (value) => value !== notAvailableLabel;
+
   const sponsorItemColumns = [
@@
-      editable: (row) => row.early_bird_rate !== "N/A",
+      editable: (row) => isRateCellEditable(row.early_bird_rate),
@@
-      editable: (row) => row.standard_rate !== "N/A",
+      editable: (row) => isRateCellEditable(row.standard_rate),
@@
-      editable: (row) => row.onsite_rate !== "N/A",
+      editable: (row) => isRateCellEditable(row.onsite_rate),

Also applies to: 226-226, 237-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js`
at line 215, The current editable callbacks (e.g., editable: (row) =>
row.early_bird_rate !== "N/A") rely on a hard-coded locale string; replace this
with a locale-independent check by adding a small helper (e.g.,
isRateEditable(value)) that returns false for null/undefined or when the value
cannot be parsed into a finite number (strip currency/spacing and use
Number.isFinite(parsedNumber)), and true otherwise; then update the editable
arrow functions for early_bird_rate and the other rate columns to call
isRateEditable(row.early_bird_rate) (and the analogous fields) instead of
comparing to "N/A".

Comment on lines +14 to +15
export const isRateEnabled = (value) =>
value !== null && value !== undefined && value !== "";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In openstack-uicore-foundation/lib/utils/money, what does amountToCents("") return?

💡 Result:

Web search failed: Server error: no LLM provider could handle the message


🏁 Script executed:

cat -n src/utils/rate-helpers.js

Repository: fntechgit/summit-admin

Length of output: 1113


🏁 Script executed:

rg "rateToCents" --type js --type ts -B 2 -A 2

Repository: fntechgit/summit-admin

Length of output: 4724


🏁 Script executed:

fd -e test.js -e spec.js -e test.ts -e spec.ts | head -20

Repository: fntechgit/summit-admin

Length of output: 1209


🏁 Script executed:

fd "rate.*helpers" --type f

Repository: fntechgit/summit-admin

Length of output: 132


🏁 Script executed:

cat -n src/utils/__tests__/rate-helpers.test.js

Repository: fntechgit/summit-admin

Length of output: 2132


🏁 Script executed:

rg "rateToCents" -B 5 -A 5 src/actions/sponsor-forms-actions.js

Repository: fntechgit/summit-admin

Length of output: 908


🏁 Script executed:

rg "isRateEnabled" -B 3 -A 3

Repository: fntechgit/summit-admin

Length of output: 3250


🏁 Script executed:

rg "amountToCents" src/ --type js --type ts -B 2 -A 2 | head -40

Repository: fntechgit/summit-admin

Length of output: 586


Empty-string handling is inconsistent between isRateEnabled and rateToCents

isRateEnabled("") returns false (line 15 checks value !== ""), but rateToCents("") does not guard against empty strings and passes them to amountToCents. This inconsistency can allow empty strings to flow into rate normalization during save operations, potentially persisting invalid values.

Add empty-string check to rateToCents to align with isRateEnabled:

Proposed fix
 export const rateToCents = (value) => {
-  if (value === null || value === undefined) return null;
+  if (value === null || value === undefined || value === "") return null;
   return amountToCents(value);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/rate-helpers.js` around lines 14 - 15, rateToCents currently passes
empty strings into amountToCents while isRateEnabled treats "" as disabled;
modify rateToCents to mirror isRateEnabled by returning undefined (or null,
matching surrounding conventions) when value is null, undefined, or an empty
string (use the same check: value !== null && value !== undefined && value !==
"") so you never call amountToCents with "" and invalid rates cannot be
normalized/persisted; update the function that calls amountToCents (rateToCents)
to early-return when the value fails that check.

.number()
.typeError(T.translate("validation.number"))
.positive(T.translate("validation.number_positive"))
.positive(T.translate("validation.non_negative"))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Yup, what is the behavioral difference between number().positive()andnumber().min(0)regarding whether0 is valid?

💡 Result:

In Yup:

  • number().positive() is strictly greater than 0, so 0 is invalid (it’s implemented as moreThan(0), i.e. value > 0). [1]
  • number().min(0) is greater than or equal to 0, so 0 is valid (the check is value >= min). [1]

So: use .positive() to reject zero; use .min(0) to allow zero. [1]

Sources:
[1] Yup NumberSchema implementation (min uses >=, positive uses moreThan(0)): (app.unpkg.com)

Citations:


🏁 Script executed:

cat -n src/utils/yup.js | head -180

Repository: fntechgit/summit-admin

Length of output: 7246


positive() contradicts the non-negative rule here

Lines 50 and 82 use positive() (> 0), but these validators advertise validation.non_negative and should accept 0. This conflicts with the semantics of "non-negative" (which means >= 0) and is inconsistent with nullableDecimalValidation at line 168, which correctly uses .min(0) for the same message.

Replace positive() with min(0) to allow zero and maintain consistency:

Fix
 export const decimalValidation = () =>
   yup
     .number()
     .typeError(T.translate("validation.number"))
-    .positive(T.translate("validation.non_negative"))
+    .min(0, T.translate("validation.non_negative"))
     .required(T.translate("validation.required"))

 export const rateCellValidation = () =>
   yup
     .number()
-    .positive(T.translate("validation.non_negative"))
+    .min(0, T.translate("validation.non_negative"))
     .test("max-decimals", T.translate("validation.two_decimals"), (value) => {
📝 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
.positive(T.translate("validation.non_negative"))
.min(0, T.translate("validation.non_negative"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/yup.js` at line 50, Replace the use of
.positive(T.translate("validation.non_negative")) with .min(0,
T.translate("validation.non_negative")) in the validators inside
src/utils/yup.js so the rule allows zero (non-negative) instead of requiring >0;
update both occurrences (the one at the shown diff and the other around line 82)
so they match the existing nullableDecimalValidation behavior (which uses
.min(0)) and keep the same translation key.

Comment on lines +163 to +171
export const nullableDecimalValidation = () =>
yup
.number()
.nullable()
.typeError(T.translate("validation.number"))
.min(0, T.translate("validation.non_negative"))
.test("max-decimals", T.translate("validation.two_decimals"), (value) => {
if (value === undefined || value === null) return true;
return /^\d+(\.\d{1,2})?$/.test(value.toString());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Yup, does number().nullable()accept an empty string""without a.transform()?

💡 Result:

No. number().nullable() only makes null a valid value; it does not treat the empty string "" as null. In Yup, number() uses parseFloat for casting and “failed casts return NaN” [1]. Since parseFloat("") results in NaN (i.e., not a valid number) [2], validating "" against yup.number().nullable() will still fail unless you add a .transform() (or otherwise convert "" to null/undefined)—a common workaround discussed by Yup users [3].

Sources: [1] Yup docs (GitHub) number() casting / failed casts -> NaN; nullable() applies to null only. [2] MDN parseFloat returns NaN when it can’t parse a number. [3] Yup issue “Number but allow empty string” (#298).


🏁 Script executed:

# Check the context around the validation function
cd src/utils && head -20 yup.js && tail -20 yup.js

Repository: fntechgit/summit-admin

Length of output: 1395


🏁 Script executed:

# Search for usages of nullableDecimalValidation
rg "nullableDecimalValidation" --type js

Repository: fntechgit/summit-admin

Length of output: 1335


🏁 Script executed:

# Check if Formik and MUI are in the project
rg "formik|@mui|material-ui" --type js --type json -l | head -20

Repository: fntechgit/summit-admin

Length of output: 1425


🏁 Script executed:

# Look for other validation functions with similar patterns
ast-grep --pattern $'export const $_ = () => yup.number()'

Repository: fntechgit/summit-admin

Length of output: 48


🏁 Script executed:

# Check the test file for nullableDecimalValidation
cat src/utils/__tests__/yup.test.js | grep -A 30 "nullableDecimalValidation"

Repository: fntechgit/summit-admin

Length of output: 1081


🏁 Script executed:

# Check how the form uses Formik with these fields
cat src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js | head -100

Repository: fntechgit/summit-admin

Length of output: 3518


Add .transform() to normalize empty strings to null in nullableDecimalValidation

With Formik/MUI text inputs, clearing a field yields "". Since number().nullable() only accepts null (not empty string), validation fails on cleared fields. parseFloat("") returns NaN, causing the field to fail the number validation regardless of .nullable().

Proposed fix
 export const nullableDecimalValidation = () =>
   yup
     .number()
+    .transform((value, originalValue) =>
+      originalValue === "" ? null : value
+    )
     .nullable()
     .typeError(T.translate("validation.number"))
     .min(0, T.translate("validation.non_negative"))
     .test("max-decimals", T.translate("validation.two_decimals"), (value) => {
       if (value === undefined || value === null) return true;
       return /^\d+(\.\d{1,2})?$/.test(value.toString());
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/yup.js` around lines 163 - 171, The nullableDecimalValidation
schema fails when Formik/MUI inputs return an empty string because
number().nullable() doesn't accept ""—add a transform to
nullableDecimalValidation that converts empty string (and whitespace-only)
original values to null before other validations run; update the function
nullableDecimalValidation to call .transform((value, originalValue) => (typeof
originalValue === "string" && originalValue.trim() === "" ? null : value)) so
cleared fields validate as null and your existing .nullable(), .typeError(),
.min(), and .test("max-decimals", ...) continue to work.

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Copy link

@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.

🧹 Nitpick comments (2)
src/utils/__tests__/yup.test.js (1)

7-34: Good test coverage for the main validation scenarios.

The test suite appropriately covers null, zero, positive integers, decimal precision limits, and negative values.

Consider adding a test for undefined since the implementation explicitly handles it in the custom test function:

if (value === undefined || value === null) return true;
🧪 Proposed addition for completeness
   it("should pass for null", async () => {
     await expect(schema.isValid(null)).resolves.toBe(true);
   });
+
+  it("should pass for undefined", async () => {
+    await expect(schema.isValid(undefined)).resolves.toBe(true);
+  });
 
   it("should pass for 0", async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/__tests__/yup.test.js` around lines 7 - 34, Add a test case to the
nullableDecimalValidation suite that asserts undefined is considered valid:
inside the existing describe("nullableDecimalValidation", ...) block add an
it("should pass for undefined", async () => { await
expect(schema.isValid(undefined)).resolves.toBe(true); }); to exercise the
explicit guard in the custom test (value === undefined || value === null) and
ensure nullableDecimalValidation handles undefined as intended.
src/pages/sponsors/sponsor-form-item-list-page/index.js (1)

140-141: Consider using raw rate values or a dedicated availability flag instead of comparing translated display strings.

The current logic compares row.early_bird_rate !== T.translate("price_tiers.not_available") to determine editability. While this works, it couples business logic to display formatting and i18n, making it fragile if the translation changes or formatting logic is updated independently.

The codebase already has an isRateEnabled() helper that could be used if raw values were available. Store raw rate values (or boolean flags) alongside the formatted display values in the reducer, then compare against the underlying data instead of the translated string. This decouples business logic from presentation.

This applies to all three rate columns (lines 140–141, 150–151, 160–161).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-form-item-list-page/index.js` around lines 140 -
141, The editable predicate currently compares the translated display string
(row.early_bird_rate !== T.translate("price_tiers.not_available")), which
couples logic to i18n; change the data model and predicate to use raw rate
values or a boolean availability flag produced by the reducer and the existing
helper isRateEnabled(), e.g., populate row.early_bird_rate_raw or
row.early_bird_enabled in the reducer alongside the formatted value and update
the editable function (and the other two rate column predicates) to call
isRateEnabled(row.early_bird_rate_raw) or check row.early_bird_enabled instead
of comparing against T.translate; this keeps business logic independent of
presentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pages/sponsors/sponsor-form-item-list-page/index.js`:
- Around line 140-141: The editable predicate currently compares the translated
display string (row.early_bird_rate !==
T.translate("price_tiers.not_available")), which couples logic to i18n; change
the data model and predicate to use raw rate values or a boolean availability
flag produced by the reducer and the existing helper isRateEnabled(), e.g.,
populate row.early_bird_rate_raw or row.early_bird_enabled in the reducer
alongside the formatted value and update the editable function (and the other
two rate column predicates) to call isRateEnabled(row.early_bird_rate_raw) or
check row.early_bird_enabled instead of comparing against T.translate; this
keeps business logic independent of presentation.

In `@src/utils/__tests__/yup.test.js`:
- Around line 7-34: Add a test case to the nullableDecimalValidation suite that
asserts undefined is considered valid: inside the existing
describe("nullableDecimalValidation", ...) block add an it("should pass for
undefined", async () => { await
expect(schema.isValid(undefined)).resolves.toBe(true); }); to exercise the
explicit guard in the custom test (value === undefined || value === null) and
ensure nullableDecimalValidation handles undefined as intended.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9070f818-3948-4fb3-9db8-5dcc0cc7bd6e

📥 Commits

Reviewing files that changed from the base of the PR and between 22d6c70 and cf6238e.

📒 Files selected for processing (5)
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/__tests__/item-price-tiers.test.js
  • src/pages/sponsors/sponsor-form-item-list-page/index.js
  • src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
  • src/utils/__tests__/yup.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/actions/sponsor-forms-actions.js
  • src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
  • src/components/mui/tests/item-price-tiers.test.js

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Copy link

@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.

♻️ Duplicate comments (1)
src/utils/rate-helpers.js (1)

22-26: ⚠️ Potential issue | 🟠 Major

Semantic mismatch: empty string returns 0 but isRateEnabled("") returns false.

The function treats empty string (cleared input field) as a valid $0.00 rate, but isRateEnabled("") treats it as disabled. This inconsistency means a user clearing a rate field persists it as zero instead of null, contradicting the semantic meaning of an empty rate value.

Change rateToCents("") to return null instead of 0 to align with isRateEnabled behavior.

Proposed fix
 export const rateToCents = (value) => {
   if (value === null || value === undefined) return null;
-  if (value === "") return 0;
+  if (value === "") return null;
   return amountToCents(value);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/rate-helpers.js` around lines 22 - 26, rateToCents currently maps
an empty string to 0 while isRateEnabled("") treats empty string as disabled;
update rateToCents to return null for value === "" so semantics match
isRateEnabled, i.e., change the empty-string branch in rateToCents (which calls
amountToCents) to return null instead of 0, and run/update any related tests
that expect the previous behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/utils/rate-helpers.js`:
- Around line 22-26: rateToCents currently maps an empty string to 0 while
isRateEnabled("") treats empty string as disabled; update rateToCents to return
null for value === "" so semantics match isRateEnabled, i.e., change the
empty-string branch in rateToCents (which calls amountToCents) to return null
instead of 0, and run/update any related tests that expect the previous
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60399247-3a87-4f77-a4a4-f7218bd98361

📥 Commits

Reviewing files that changed from the base of the PR and between cf6238e and 3ab8c27.

📒 Files selected for processing (4)
  • src/utils/__tests__/rate-helpers.test.js
  • src/utils/__tests__/yup.test.js
  • src/utils/rate-helpers.js
  • src/utils/yup.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/utils/tests/rate-helpers.test.js
  • src/utils/yup.js
  • src/utils/tests/yup.test.js

@tomrndom tomrndom requested a review from smarcet March 18, 2026 12:20
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