diff --git a/package.json b/package.json index ab1950522..e68818651 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.58", + "openstack-uicore-foundation": "5.0.62", "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 a73dddca0..e8af2c069 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.58: - version "5.0.58" - resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.58.tgz#641aa67d2d231373dda1e5f5edaebbd68e908320" - integrity sha512-9p28r/arDv6Y6RF3KT/xppOyO8gvo+QdZqxGwsk5+6/AEBtcXVD9nRBV2jeMfO0cZKiEe8ocXFElptymSN7WWg== +openstack-uicore-foundation@5.0.62: + version "5.0.62" + resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.62.tgz#d7e680d5df00b9456b48186e156804c05826ddcf" + integrity sha512-oRtVrVNvvYGvfiwK8gU0mAiXMQJegaQ4mH6vMJtT2vmtAZAhsUf2P8aOT4ie7ER8BggLjYoYG8M2qNyz7Kk1qg== dependencies: use-sync-external-store "^1.6.0"