From 5440c0a82c77a040f1caf530001a8b8ab22514d1 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 26 Aug 2026 10:20:39 -0300 Subject: [PATCH 1/4] chore: fix custom rate path - WIP --- src/components/mui/FormItemTable/helpers.js | 4 ++-- src/components/mui/FormItemTable/index.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/mui/FormItemTable/helpers.js b/src/components/mui/FormItemTable/helpers.js index a2f7873c..ecfc7dde 100644 --- a/src/components/mui/FormItemTable/helpers.js +++ b/src/components/mui/FormItemTable/helpers.js @@ -39,8 +39,8 @@ export const getCurrentApplicableRate = (timeZone, rateDates) => { return "expired"; }; -export const isItemAvailable = (item, currentApplicableRate) => - item.rates?.[currentApplicableRate] != null; +export const isItemAvailable = (item, currentApplicableRate, customRate = 0) => + customRate || item.rates?.[currentApplicableRate] != null; // The global quantity for a row is driven (and therefore read-only/computed) // when a Form-class metafield of type Quantity exists for it (extraColumns, diff --git a/src/components/mui/FormItemTable/index.js b/src/components/mui/FormItemTable/index.js index 3e8aba60..ffa1cedf 100644 --- a/src/components/mui/FormItemTable/index.js +++ b/src/components/mui/FormItemTable/index.js @@ -164,13 +164,12 @@ const FormItemTable = ({ ); const calculateRowTotal = (row) => { + const customRate = values[`i-${row.form_item_id}-c-global-f-custom_rate`]; const qty = values[`i-${row.form_item_id}-c-global-f-quantity`] || calculateQuantity(row); - if (currentApplicableRate === "expired") return 0; - - const customRate = values[`i-${row.form_item_id}-c-global-f-custom_rate`]; + // if currentRate is expired or not set then we return, unless customRate is set const rate = customRate || row.rates[currentApplicableRate]; if (rate == null || qty == null) return 0; @@ -230,7 +229,8 @@ const FormItemTable = ({ {data.map((row) => { - const disabled = !isItemAvailable(row, currentApplicableRate); + const customRate = values[`i-${row.form_item_id}-c-global-f-custom_rate`]; + const disabled = !isItemAvailable(row, currentApplicableRate, customRate); const isOpen = !!openRows[row.form_item_id]; return ( From f680f29512e0dc6f697b8b8acf4236e67b11fee7 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 26 Aug 2026 18:33:57 -0300 Subject: [PATCH 2/4] v5.0.55-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 565cb8d7..e4560d1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.56", + "version": "5.0.55-beta.0", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From ac6b36c3925d0f438bc46e190ee96bf2576c48d9 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 2 Sep 2026 16:31:30 -0300 Subject: [PATCH 3/4] chore: cast boolean --- src/components/mui/FormItemTable/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/mui/FormItemTable/helpers.js b/src/components/mui/FormItemTable/helpers.js index ecfc7dde..852bef34 100644 --- a/src/components/mui/FormItemTable/helpers.js +++ b/src/components/mui/FormItemTable/helpers.js @@ -40,7 +40,7 @@ export const getCurrentApplicableRate = (timeZone, rateDates) => { }; export const isItemAvailable = (item, currentApplicableRate, customRate = 0) => - customRate || item.rates?.[currentApplicableRate] != null; + !!customRate || item.rates?.[currentApplicableRate] != null; // The global quantity for a row is driven (and therefore read-only/computed) // when a Form-class metafield of type Quantity exists for it (extraColumns, From b0f566298a760488a0b608a3b1016c74bbda5688 Mon Sep 17 00:00:00 2001 From: smarcet Date: Mon, 7 Sep 2026 14:35:04 -0300 Subject: [PATCH 4/4] chore: add regression tests for custom rate over expired tier rate The custom-rate fix shipped without coverage: reverting both production hunks left the suite fully green (74/74), so nothing protected it. helpers.test.js: cover the new customRate argument of isItemAvailable -- truthy custom rate makes an otherwise unavailable item available, 0 is the "not set" sentinel (matching what the API treats as unset), and the strict toBe(true) pins the boolean coercion so the short-circuit cannot leak the raw rate back to callers. FormItemTable.test.js: new Custom Rate block covering the row total priced off the custom rate when the tier rate is expired, the total staying at zero without one, the row's fields staying editable with a custom rate set, being disabled without one, and re-disabling when the custom rate is cleared back to 0. Verified red-green -- each production hunk reverted in isolation fails at least one of the new tests (4, 2, 1 and 2 failures respectively). --- .../__tests__/FormItemTable.test.js | 97 +++++++++++++++++++ .../FormItemTable/__tests__/helpers.test.js | 23 +++++ 2 files changed, 120 insertions(+) diff --git a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js index 7d601bfb..34c2c656 100644 --- a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js +++ b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js @@ -921,6 +921,103 @@ describe("FormItemTable Component", () => { }); }); + // A custom rate is an explicit override that must win over the tier rate + // even when no tier rate applies (expired window, or null rates), both for + // pricing and for whether the row's fields stay editable. 0 is the + // "no custom rate" sentinel, matching what the API treats as unset. + describe("Custom Rate", () => { + const CUSTOM_RATE = 5000; + const QUANTITY = 3; + + it("prices the row off the custom rate when the applicable rate is expired", () => { + // 3 * 5000 = 15000 cents -> $150.00, as row total and as grand total. + render( + + ); + + const dollarValues = screen + .getAllByText(/\$/) + .map((el) => el.textContent); + + expect(dollarValues).toContain("$150.00"); + expect(dollarValues).not.toContain("$0.00"); + }); + + it("keeps the row total at zero when the rate is expired and no custom rate is set", () => { + render( + + ); + + const dollarValues = screen + .getAllByText(/\$/) + .map((el) => el.textContent); + + expect(dollarValues).toContain("$0.00"); + expect(dollarValues).not.toContain("$150.00"); + }); + + it("leaves the row's fields editable when a custom rate is set but no tier rate applies", () => { + render( + + ); + + expect(screen.getByTestId("textfield-i-9-c-Item-f-1")).toBeEnabled(); + }); + + it("disables the row's fields when no tier rate applies and no custom rate is set", () => { + render( + + ); + + expect(screen.getByTestId("textfield-i-9-c-Item-f-1")).toBeDisabled(); + }); + + it("re-disables the row's fields when the custom rate is cleared back to 0", async () => { + render( + + ); + + expect(screen.getByTestId("textfield-i-9-c-Item-f-1")).toBeEnabled(); + + fireEvent.change( + screen.getByTestId("pricefield-i-9-c-global-f-custom_rate"), + { target: { value: "0" } } + ); + + await waitFor(() => + expect(screen.getByTestId("textfield-i-9-c-Item-f-1")).toBeDisabled() + ); + }); + }); + describe("Rate Highlighting", () => { it("highlights early_bird rate when currentApplicableRate is early_bird", () => { render( diff --git a/src/components/mui/FormItemTable/__tests__/helpers.test.js b/src/components/mui/FormItemTable/__tests__/helpers.test.js index 3d80e9de..b5f99c8f 100644 --- a/src/components/mui/FormItemTable/__tests__/helpers.test.js +++ b/src/components/mui/FormItemTable/__tests__/helpers.test.js @@ -53,6 +53,29 @@ describe("isItemAvailable", () => { const item = { rates: { early_bird: null } }; expect(isItemAvailable(item, "early_bird")).toBe(false); }); + + test("returns true when a custom rate is set and no rate applies for the period", () => { + const item = { rates: { early_bird: 100 } }; + // Strict toBe(true) also pins the boolean coercion: without it the + // short-circuit would hand back the raw custom rate (5000). + expect(isItemAvailable(item, "expired", 5000)).toBe(true); + }); + + test("returns true when a custom rate is set and the item has no rates at all", () => { + expect(isItemAvailable({}, "early_bird", 5000)).toBe(true); + }); + + test("returns false when the custom rate is 0 and no rate applies", () => { + // 0 is the "no custom rate" sentinel on both sides of the wire — it must + // not make an otherwise-unavailable item available. + const item = { rates: { early_bird: 100 } }; + expect(isItemAvailable(item, "expired", 0)).toBe(false); + }); + + test("stays available on the applicable rate when no custom rate is passed", () => { + const item = { rates: { early_bird: 100 } }; + expect(isItemAvailable(item, "early_bird", 0)).toBe(true); + }); }); describe("hasDrivingQuantityField", () => {