diff --git a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap index 718ab75f8..496d9e522 100644 --- a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap @@ -358,6 +358,9 @@ Object { "FETCH_COLLECT_POINTS_FAILURE": "@farfetch/blackout-redux/FETCH_COLLECT_POINTS_FAILURE", "FETCH_COLLECT_POINTS_REQUEST": "@farfetch/blackout-redux/FETCH_COLLECT_POINTS_REQUEST", "FETCH_COLLECT_POINTS_SUCCESS": "@farfetch/blackout-redux/FETCH_COLLECT_POINTS_SUCCESS", + "FETCH_PACKAGING_OPTIONS_FAILURE": "@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_FAILURE", + "FETCH_PACKAGING_OPTIONS_REQUEST": "@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_REQUEST", + "FETCH_PACKAGING_OPTIONS_SUCCESS": "@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_SUCCESS", "REMOVE_CHECKOUT_ORDER_CONTEXT_FAILURE": "@farfetch/blackout-redux/REMOVE_CHECKOUT_ORDER_CONTEXT_FAILURE", "REMOVE_CHECKOUT_ORDER_CONTEXT_REQUEST": "@farfetch/blackout-redux/REMOVE_CHECKOUT_ORDER_CONTEXT_REQUEST", "REMOVE_CHECKOUT_ORDER_CONTEXT_SUCCESS": "@farfetch/blackout-redux/REMOVE_CHECKOUT_ORDER_CONTEXT_SUCCESS", @@ -733,6 +736,8 @@ Object { "fetchOrderItemAvailableActivitiesFactory": [Function], "fetchOrderReturnOptions": [Function], "fetchOrderReturnOptionsFactory": [Function], + "fetchPackagingOptions": [Function], + "fetchPackagingOptionsFactory": [Function], "fetchPaymentIntent": [Function], "fetchPaymentIntentCharge": [Function], "fetchPaymentIntentChargeFactory": [Function], @@ -1044,6 +1049,8 @@ Object { "getOrderReturns": [Function], "getOrderSummaries": [Function], "getOrders": [Function], + "getPackagingOptionsError": [Function], + "getPackagingOptionsResult": [Function], "getPaymentIntentChargeError": [Function], "getPaymentIntentChargeResult": [Function], "getPaymentIntentError": [Function], @@ -1309,6 +1316,7 @@ Object { "isLogoutLoading": [Function], "isOrderFetched": [Function], "isOrderLoading": [Function], + "isPackagingOptionsLoading": [Function], "isPaymentIntentChargeLoading": [Function], "isPaymentIntentFetched": [Function], "isPaymentIntentLoading": [Function], @@ -1477,6 +1485,7 @@ Object { "@farfetch/blackout-redux/RESET_ORDER_RETURN_OPTIONS_ENTITIES": [Function], }, "ordersReducer": [Function], + "packagingOptionsReducer": [Function], "paymentsActionTypes": Object { "CREATE_PAYMENT_INTENT_CHARGE_FAILURE": "@farfetch/blackout-redux/CREATE_PAYMENT_INTENT_CHARGE_FAILURE", "CREATE_PAYMENT_INTENT_CHARGE_REQUEST": "@farfetch/blackout-redux/CREATE_PAYMENT_INTENT_CHARGE_REQUEST", diff --git a/packages/redux/src/checkout/actionTypes.ts b/packages/redux/src/checkout/actionTypes.ts index 3e2e37b1c..9315b1000 100644 --- a/packages/redux/src/checkout/actionTypes.ts +++ b/packages/redux/src/checkout/actionTypes.ts @@ -514,3 +514,19 @@ export const REMOVE_CHECKOUT_ORDER_CONTEXT_REQUEST = */ export const REMOVE_CHECKOUT_ORDER_CONTEXT_SUCCESS = '@farfetch/blackout-redux/REMOVE_CHECKOUT_ORDER_CONTEXT_SUCCESS'; + +/** + * Action type dispatched when the packaging options request fails. + */ +export const FETCH_PACKAGING_OPTIONS_FAILURE = + '@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_FAILURE'; +/** + * Action type dispatched when the packaging options request starts. + */ +export const FETCH_PACKAGING_OPTIONS_REQUEST = + '@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_REQUEST'; +/** + * Action type dispatched when the fetch packaging options request succeeds. + */ +export const FETCH_PACKAGING_OPTIONS_SUCCESS = + '@farfetch/blackout-redux/FETCH_PACKAGING_OPTIONS_SUCCESS'; diff --git a/packages/redux/src/checkout/index.ts b/packages/redux/src/checkout/index.ts index 47763a175..316b907f4 100644 --- a/packages/redux/src/checkout/index.ts +++ b/packages/redux/src/checkout/index.ts @@ -2,6 +2,7 @@ export * as checkoutActionTypes from './actionTypes.js'; export * from './actions/index.js'; export * from './actions/factories/index.js'; +export * from './packaging/index.js'; export * from './selectors.js'; export { diff --git a/packages/redux/src/checkout/packaging/__tests__/reducer.test.ts b/packages/redux/src/checkout/packaging/__tests__/reducer.test.ts new file mode 100644 index 000000000..538a7e54a --- /dev/null +++ b/packages/redux/src/checkout/packaging/__tests__/reducer.test.ts @@ -0,0 +1,59 @@ +import * as actionTypes from '../../actionTypes.js'; +import reducer, * as fromReducer from '../reducer.js'; +import type { PackagingOptionsState } from '../types/index.js'; + +let initialState: PackagingOptionsState; + +describe('Packaging Options reducer', () => { + beforeEach(() => { + initialState = fromReducer.INITIAL_STATE; + }); + + it.each([actionTypes.FETCH_PACKAGING_OPTIONS_REQUEST])( + 'should handle %s action type', + actionType => { + expect(reducer(undefined, { type: actionType })).toEqual({ + error: initialState.error, + isLoading: true, + result: null, + }); + }, + ); + + it.each([actionTypes.FETCH_PACKAGING_OPTIONS_FAILURE])( + 'should handle %s action type', + actionType => { + const error = 'foo'; + const reducerResult = reducer(undefined, { + payload: { error }, + type: actionType, + }); + const expectedResult = { + error, + isLoading: false, + result: null, + }; + + expect(reducerResult).toEqual(expectedResult); + }, + ); + + it.each([actionTypes.FETCH_PACKAGING_OPTIONS_SUCCESS])( + 'should handle %s action type', + actionType => { + const result = 'foo'; + + const reducerResult = reducer(undefined, { + payload: { result }, + type: actionType, + }); + const expectedResult = { + error: initialState.error, + isLoading: false, + result, + }; + + expect(reducerResult).toEqual(expectedResult); + }, + ); +}); diff --git a/packages/redux/src/checkout/packaging/__tests__/selectors.test.ts b/packages/redux/src/checkout/packaging/__tests__/selectors.test.ts new file mode 100644 index 000000000..a75bcd81f --- /dev/null +++ b/packages/redux/src/checkout/packaging/__tests__/selectors.test.ts @@ -0,0 +1,47 @@ +import * as packagingOptionsReducer from '../reducer.js'; +import * as selectors from '../selectors.js'; +import { mockPackagingOptionsState } from 'tests/__fixtures__/checkout/index.mjs'; +import type { StoreState } from '../../../index.js'; + +describe('packaging options redux selectors', () => { + beforeEach(jest.clearAllMocks); + + it('should get the packaging options result property from state', () => { + const spy = jest.spyOn(packagingOptionsReducer, 'getPackagingOptions'); + + expect( + selectors.getPackagingOptionsResult( + mockPackagingOptionsState as StoreState, + ), + ).toEqual(mockPackagingOptionsState.packagingOptions.result); + expect(spy).toHaveBeenCalledWith( + mockPackagingOptionsState.packagingOptions, + ); + }); + + it('should get the packaging options error property from state', () => { + const spy = jest.spyOn(packagingOptionsReducer, 'getPackagingOptions'); + + expect( + selectors.getPackagingOptionsError( + mockPackagingOptionsState as StoreState, + ), + ).toEqual(mockPackagingOptionsState.packagingOptions.error); + expect(spy).toHaveBeenCalledWith( + mockPackagingOptionsState.packagingOptions, + ); + }); + + it('should get the packaging options isLoading property from state', () => { + const spy = jest.spyOn(packagingOptionsReducer, 'getPackagingOptions'); + + expect( + selectors.isPackagingOptionsLoading( + mockPackagingOptionsState as StoreState, + ), + ).toEqual(mockPackagingOptionsState.packagingOptions.isLoading); + expect(spy).toHaveBeenCalledWith( + mockPackagingOptionsState.packagingOptions, + ); + }); +}); diff --git a/packages/redux/src/checkout/packaging/actions/factories/fetchPackagingOptionsFactory.ts b/packages/redux/src/checkout/packaging/actions/factories/fetchPackagingOptionsFactory.ts new file mode 100644 index 000000000..0d0b2b05f --- /dev/null +++ b/packages/redux/src/checkout/packaging/actions/factories/fetchPackagingOptionsFactory.ts @@ -0,0 +1,47 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type Config, + type GetPackagingOptions, + type GetPackagingOptionsQuery, + type PackagingOption, + toBlackoutError, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; + +/** + * Method responsible for get all packaging options. + * + * @param getPackagingOptions - Get all packaging options. + * + * @returns Thunk factory. + */ +const fetchPackagingOptionsFactory = + (getPackagingOptions: GetPackagingOptions) => + (query: GetPackagingOptionsQuery, config?: Config) => + async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: actionTypes.FETCH_PACKAGING_OPTIONS_REQUEST, + }); + + const result = await getPackagingOptions(query, config); + + dispatch({ + payload: result, + type: actionTypes.FETCH_PACKAGING_OPTIONS_SUCCESS, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.FETCH_PACKAGING_OPTIONS_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default fetchPackagingOptionsFactory; diff --git a/packages/redux/src/checkout/packaging/actions/factories/index.ts b/packages/redux/src/checkout/packaging/actions/factories/index.ts new file mode 100644 index 000000000..19419bea8 --- /dev/null +++ b/packages/redux/src/checkout/packaging/actions/factories/index.ts @@ -0,0 +1,5 @@ +/** + * Packaging options actions factories. + */ + +export { default as fetchPackagingOptionsFactory } from './fetchPackagingOptionsFactory.js'; diff --git a/packages/redux/src/checkout/packaging/actions/fetchPackagingOptions.ts b/packages/redux/src/checkout/packaging/actions/fetchPackagingOptions.ts new file mode 100644 index 000000000..f58030abb --- /dev/null +++ b/packages/redux/src/checkout/packaging/actions/fetchPackagingOptions.ts @@ -0,0 +1,7 @@ +import { fetchPackagingOptionsFactory } from './factories/index.js'; +import { getPackagingOptions } from '@farfetch/blackout-client'; + +/** + * Fetch all packaging options. + */ +export default fetchPackagingOptionsFactory(getPackagingOptions); diff --git a/packages/redux/src/checkout/packaging/actions/index.ts b/packages/redux/src/checkout/packaging/actions/index.ts new file mode 100644 index 000000000..38e2c78fc --- /dev/null +++ b/packages/redux/src/checkout/packaging/actions/index.ts @@ -0,0 +1,5 @@ +/** + * Packaging actions. + */ + +export { default as fetchPackagingOptions } from './fetchPackagingOptions.js'; diff --git a/packages/redux/src/checkout/packaging/index.ts b/packages/redux/src/checkout/packaging/index.ts new file mode 100644 index 000000000..a35d23c4a --- /dev/null +++ b/packages/redux/src/checkout/packaging/index.ts @@ -0,0 +1,5 @@ +export * from './actions/index.js'; +export * from './actions/factories/index.js'; +export * from './types/index.js'; +export { default as packagingOptionsReducer } from './reducer.js'; +export * from './selectors.js'; diff --git a/packages/redux/src/checkout/packaging/reducer.ts b/packages/redux/src/checkout/packaging/reducer.ts new file mode 100644 index 000000000..098f4233e --- /dev/null +++ b/packages/redux/src/checkout/packaging/reducer.ts @@ -0,0 +1,69 @@ +import * as actionTypes from './../actionTypes.js'; +import { type AnyAction, combineReducers } from 'redux'; +import { createReducerWithResult } from '../../helpers/reducerFactory.js'; +import type { PackagingOptionsState } from './index.js'; + +export const INITIAL_STATE: PackagingOptionsState = { + error: null, + result: null, + isLoading: false, +}; + +export const packagingOptions = createReducerWithResult( + 'FETCH_PACKAGING_OPTIONS', + INITIAL_STATE, + actionTypes, +); + +const error = ( + state = INITIAL_STATE.error, + action: AnyAction, +): PackagingOptionsState['error'] => { + switch (action.type) { + case actionTypes.FETCH_PACKAGING_OPTIONS_FAILURE: + return action.payload.error; + case actionTypes.FETCH_PACKAGING_OPTIONS_REQUEST: + return INITIAL_STATE.error; + default: + return state; + } +}; + +const isLoading = ( + state = INITIAL_STATE.isLoading, + action: AnyAction, +): PackagingOptionsState['isLoading'] => { + switch (action.type) { + case actionTypes.FETCH_PACKAGING_OPTIONS_REQUEST: + return true; + case actionTypes.FETCH_PACKAGING_OPTIONS_SUCCESS: + case actionTypes.FETCH_PACKAGING_OPTIONS_FAILURE: + return INITIAL_STATE.isLoading; + default: + return state; + } +}; + +const result = ( + state = INITIAL_STATE.result, + action: AnyAction, +): PackagingOptionsState['result'] => { + switch (action.type) { + case actionTypes.FETCH_PACKAGING_OPTIONS_SUCCESS: + return action.payload.result; + default: + return state; + } +}; + +export const getPackagingOptions = ( + state: PackagingOptionsState, +): PackagingOptionsState => state; + +const packagingOptionsReducer = combineReducers({ + result, + error, + isLoading, +}); + +export default packagingOptionsReducer; diff --git a/packages/redux/src/checkout/packaging/selectors.ts b/packages/redux/src/checkout/packaging/selectors.ts new file mode 100644 index 000000000..1aef6b0e8 --- /dev/null +++ b/packages/redux/src/checkout/packaging/selectors.ts @@ -0,0 +1,33 @@ +import { getPackagingOptions } from './reducer.js'; +import type { PackagingOptionsState, StoreState } from '../../index.js'; + +/** + * Returns the packaging options. + * + * @param state - Application state. + * + * @returns Packaging Options result. + */ +export const getPackagingOptionsResult = (state: StoreState) => + getPackagingOptions(state.packagingOptions as PackagingOptionsState).result; + +/** + * Returns the loading status for the packaging options operation. + * + * @param state - Application state. + * + * @returns Packaging Options operation Loading status. + */ +export const isPackagingOptionsLoading = (state: StoreState) => + getPackagingOptions(state.packagingOptions as PackagingOptionsState) + .isLoading; + +/** + * Returns the error status for the packaging options operation. + * + * @param state - Application state. + * + * @returns Packaging Options operation error. + */ +export const getPackagingOptionsError = (state: StoreState) => + getPackagingOptions(state.packagingOptions as PackagingOptionsState).error; diff --git a/packages/redux/src/checkout/packaging/types/index.ts b/packages/redux/src/checkout/packaging/types/index.ts new file mode 100644 index 000000000..33bd8af88 --- /dev/null +++ b/packages/redux/src/checkout/packaging/types/index.ts @@ -0,0 +1,7 @@ +import type { BlackoutError, PackagingOption } from '@farfetch/blackout-client'; + +export type PackagingOptionsState = { + result: PackagingOption[] | null; + isLoading: boolean; + error: BlackoutError | null; +}; diff --git a/packages/redux/src/entities/schemas/packagingOptions.ts b/packages/redux/src/entities/schemas/packagingOptions.ts new file mode 100644 index 000000000..4ae61ab5c --- /dev/null +++ b/packages/redux/src/entities/schemas/packagingOptions.ts @@ -0,0 +1,3 @@ +import { schema } from 'normalizr'; + +export default new schema.Entity('packagingOptions'); diff --git a/packages/redux/src/entities/types/index.ts b/packages/redux/src/entities/types/index.ts index 4224abf86..6f678410d 100644 --- a/packages/redux/src/entities/types/index.ts +++ b/packages/redux/src/entities/types/index.ts @@ -51,3 +51,4 @@ export * from './user.types.js'; export * from './raffle.types.js'; export * from './raffleParticipations.types.js'; export * from './raffleEstimation.types.js'; +export * from './packagingOptions.types.js'; diff --git a/packages/redux/src/entities/types/packagingOptions.types.ts b/packages/redux/src/entities/types/packagingOptions.types.ts new file mode 100644 index 000000000..1834695d1 --- /dev/null +++ b/packages/redux/src/entities/types/packagingOptions.types.ts @@ -0,0 +1,3 @@ +import type { PackagingOption } from '@farfetch/blackout-client'; + +export type PackagingOptionsEntity = PackagingOption[]; diff --git a/packages/redux/src/types/storeState.types.ts b/packages/redux/src/types/storeState.types.ts index 9d5fde06a..ed60a6667 100644 --- a/packages/redux/src/types/storeState.types.ts +++ b/packages/redux/src/types/storeState.types.ts @@ -73,6 +73,7 @@ import type { LocaleState } from '../locale/types/index.js'; import type { LoyaltyState } from '../loyalty/types/index.js'; import type { MerchantsLocationsState } from '../merchantsLocations/types/index.js'; import type { OrdersState } from '../orders/types/index.js'; +import type { PackagingOptionsState } from '../index.js'; import type { PaymentsState } from '../payments/types/index.js'; import type { ProductsState } from '../products/types/index.js'; import type { PromotionEvaluationsState } from '../promotionEvaluations/types/index.js'; @@ -191,6 +192,7 @@ export type StoreState = Partial<{ user: UserEntity; wishlistItems: Record; wishlistSets: Record; + // packagingOptions: PackagingOptionsEntity; // Keep adding/changing here as we migrate chunks }>; addresses: AddressesState; @@ -220,5 +222,6 @@ export type StoreState = Partial<{ themes: ThemeState; users: UsersState; wishlist: WishlistsState; + packagingOptions: PackagingOptionsState; // Keep adding here as we migrate chunks }>; diff --git a/tests/__fixtures__/checkout/packagingOptions.fixtures.mts b/tests/__fixtures__/checkout/packagingOptions.fixtures.mts index e28adcc0b..71a023400 100644 --- a/tests/__fixtures__/checkout/packagingOptions.fixtures.mts +++ b/tests/__fixtures__/checkout/packagingOptions.fixtures.mts @@ -1,3 +1,11 @@ export const mockPackagingOption = 'Basic'; export const mockPackagingOptionsResponse = [mockPackagingOption, 'Signature']; + +export const mockPackagingOptionsState = { + packagingOptions: { + error: null, + isLoading: false, + result: mockPackagingOptionsResponse, + }, +};