diff --git a/package.json b/package.json index 5245ac019..abe08973c 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.33", "mui-color-input": "^9.0.0", - "openstack-uicore-foundation": "5.0.56", + "openstack-uicore-foundation": "5.0.59-beta.4", "p-limit": "^6.1.0", "path-browserify": "^1.0.1", "postcss-loader": "^6.2.1", diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js index 6482f2aee..2c421ee83 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js @@ -98,6 +98,7 @@ import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import showConfirmDialog from "../../../../../../../../components/mui/showConfirmDialog"; import EditCartForm from "../edit-cart-form"; +import { buildGlobalQuantitySchema } from "../quantity-schema"; /* eslint-enable import/first */ const middlewares = [thunk]; @@ -577,4 +578,71 @@ describe("EditCartForm", () => { }); }); }); + + describe("EditForm - buildInitialValues (out-of-stock default_quantity)", () => { + const buildQuantityInitialValue = (item) => { + const hasStock = + !item.is_sold_out && item.remaining_quantity_sponsor !== 0; + return hasStock + ? item.quantity || item.default_quantity || 0 + : item.quantity || 0; + }; + + it("ignores default_quantity when the item is sold out for the show", () => { + const item = { is_sold_out: true, default_quantity: 1 }; + expect(buildQuantityInitialValue(item)).toBe(0); + }); + + it("ignores default_quantity when the sponsor's remaining quantity is 0", () => { + const item = { remaining_quantity_sponsor: 0, default_quantity: 1 }; + expect(buildQuantityInitialValue(item)).toBe(0); + }); + + it("keeps the sponsor's own existing quantity even when out of stock", () => { + const item = { is_sold_out: true, quantity: 3, default_quantity: 1 }; + expect(buildQuantityInitialValue(item)).toBe(3); + }); + + it("still applies default_quantity when the item has stock", () => { + const item = { is_sold_out: false, default_quantity: 1 }; + expect(buildQuantityInitialValue(item)).toBe(1); + }); + }); + + describe("EditForm - buildValidationSchema (quantity cap)", () => { + it("rejects a quantity above remaining_quantity_show when it is the tighter axis", async () => { + const schema = buildGlobalQuantitySchema({ + remaining_quantity_show: 2, + remaining_quantity_sponsor: 5 + }); + await expect(schema.isValid(3)).resolves.toBe(false); + await expect(schema.isValid(2)).resolves.toBe(true); + }); + + it("rejects a quantity above remaining_quantity_sponsor when it is the tighter axis", async () => { + const schema = buildGlobalQuantitySchema({ + remaining_quantity_show: 8, + remaining_quantity_sponsor: 3 + }); + await expect(schema.isValid(4)).resolves.toBe(false); + await expect(schema.isValid(3)).resolves.toBe(true); + }); + + it("applies no upper bound when both remaining quantities are null", async () => { + const schema = buildGlobalQuantitySchema({ + remaining_quantity_show: null, + remaining_quantity_sponsor: null + }); + await expect(schema.isValid(1000)).resolves.toBe(true); + }); + + it("rejects any positive quantity when remaining_quantity_show is 0 (boundary)", async () => { + const schema = buildGlobalQuantitySchema({ + remaining_quantity_show: 0, + remaining_quantity_sponsor: 5 + }); + await expect(schema.isValid(1)).resolves.toBe(false); + await expect(schema.isValid(0)).resolves.toBe(true); + }); + }); }); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js index 092d71263..53da95a94 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js @@ -25,6 +25,7 @@ import MuiFormItemTable, { } from "openstack-uicore-foundation/lib/components/mui/form-item-table"; import { DISCOUNT_TYPES } from "../../../../../../../utils/constants"; import showConfirmDialog from "../../../../../../../components/mui/showConfirmDialog"; +import { buildGlobalQuantitySchema } from "./quantity-schema"; const parseValue = (item, timeZone) => { switch (item.type) { @@ -146,8 +147,10 @@ const buildInitialValues = (form, timeZone) => { // add notes acc[`i-${item.form_item_id}-c-global-f-notes`] = item.notes || ""; // if no quantity inputs we add the global quantity input - acc[`i-${item.form_item_id}-c-global-f-quantity`] = - item.quantity || item.default_quantity || 0; + const hasStock = !item.is_sold_out && item.remaining_quantity_sponsor !== 0; + acc[`i-${item.form_item_id}-c-global-f-quantity`] = hasStock + ? item.quantity || item.default_quantity || 0 + : item.quantity || 0; // custom rate acc[`i-${item.form_item_id}-c-global-f-custom_rate`] = item.custom_rate || item.rates.custom || 0; @@ -168,16 +171,7 @@ const buildValidationSchema = (items) => { // notes acc[`i-${item.form_item_id}-c-global-f-notes`] = yup.string(); // validation for the global quantity input - let globalQtySchema = yup.number().min(0, " "); - - if (item.quantity_limit_per_sponsor > 0) { - globalQtySchema = globalQtySchema.max( - item.quantity_limit_per_sponsor, - " " - ); - } - globalQtySchema = globalQtySchema.required(" "); - acc[quantityKey] = globalQtySchema; + acc[quantityKey] = buildGlobalQuantitySchema(item); // custom rate acc[`i-${item.form_item_id}-c-global-f-custom_rate`] = yup.number(); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/quantity-schema.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/quantity-schema.js new file mode 100644 index 000000000..7f544cfdb --- /dev/null +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/quantity-schema.js @@ -0,0 +1,16 @@ +import * as yup from "yup"; + +// The sponsor-facing quantity input is capped by whichever axis is tighter: +// what's left for the show, or what's left for this sponsor specifically. +// Either field being null means that axis has no cap. +export const buildGlobalQuantitySchema = (item) => { + let schema = yup.number().min(0, " "); + const maxQty = Math.min( + item.remaining_quantity_show ?? Infinity, + item.remaining_quantity_sponsor ?? Infinity + ); + if (Number.isFinite(maxQty)) { + schema = schema.max(maxQty, " "); + } + return schema.required(" "); +}; diff --git a/yarn.lock b/yarn.lock index 49830f251..bd600d329 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9042,10 +9042,10 @@ open@^10.0.3: is-inside-container "^1.0.0" wsl-utils "^0.1.0" -openstack-uicore-foundation@5.0.56: - version "5.0.56" - resolved "https://registry.npmjs.org/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.56.tgz#806dc75481fb136295cf3f33a2d6da6536fa62cd" - integrity sha512-51p1q9Emeiurni5ttDeYFmPa9q0Su1bg49HgyqZLyfBqB1JbaaNwSmyChc590WBRhKYKEdKKqF8lYoVORCWYtA== +openstack-uicore-foundation@5.0.59-beta.4: + version "5.0.59-beta.4" + resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.59-beta.4.tgz#084823ed10acc82b88c56ef532d97aa8a4b84ea2" + integrity sha512-kKpI3+9ctWOg3kp0Hgm8G+BrIfM2WDeuShOACOLqW7zCnme0IjJ837eawFOiwsttUrnokhNg7Pz7DN3uLM+t8A== dependencies: use-sync-external-store "^1.6.0"