Commit 8d21bf4f80 for woocommerce

commit 8d21bf4f80b4dd803990cfc49e289dfd7833131e
Author: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
Date:   Tue Feb 3 12:51:25 2026 +0100

    Refactor `getQuantityConstraints` and `normalizeProductFromStore` in iAPI cart store (#63033)

    * Inline getQuantityConstraints logic into getProductData

    Inline the quantity constraints logic directly into normalizeProductFromStore
    to improve code readability by eliminating the need to navigate between
    multiple functions to understand how product data is built.

    - Remove getQuantityConstraints function and QuantityConstraints type
    - Move min, max, step computation directly into normalizeProductFromStore
    - Maintains identical behavior for quantity constraint computation

    Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

    * Inline normalizeProductFromStore logic into getProductData

    Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

    * feat: US-003 - Clean up unused types and imports

    Remove the unused QuantitySelectorStyleProps type which was defined
    but never imported or used anywhere in the codebase.

    Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

    * Unify `getProductData` return

    * Remove destructuring

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Add unit tests

    * Fix linting

    ---------

    Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/63033-wooplug-6209-refactor-getquantityconstraints-and b/plugins/woocommerce/changelog/63033-wooplug-6209-refactor-getquantityconstraints-and
new file mode 100644
index 0000000000..03930ef727
--- /dev/null
+++ b/plugins/woocommerce/changelog/63033-wooplug-6209-refactor-getquantityconstraints-and
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Refactor `getQuantityConstraints` and `normalizeProductFromStore` in iAPI cart store
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/__tests__/frontend.test.ts
new file mode 100644
index 0000000000..15f6ba49af
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/__tests__/frontend.test.ts
@@ -0,0 +1,276 @@
+/**
+ * External dependencies
+ */
+import type { ProductsStoreState } from '@woocommerce/stores/woocommerce/products';
+
+/**
+ * Internal dependencies
+ */
+import type { NormalizedProductData } from '../types';
+
+let mockProductsState: {
+	products: Record<
+		number,
+		Partial< ProductsStoreState[ 'products' ][ number ] >
+	>;
+	productVariations: Record<
+		number,
+		Partial< ProductsStoreState[ 'productVariations' ][ number ] >
+	>;
+};
+
+jest.mock(
+	'@wordpress/interactivity',
+	() => ( {
+		store: jest.fn( ( name ) => {
+			if ( name === 'woocommerce/products' ) {
+				return { state: mockProductsState };
+			}
+			return { state: {} };
+		} ),
+	} ),
+	{ virtual: true }
+);
+
+jest.mock( '@woocommerce/stores/woocommerce/product-data', () => ( {} ), {
+	virtual: true,
+} );
+
+jest.mock( '@woocommerce/stores/woocommerce/products', () => ( {} ), {
+	virtual: true,
+} );
+
+describe( 'getProductData', () => {
+	let getProductData: (
+		id: number,
+		selectedAttributes: Array< { attribute: string; value: string } >
+	) => NormalizedProductData | null;
+
+	beforeEach( () => {
+		jest.resetModules();
+		mockProductsState = {
+			products: {},
+			productVariations: {},
+		};
+
+		jest.isolateModules( () => {
+			//eslint-disable-next-line @typescript-eslint/no-var-requires
+			const frontend = require( '../frontend' );
+			getProductData = frontend.getProductData;
+		} );
+	} );
+
+	it( 'returns null when product is not in store', () => {
+		mockProductsState.products = {};
+
+		const result = getProductData( 999, [] );
+		expect( result ).toBeNull();
+	} );
+
+	it( 'returns product data with default quantity constraints', () => {
+		mockProductsState.products = {
+			1: {
+				id: 1,
+				type: 'simple',
+				is_purchasable: true,
+				is_in_stock: true,
+				sold_individually: false,
+			},
+		};
+
+		const result = getProductData( 1, [] );
+
+		expect( result ).toEqual( {
+			id: 1,
+			type: 'simple',
+			is_in_stock: true,
+			sold_individually: false,
+			min: 1,
+			max: Number.MAX_SAFE_INTEGER,
+			step: 1,
+		} );
+	} );
+
+	it( 'returns product data with custom quantity constraints', () => {
+		mockProductsState.products = {
+			1: {
+				id: 1,
+				type: 'simple',
+				is_purchasable: true,
+				is_in_stock: true,
+				sold_individually: false,
+				add_to_cart: {
+					text: 'Add to cart',
+					description: 'Add to cart',
+					url: '',
+					minimum: 2,
+					maximum: 10,
+					multiple_of: 2,
+					single_text: 'Add to cart',
+				},
+			},
+		};
+
+		const result = getProductData( 1, [] );
+
+		expect( result ).toEqual( {
+			id: 1,
+			type: 'simple',
+			is_in_stock: true,
+			sold_individually: false,
+			min: 2,
+			max: 10,
+			step: 2,
+		} );
+	} );
+
+	it( 'returns max as MAX_SAFE_INTEGER when maximum is 0', () => {
+		mockProductsState.products = {
+			1: {
+				id: 1,
+				type: 'simple',
+				is_purchasable: true,
+				is_in_stock: true,
+				sold_individually: false,
+				add_to_cart: {
+					text: 'Add to cart',
+					description: 'Add to cart',
+					url: '',
+					minimum: 1,
+					maximum: 0,
+					multiple_of: 1,
+					single_text: 'Add to cart',
+				},
+			},
+		};
+
+		const result = getProductData( 1, [] );
+
+		expect( result?.max ).toBe( Number.MAX_SAFE_INTEGER );
+	} );
+
+	describe( 'variable products', () => {
+		it( 'returns variation data when attributes match a variation', () => {
+			mockProductsState.products = {
+				1: {
+					id: 1,
+					type: 'variable',
+					is_purchasable: true,
+					is_in_stock: true,
+					sold_individually: false,
+					add_to_cart: {
+						text: 'Add to cart',
+						description: 'Add to cart',
+						url: '',
+						minimum: 1,
+						maximum: 100,
+						multiple_of: 1,
+						single_text: 'Add to cart',
+					},
+					variations: [
+						{
+							id: 10,
+							attributes: [ { name: 'Color', value: 'red' } ],
+						},
+					],
+				},
+			};
+			mockProductsState.productVariations = {
+				10: {
+					id: 10,
+					type: 'variation',
+					is_purchasable: true,
+					is_in_stock: true,
+					sold_individually: false,
+					add_to_cart: {
+						text: 'Add to cart',
+						description: 'Add to cart',
+						url: '',
+						minimum: 5,
+						maximum: 50,
+						multiple_of: 5,
+						single_text: 'Add to cart',
+					},
+				},
+			};
+
+			const result = getProductData( 1, [
+				{ attribute: 'Color', value: 'red' },
+			] );
+
+			expect( result ).toEqual( {
+				id: 10,
+				type: 'variation',
+				is_in_stock: true,
+				sold_individually: false,
+				min: 5,
+				max: 50,
+				step: 5,
+			} );
+		} );
+
+		it( 'returns null when variation is matched but not in store', () => {
+			mockProductsState.products = {
+				1: {
+					id: 1,
+					type: 'variable',
+					is_purchasable: true,
+					is_in_stock: true,
+					sold_individually: false,
+					variations: [
+						{
+							id: 10,
+							attributes: [ { name: 'Color', value: 'red' } ],
+						},
+					],
+				},
+			};
+			mockProductsState.productVariations = {};
+
+			const result = getProductData( 1, [
+				{ attribute: 'Color', value: 'red' },
+			] );
+
+			expect( result ).toBeNull();
+		} );
+
+		it( 'returns parent product data when no attributes are selected', () => {
+			mockProductsState.products = {
+				1: {
+					id: 1,
+					type: 'variable',
+					is_purchasable: true,
+					is_in_stock: true,
+					sold_individually: false,
+					add_to_cart: {
+						text: 'Add to cart',
+						description: 'Add to cart',
+						url: '',
+						minimum: 1,
+						maximum: 100,
+						multiple_of: 1,
+						single_text: 'Add to cart',
+					},
+					variations: [
+						{
+							id: 10,
+							attributes: [ { name: 'Color', value: 'red' } ],
+						},
+					],
+				},
+			};
+
+			const result = getProductData( 1, [] );
+
+			expect( result ).toEqual( {
+				id: 1,
+				type: 'variable',
+				is_in_stock: true,
+				sold_individually: false,
+				min: 1,
+				max: 100,
+				step: 1,
+			} );
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
index 8eb4ca19cc..06f7b2097f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
@@ -11,7 +11,6 @@ import '@woocommerce/stores/woocommerce/products';
 import type { Store as StoreNotices } from '@woocommerce/stores/store-notices';
 import type { ProductDataStore } from '@woocommerce/stores/woocommerce/product-data';
 import type { ProductsStore } from '@woocommerce/stores/woocommerce/products';
-import type { ProductResponseItem } from '@woocommerce/types';

 /**
  * Internal dependencies
@@ -37,38 +36,6 @@ export type AddToCartError = {
 	message: string;
 };

-/**
- * Quantity constraints normalized from the Store API format.
- */
-type QuantityConstraints = {
-	min: number;
-	max: number;
-	step: number;
-};
-
-/**
- * Extract quantity constraints from a product in Store API format.
- *
- * @param product The product in Store API format.
- * @return Normalized quantity constraints.
- */
-const getQuantityConstraints = (
-	product: ProductResponseItem | null
-): QuantityConstraints => {
-	if ( ! product ) {
-		return { min: 1, max: Number.MAX_SAFE_INTEGER, step: 1 };
-	}
-
-	const addToCart = product.add_to_cart;
-	const maximum = addToCart?.maximum ?? 0;
-
-	return {
-		min: addToCart?.minimum ?? 1,
-		max: maximum > 0 ? maximum : Number.MAX_SAFE_INTEGER,
-		step: addToCart?.multiple_of ?? 1,
-	};
-};
-
 /**
  * Manually dispatches a 'change' event on the quantity input element.
  *
@@ -108,26 +75,6 @@ const { state: productsState } = store< ProductsStore >(
 	{ lock: universalLock }
 );

-/**
- * Normalize a Store API product into the format expected by consumers.
- *
- * @param product The product in Store API format.
- * @return Normalized product data.
- */
-const normalizeProductFromStore = (
-	product: ProductResponseItem
-): NormalizedProductData | NormalizedVariationData => {
-	const constraints = getQuantityConstraints( product );
-
-	return {
-		id: product.id,
-		type: product.type,
-		is_in_stock: product.is_purchasable && product.is_in_stock,
-		sold_individually: product.sold_individually,
-		...constraints,
-	};
-};
-
 export const getProductData = (
 	id: number,
 	selectedAttributes: SelectedAttributes[]
@@ -138,6 +85,9 @@ export const getProductData = (
 		return null;
 	}

+	// Determine which product to use for the response.
+	let product = productFromStore;
+
 	// For variable products with selected attributes, find the matching variation.
 	if (
 		productFromStore.type === 'variable' &&
@@ -151,16 +101,27 @@ export const getProductData = (
 		if ( matchedVariation ) {
 			const variation =
 				productsState.productVariations[ matchedVariation.id ];
-			if ( variation ) {
-				return normalizeProductFromStore( variation );
+			if ( ! variation ) {
+				// Variation was matched but its data isn't in the store.
+				// Return null to prevent using stale parent product data.
+				return null;
 			}
-			// Variation was matched but its data isn't in the store.
-			// Return null to prevent using stale parent product data.
-			return null;
+			product = variation;
 		}
 	}

-	return normalizeProductFromStore( productFromStore );
+	const { add_to_cart: addToCart } = product;
+	const maximum = addToCart?.maximum ?? 0;
+
+	return {
+		id: product.id,
+		type: product.type,
+		is_in_stock: product.is_purchasable && product.is_in_stock,
+		sold_individually: product.sold_individually,
+		min: addToCart?.minimum ?? 1,
+		max: maximum > 0 ? maximum : Number.MAX_SAFE_INTEGER,
+		step: addToCart?.multiple_of ?? 1,
+	};
 };

 export const getNewQuantity = (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/types.ts
index 6744e69ed9..0c57d09442 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/types.ts
@@ -6,8 +6,6 @@ import {
 	VariationData,
 } from '@woocommerce/stores/woocommerce/cart';

-export type QuantitySelectorStyleProps = 'input' | 'stepper';
-
 export interface Attributes {
 	className?: string;
 }