diff --git a/packages/client/src/__tests__/__snapshots__/index.test.ts.snap b/packages/client/src/__tests__/__snapshots__/index.test.ts.snap index 4789765ea..af082d677 100644 --- a/packages/client/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/client/src/__tests__/__snapshots__/index.test.ts.snap @@ -1033,6 +1033,7 @@ Object { "deleteToken": [Function], "deleteUserAddress": [Function], "deleteUserAttribute": [Function], + "deleteUserClosetItem": [Function], "deleteUserContact": [Function], "deleteUserDefaultContactAddress": [Function], "deleteUserExternalLogin": [Function], @@ -1146,6 +1147,8 @@ Object { "getUserAttribute": [Function], "getUserAttributes": [Function], "getUserBenefits": [Function], + "getUserCloset": [Function], + "getUserClosets": [Function], "getUserContact": [Function], "getUserContacts": [Function], "getUserCreditBalance": [Function], diff --git a/packages/client/src/exchanges/types/exchangeFilter.types.ts b/packages/client/src/exchanges/types/exchangeFilter.types.ts index 273b1b06d..44079d7e5 100644 --- a/packages/client/src/exchanges/types/exchangeFilter.types.ts +++ b/packages/client/src/exchanges/types/exchangeFilter.types.ts @@ -31,7 +31,7 @@ export type ExchangeFilterCondition = ExchangeFilterLogicOperatorData; export type ExchangeFilterLogicOperatorData = { criteria: ExchangeFilterLogicOperatorCriteria; comparator: ExchangeFilterLogicOperatorComparator; - value: string | string[]; + values: string | string[]; }; export enum ExchangeFilterLogicOperatorCriteria { diff --git a/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts b/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts new file mode 100644 index 000000000..2d812a84e --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts @@ -0,0 +1,16 @@ +import { rest, type RestHandler } from 'msw'; + +const path = '/api/account/v1/users/:userId/closets/:closetId/items/:itemId'; + +const fixtures = { + success: (response: number): RestHandler => + rest.delete(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.delete(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__fixtures__/getUserCloset.fixtures.ts b/packages/client/src/users/closets/__fixtures__/getUserCloset.fixtures.ts new file mode 100644 index 000000000..98dfe5bd7 --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/getUserCloset.fixtures.ts @@ -0,0 +1,17 @@ +import { rest, type RestHandler } from 'msw'; +import type { Closet } from '../index.js'; + +const path = '/api/account/v1/users/:userId/closets/:closetId'; + +const fixtures = { + success: (response: Closet[]): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts b/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts new file mode 100644 index 000000000..8297c26ac --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts @@ -0,0 +1,17 @@ +import { rest, type RestHandler } from 'msw'; +import type { ClosetSummary } from '../index.js'; + +const path = '/api/account/v1/users/:userId/closets'; + +const fixtures = { + success: (response: ClosetSummary[]): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap new file mode 100644 index 000000000..77ff8507b --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deleteUserClosetItem should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/getUserCloset.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/getUserCloset.test.ts.snap new file mode 100644 index 000000000..87ab6c741 --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/getUserCloset.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUserCloset should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap new file mode 100644 index 000000000..191a63afd --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUserClosets should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts b/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts new file mode 100644 index 000000000..ba1ffb314 --- /dev/null +++ b/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts @@ -0,0 +1,44 @@ +import { + closetId, + closetItemId, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { deleteUserClosetItem } from '../index.js'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/deleteUserClosetItem.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('deleteUserClosetItem', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'delete'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + const response = 200; + + mswServer.use(fixtures.success(response)); + + await expect( + deleteUserClosetItem(userId, closetId, closetItemId), + ).resolves.toBe(response); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items/${closetItemId}`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect( + deleteUserClosetItem(userId, closetId, closetItemId), + ).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items/${closetItemId}`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/__tests__/getUserCloset.test.ts b/packages/client/src/users/closets/__tests__/getUserCloset.test.ts new file mode 100644 index 000000000..171696816 --- /dev/null +++ b/packages/client/src/users/closets/__tests__/getUserCloset.test.ts @@ -0,0 +1,40 @@ +import { + closetId, + mockGetUserClosetResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { getUserCloset } from '../index.js'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/getUserCloset.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('getUserCloset', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'get'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + mswServer.use(fixtures.success(mockGetUserClosetResponse)); + + await expect(getUserCloset(userId, closetId)).resolves.toStrictEqual( + mockGetUserClosetResponse, + ); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect(getUserCloset(userId, closetId)).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/__tests__/getUserClosets.test.ts b/packages/client/src/users/closets/__tests__/getUserClosets.test.ts new file mode 100644 index 000000000..fef90a59a --- /dev/null +++ b/packages/client/src/users/closets/__tests__/getUserClosets.test.ts @@ -0,0 +1,39 @@ +import { getUserClosets } from '../index.js'; +import { + mockGetUserClosetsResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/getUserClosets.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('getUserClosets', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'get'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + mswServer.use(fixtures.success(mockGetUserClosetsResponse)); + + await expect(getUserClosets(userId)).resolves.toStrictEqual( + mockGetUserClosetsResponse, + ); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect(getUserClosets(userId)).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/deleteUserClosetItem.ts b/packages/client/src/users/closets/deleteUserClosetItem.ts new file mode 100644 index 000000000..9a56d7dfa --- /dev/null +++ b/packages/client/src/users/closets/deleteUserClosetItem.ts @@ -0,0 +1,39 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { DeleteUserClosetItem } from './types/index.js'; + +/** + * Method responsible for deleting a user closet. + * + * @param userId - Universal identifier of the user. + * @param closetId - Closet identifier. + * @param itemId - Closet item identifier. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const deleteUserCloset: DeleteUserClosetItem = ( + userId, + closetId, + itemId, + config, +) => + client + .delete( + join( + '/account/v1/users', + userId, + '/closets/', + closetId, + '/items/', + itemId, + ), + config, + ) + .then(response => response.status) + .catch(error => { + throw adaptError(error); + }); + +export default deleteUserCloset; diff --git a/packages/client/src/users/closets/getUserCloset.ts b/packages/client/src/users/closets/getUserCloset.ts new file mode 100644 index 000000000..99d433385 --- /dev/null +++ b/packages/client/src/users/closets/getUserCloset.ts @@ -0,0 +1,27 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { GetUserCloset } from './types/index.js'; + +/** + * Method responsible for getting a specific user closet. + * + * @param userId - Universal identifier of the user. + * @param closetId - Closet identifier. + * @param query - Query params. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const getUserCloset: GetUserCloset = (userId, closetId, query, config) => + client + .get( + join('/account/v1/users/', userId, '/closets/', closetId, { query }), + config, + ) + .then(response => response.data) + .catch(error => { + throw adaptError(error); + }); + +export default getUserCloset; diff --git a/packages/client/src/users/closets/getUserClosets.ts b/packages/client/src/users/closets/getUserClosets.ts new file mode 100644 index 000000000..026b23a50 --- /dev/null +++ b/packages/client/src/users/closets/getUserClosets.ts @@ -0,0 +1,22 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { GetUserClosets } from './types/index.js'; + +/** + * Method responsible for getting user closets. + * + * @param userId - Universal identifier of the user. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const getUserClosets: GetUserClosets = (userId, config) => + client + .get(join('/account/v1/users/', userId, '/closets'), config) + .then(response => response.data) + .catch(error => { + throw adaptError(error); + }); + +export default getUserClosets; diff --git a/packages/client/src/users/closets/index.ts b/packages/client/src/users/closets/index.ts new file mode 100644 index 000000000..df1b90d84 --- /dev/null +++ b/packages/client/src/users/closets/index.ts @@ -0,0 +1,4 @@ +export { default as deleteUserClosetItem } from './deleteUserClosetItem.js'; +export { default as getUserCloset } from './getUserCloset.js'; +export { default as getUserClosets } from './getUserClosets.js'; +export * from './types/index.js'; diff --git a/packages/client/src/users/closets/types/closet.types.ts b/packages/client/src/users/closets/types/closet.types.ts new file mode 100644 index 000000000..50237185a --- /dev/null +++ b/packages/client/src/users/closets/types/closet.types.ts @@ -0,0 +1,31 @@ +import type { Brand } from '../../../brands/index.js'; +import type { + Color, + PagedResponse, + ProductImageGroup, + ProductVariantAttribute, +} from '../../../types/index.js'; +import type { Order } from '../../../orders/index.js'; +import type { Price, Product } from '../../../products/index.js'; +import type { ProductCategory } from '../../../categories/index.js'; + +export type Closet = PagedResponse; + +export type ClosetItem = { + id: string; + orderId: Order['id']; + merchantId: number; + productId: Product['result']['id']; + productName: string; + productDescription: string; + attributes: ProductVariantAttribute[]; + images: ProductImageGroup; + categories: ProductCategory[]; + colors: Color[]; + price: Price; + purchasePrice: Price; + brand: Brand; + customAttributes: string; + isAvailable: boolean; + createdDate: string; +}; diff --git a/packages/client/src/users/closets/types/closetSummary.types.ts b/packages/client/src/users/closets/types/closetSummary.types.ts new file mode 100644 index 000000000..6d4fa3ab1 --- /dev/null +++ b/packages/client/src/users/closets/types/closetSummary.types.ts @@ -0,0 +1,5 @@ +export type ClosetSummary = { + id: string; + createdDate: string; + totalItems: number; +}; diff --git a/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts b/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts new file mode 100644 index 000000000..17058c64e --- /dev/null +++ b/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts @@ -0,0 +1,11 @@ +import type { ClosetItem } from './closet.types.js'; +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config } from '../../../types/index.js'; +import type { User } from '../../authentication/types/user.types.js'; + +export type DeleteUserClosetItem = ( + userId: User['id'], + closetId: ClosetSummary['id'], + itemId: ClosetItem['id'], + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/getUserCloset.types.ts b/packages/client/src/users/closets/types/getUserCloset.types.ts new file mode 100644 index 000000000..7745ee405 --- /dev/null +++ b/packages/client/src/users/closets/types/getUserCloset.types.ts @@ -0,0 +1,39 @@ +import type { Closet } from './closet.types.js'; +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config, User } from '../../../index.js'; + +export type GetUserClosetQuerySort = + | 'createdDate' + | 'name' + | 'isAvailable' + | 'brand' + | 'price' + | 'purchasedPrice'; + +export type GetUserClosetQuerySortDirection = 'desc' | 'asc'; + +export type GetUserClosetQuery = { + /** Number of the page to get, starting at 1. The default is 1. */ + page?: number; + /** Size of each page, as a number between 1 and 180. The default is 60. */ + pageSize?: number; + /** The product category id to filter. */ + categoryId?: number; + /** The product color id to filter. */ + colorId?: number; + /** The brand id to filter. */ + brandId?: number; + /** Get products of some sizes, specified by their numeric identifiers, separated by pipes. */ + sizes?: string; + /** Sort by specified value. The default is to sort by created date. */ + sort?: GetUserClosetQuerySort; + /** Sorts in ascending (asc) or descending (desc) order. Default value is desc */ + sortDirection?: GetUserClosetQuerySortDirection; +}; + +export type GetUserCloset = ( + userId: User['id'], + closetId: ClosetSummary['id'], + query?: GetUserClosetQuery, + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/getUserClosets.types.ts b/packages/client/src/users/closets/types/getUserClosets.types.ts new file mode 100644 index 000000000..c35704395 --- /dev/null +++ b/packages/client/src/users/closets/types/getUserClosets.types.ts @@ -0,0 +1,7 @@ +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config, User } from '../../../index.js'; + +export type GetUserClosets = ( + userId: User['id'], + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/index.ts b/packages/client/src/users/closets/types/index.ts new file mode 100644 index 000000000..ab59b4043 --- /dev/null +++ b/packages/client/src/users/closets/types/index.ts @@ -0,0 +1,5 @@ +export * from './closet.types.js'; +export * from './closetSummary.types.js'; +export * from './deleteUserClosetItem.types.js'; +export * from './getUserCloset.types.js'; +export * from './getUserClosets.types.js'; diff --git a/packages/client/src/users/index.ts b/packages/client/src/users/index.ts index ee989cacc..b529430d0 100644 --- a/packages/client/src/users/index.ts +++ b/packages/client/src/users/index.ts @@ -3,12 +3,13 @@ */ export * from './addresses/index.js'; -export * from './authentication/index.js'; export * from './attributes/index.js'; +export * from './authentication/index.js'; export * from './benefits/index.js'; +export * from './closets/index.js'; export * from './contacts/index.js'; export * from './credits/index.js'; export * from './personalIds/index.js'; export * from './preferences/index.js'; -export * from './titles/index.js'; export * from './returns/index.js'; +export * from './titles/index.js'; diff --git a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap index 2e7f0c226..0d4149e35 100644 --- a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap @@ -129,6 +129,8 @@ Object { "areUserAddressesLoading": [Function], "areUserAttributesLoading": [Function], "areUserBenefitsLoading": [Function], + "areUserClosetsFetched": [Function], + "areUserClosetsLoading": [Function], "areUserContactsFetched": [Function], "areUserContactsLoading": [Function], "areUserCreditMovementsLoading": [Function], @@ -782,6 +784,10 @@ Object { "fetchUserAttributesFactory": [Function], "fetchUserBenefits": [Function], "fetchUserBenefitsFactory": [Function], + "fetchUserCloset": [Function], + "fetchUserClosetFactory": [Function], + "fetchUserClosets": [Function], + "fetchUserClosetsFactory": [Function], "fetchUserContact": [Function], "fetchUserContactFactory": [Function], "fetchUserContacts": [Function], @@ -1148,6 +1154,10 @@ Object { "getUserBagId": [Function], "getUserBenefits": [Function], "getUserBenefitsError": [Function], + "getUserClosetError": [Function], + "getUserClosetResult": [Function], + "getUserClosets": [Function], + "getUserClosetsError": [Function], "getUserContacts": [Function], "getUserContactsError": [Function], "getUserCreditBalanceError": [Function], @@ -1291,6 +1301,8 @@ Object { "isUpdateOrderItemLoading": [Function], "isUserAddressFetched": [Function], "isUserAddressLoading": [Function], + "isUserClosetFetched": [Function], + "isUserClosetLoading": [Function], "isUserCreditBalanceLoading": [Function], "isUserDefaultPersonalIdLoading": [Function], "isUserLoading": [Function], @@ -1603,6 +1615,8 @@ Object { "removeUserAddressFactory": [Function], "removeUserAttribute": [Function], "removeUserAttributeFactory": [Function], + "removeUserClosetItem": [Function], + "removeUserClosetItemFactory": [Function], "removeUserContact": [Function], "removeUserContactFactory": [Function], "removeUserDefaultContactAddress": [Function], @@ -1672,6 +1686,8 @@ Object { "resetUpdateCheckoutOrderItem": [Function], "resetUser": [Function], "resetUserAddresses": [Function], + "resetUserCloset": [Function], + "resetUserClosets": [Function], "resetUserCreditBalance": [Function], "resetUserSubscriptions": [Function], "resetWishlist": [Function], @@ -1899,6 +1915,12 @@ Object { "FETCH_USER_BENEFITS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_FAILURE", "FETCH_USER_BENEFITS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_REQUEST", "FETCH_USER_BENEFITS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_SUCCESS", + "FETCH_USER_CLOSETS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_FAILURE", + "FETCH_USER_CLOSETS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_REQUEST", + "FETCH_USER_CLOSETS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS", + "FETCH_USER_CLOSET_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CLOSET_FAILURE", + "FETCH_USER_CLOSET_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CLOSET_REQUEST", + "FETCH_USER_CLOSET_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CLOSET_SUCCESS", "FETCH_USER_CONTACTS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_FAILURE", "FETCH_USER_CONTACTS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_REQUEST", "FETCH_USER_CONTACTS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_SUCCESS", @@ -1965,6 +1987,9 @@ Object { "REMOVE_USER_ATTRIBUTE_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_FAILURE", "REMOVE_USER_ATTRIBUTE_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_REQUEST", "REMOVE_USER_ATTRIBUTE_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_SUCCESS", + "REMOVE_USER_CLOSET_ITEM_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_FAILURE", + "REMOVE_USER_CLOSET_ITEM_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_REQUEST", + "REMOVE_USER_CLOSET_ITEM_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS", "REMOVE_USER_CONTACT_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_FAILURE", "REMOVE_USER_CONTACT_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_REQUEST", "REMOVE_USER_CONTACT_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_SUCCESS", @@ -1989,6 +2014,8 @@ Object { "RESET_REGISTER": "@farfetch/blackout-redux/RESET_REGISTER", "RESET_REMOVE_TOKEN": "@farfetch/blackout-redux/RESET_REMOVE_TOKEN", "RESET_USER_ADDRESSES": "@farfetch/blackout-redux/RESET_USER_ADDRESSES", + "RESET_USER_CLOSETS": "@farfetch/blackout-redux/RESET_USER_CLOSETS", + "RESET_USER_CLOSET_STATE": "@farfetch/blackout-redux/RESET_USER_CLOSET_STATE", "RESET_USER_ENTITIES": "@farfetch/blackout-redux/RESET_USER_ENTITIES", "RESET_USER_PERSONAL_IDS": "@farfetch/blackout-redux/RESET_USER_PERSONAL_IDS", "RESET_USER_STATE": "@farfetch/blackout-redux/RESET_USER_STATE", diff --git a/packages/redux/src/users/actionTypes.ts b/packages/redux/src/users/actionTypes.ts index 82a2f7346..ac6b806c3 100644 --- a/packages/redux/src/users/actionTypes.ts +++ b/packages/redux/src/users/actionTypes.ts @@ -2,6 +2,8 @@ export * from './addresses/actionTypes.js'; export * from './attributes/actionTypes.js'; export * from './authentication/actionTypes.js'; export * from './benefits/actionTypes.js'; +export * from './closets/actionTypes.js'; +export * from './closets/actionTypes.js'; export * from './contacts/actionTypes.js'; export * from './credits/actionTypes.js'; export * from './personalIds/actionTypes.js'; diff --git a/packages/redux/src/users/actions.ts b/packages/redux/src/users/actions.ts index af885b907..24e0ca8d9 100644 --- a/packages/redux/src/users/actions.ts +++ b/packages/redux/src/users/actions.ts @@ -2,9 +2,10 @@ export * from './addresses/actions/index.js'; export * from './attributes/actions/index.js'; export * from './authentication/actions/index.js'; export * from './benefits/actions/index.js'; +export * from './closets/actions/index.js'; export * from './contacts/actions/index.js'; export * from './credits/actions/index.js'; export * from './personalIds/actions/index.js'; export * from './preferences/actions/index.js'; -export * from './titles/actions/index.js'; export * from './returns/actions/index.js'; +export * from './titles/actions/index.js'; diff --git a/packages/redux/src/users/closets/__tests__/reducer.test.ts b/packages/redux/src/users/closets/__tests__/reducer.test.ts new file mode 100644 index 000000000..acf186a05 --- /dev/null +++ b/packages/redux/src/users/closets/__tests__/reducer.test.ts @@ -0,0 +1,281 @@ +import * as actionTypes from '../actionTypes.js'; +import { + mockGetUserClosetResponse, + mockGetUserClosetsResponse, + mockState, +} from 'tests/__fixtures__/users/index.mjs'; +import { toBlackoutError } from '@farfetch/blackout-client'; +import reducer, * as fromReducer from '../reducer.js'; +import type { UserClosetsState } from '../types/index.js'; + +let initialState: UserClosetsState; +const randomAction = { type: 'this_is_a_random_action' }; + +describe('closets reducer', () => { + beforeEach(() => { + initialState = fromReducer.INITIAL_STATE; + }); + + describe('reset handling', () => { + it('RESET_USER_CLOSETS should return the initial state', () => { + expect( + reducer(mockState.closets, { + type: actionTypes.RESET_USER_CLOSETS, + }), + ).toEqual(initialState); + }); + }); + + describe('error() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).error; + + expect(state).toBe(initialState.error); + expect(state).toBeNull(); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_REQUEST])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + error: toBlackoutError(new Error('error')), + }; + + expect( + reducer(state, { + type: actionType, + }).error, + ).toBe(initialState.error); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_FAILURE])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + error: null, + }; + const error = 'foo'; + + expect( + reducer(state, { + payload: { error }, + type: actionType, + }).error, + ).toBe(error); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = { + ...initialState, + error: toBlackoutError(new Error('error')), + }; + + expect(reducer(state, randomAction).error).toBe(state.error); + }); + }); + + describe('isLoading() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).isLoading; + + expect(state).toBe(initialState.isLoading); + expect(state).toBe(false); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_REQUEST])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + isLoading: false, + }; + + expect( + reducer(state, { + type: actionType, + }).isLoading, + ).toBe(true); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_SUCCESS])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + isLoading: true, + }; + + expect( + reducer(state, { + payload: { result: '' }, + type: actionType, + }).isLoading, + ).toBe(initialState.isLoading); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_FAILURE])( + 'should handle %s action type', + actionType => { + expect( + reducer(undefined, { + payload: { error: '' }, + type: actionType, + }).isLoading, + ).toBe(initialState.isLoading); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = { ...initialState, isLoading: false }; + + expect(reducer(state, randomAction).isLoading).toBe(state.isLoading); + }); + }); + + describe('result() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).result; + + expect(state).toBe(initialState.result); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_SUCCESS])( + 'should handle %s action type', + actionType => { + const expectedResult = mockGetUserClosetsResponse; + const state = { + ...initialState, + isLoading: true, + }; + + expect( + reducer(state, { + payload: expectedResult, + type: actionType, + }).result, + ).toBe(expectedResult); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = mockState?.closets; + + expect(reducer(state, randomAction).result).toBe(state.result); + }); + }); + + describe('closet() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).closet; + + expect(state).toEqual(initialState.closet); + expect(state).toEqual({ error: null, isLoading: false, result: null }); + }); + + it('should handle FETCH_USER_CLOSET_REQUEST action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closet: { + error: toBlackoutError(new Error('dummy error')), + isLoading: false, + result: null, + }, + }; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_REQUEST, + }).closet, + ).toEqual({ + error: null, + isLoading: true, + }); + }); + + it('should handle FETCH_USER_CLOSET_FAILURE action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closet: { + error: null, + isLoading: true, + result: null, + }, + }; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_FAILURE, + payload: { error: toBlackoutError(new Error('dummy error')) }, + }).closet, + ).toEqual({ + error: toBlackoutError(new Error('dummy error')), + isLoading: false, + }); + }); + + it('should handle FETCH_USER_CLOSET_SUCCESS action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closet: { + error: null, + isLoading: true, + result: null, + }, + }; + + const expectedResult = mockGetUserClosetResponse; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_SUCCESS, + payload: expectedResult, + }).closet, + ).toEqual({ + error: null, + isLoading: false, + result: expectedResult, + }); + }); + + it('should handle other actions by returning the previous state', () => { + const state = { + ...initialState, + closet: { error: null, isLoading: false, result: null }, + }; + + expect(reducer(state, randomAction).closet).toEqual(state.closet); + }); + }); + + describe('getError() selector', () => { + it('should return the `error` property from a given state', () => { + expect(fromReducer.getError(initialState)).toBe(initialState.error); + }); + }); + + describe('getIsLoading() selector', () => { + it('should return the `isLoading` property from a given state', () => { + expect(fromReducer.getIsLoading(initialState)).toBe( + initialState.isLoading, + ); + }); + }); + + describe('getResult() selector', () => { + it('should return the `result` property from a given state', () => { + expect(fromReducer.getResult(initialState)).toBe(initialState.result); + }); + }); + + describe('getUserCloset() selector', () => { + it('should return the `closetSummaries` property from a given state', () => { + expect(fromReducer.getUserCloset(initialState)).toBe(initialState.closet); + }); + }); +}); diff --git a/packages/redux/src/users/closets/__tests__/selectors.test.ts b/packages/redux/src/users/closets/__tests__/selectors.test.ts new file mode 100644 index 000000000..90c5b72dc --- /dev/null +++ b/packages/redux/src/users/closets/__tests__/selectors.test.ts @@ -0,0 +1,131 @@ +import * as selectors from '../selectors.js'; +import { merge } from 'lodash-es'; +import { mockBaseState } from '../../__fixtures__/state.fixtures.js'; +import { + mockGetUserClosetResponse, + mockGetUserClosetsResponse, +} from 'tests/__fixtures__/users/index.mjs'; + +describe('redux selectors', () => { + describe('areUserClosetsLoading()', () => { + const mockLoadingState = merge({}, mockBaseState, { + users: { closets: { isLoading: true } }, + }); + + it('should return `isLoading` value from `closets` slice', () => { + expect(selectors.areUserClosetsLoading(mockBaseState)).toBe(false); + + expect(selectors.areUserClosetsLoading(mockLoadingState)).toBe(true); + }); + }); + + describe('getUserClosetsError()', () => { + const dummyError = new Error('dummy error'); + + const mockErrorState = merge({}, mockBaseState, { + users: { closets: { error: dummyError } }, + }); + + it('should return `error` value from `closets` slice', () => { + expect(selectors.getUserClosetsError(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetsError(mockErrorState)).toBe(dummyError); + }); + }); + + describe('getUserClosets()', () => { + it('should return `result` value from `closets` slice', () => { + const userClosets = mockGetUserClosetsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { result: userClosets }, + }, + }); + + expect(selectors.getUserClosets(mockStateWithResult)).toEqual( + userClosets, + ); + }); + }); + + describe('areUserClosetsFetched()', () => { + it('should return true if the user closets is fetched', () => { + const userClosets = mockGetUserClosetsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { result: userClosets }, + }, + }); + + expect(selectors.areUserClosetsFetched(mockStateWithResult)).toBeTruthy(); + }); + }); + + describe('isUserClosetLoading()', () => { + const mockLoadingState = merge({}, mockBaseState, { + users: { + closets: { + closet: { + error: null, + isLoading: true, + result: null, + }, + }, + }, + }); + + it('should return `isLoading` value from `closets.closet` slice', () => { + expect(selectors.isUserClosetLoading(mockBaseState)).toBe(false); + + expect(selectors.isUserClosetLoading(mockLoadingState)).toBe(true); + }); + }); + + describe('getUserClosetError()', () => { + const dummyError = new Error('dummy error'); + + const mockErrorState = merge({}, mockBaseState, { + users: { closets: { closet: { error: dummyError } } }, + }); + + it('should return `error` value from `closets.closet` slice', () => { + expect(selectors.getUserClosetError(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetError(mockErrorState)).toBe(dummyError); + }); + }); + + describe('getUserClosetResult()', () => { + const closetSummaries = mockGetUserClosetResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { closet: { result: closetSummaries } }, + }, + }); + + it('should return `result` value from `closets.closet` slice', () => { + expect(selectors.getUserClosetResult(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetResult(mockStateWithResult)).toEqual( + closetSummaries, + ); + }); + }); + + describe('isUserClosetFetched()', () => { + it('should return true if the user closet is fetched', () => { + const userCloset = mockGetUserClosetResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { closet: { result: userCloset } }, + }, + }); + + expect(selectors.isUserClosetFetched(mockStateWithResult)).toBeTruthy(); + }); + }); +}); diff --git a/packages/redux/src/users/closets/actionTypes.ts b/packages/redux/src/users/closets/actionTypes.ts new file mode 100644 index 000000000..af58d2caf --- /dev/null +++ b/packages/redux/src/users/closets/actionTypes.ts @@ -0,0 +1,54 @@ +/** + * Action type dispatched when the fetch user closet request fails. + */ +export const FETCH_USER_CLOSET_FAILURE = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_FAILURE'; +/** + * Action type dispatched when the fetch user closet request starts. + */ +export const FETCH_USER_CLOSET_REQUEST = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_REQUEST'; +/** + * Action type dispatched when the fetch user closet request succeeds. + */ +export const FETCH_USER_CLOSET_SUCCESS = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_SUCCESS'; + +/** + * Action type dispatched when the fetch user closets request fails. + */ +export const FETCH_USER_CLOSETS_FAILURE = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_FAILURE'; +/** + * Action type dispatched when the fetch user closets request starts. + */ +export const FETCH_USER_CLOSETS_REQUEST = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_REQUEST'; +/** + * Action type dispatched when the fetch user closets request succeeds. + */ +export const FETCH_USER_CLOSETS_SUCCESS = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS'; + +/** + * Action type dispatched when the remove user closet item request fails. + */ +export const REMOVE_USER_CLOSET_ITEM_FAILURE = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_FAILURE'; +/** + * Action type dispatched when the remove user closet item request starts. + */ +export const REMOVE_USER_CLOSET_ITEM_REQUEST = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_REQUEST'; +/** + * Action type dispatched when the remove user closet item request succeeds. + */ +export const REMOVE_USER_CLOSET_ITEM_SUCCESS = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS'; + +/** Action type dispatched when resetting the user closets state. */ +export const RESET_USER_CLOSETS = '@farfetch/blackout-redux/RESET_USER_CLOSETS'; + +/** Action type dispatched when resetting the user closet slice state. */ +export const RESET_USER_CLOSET_STATE = + '@farfetch/blackout-redux/RESET_USER_CLOSET_STATE'; diff --git a/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserCloset.test.ts.snap b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserCloset.test.ts.snap new file mode 100644 index 000000000..8be05f5ac --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserCloset.test.ts.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fetchUserCloset() action creator should create the correct actions for when the fetch user closet procedure is successful: fetch user closet success payload 1`] = ` +Object { + "payload": Array [ + Object { + "entries": Array [ + Object { + "attributes": Array [ + Object { + "description": "", + "type": 0, + "value": "10", + }, + ], + "brand": Object { + "description": "Yves Saint Laurent Beauty", + "id": 45511224, + "isActive": true, + "name": "Yves Saint Laurent Beauty", + "priceType": 0, + }, + "categories": Array [ + Object { + "gender": 0, + "id": 0, + "name": "string", + "parentId": 0, + }, + ], + "colors": Array [ + Object { + "color": Object { + "id": 112504, + "name": "Red", + }, + "tags": Array [ + "MainColor", + ], + }, + ], + "createdDate": "2023-08-17T11:52:35.941Z", + "customAttributes": "", + "id": "654321", + "images": Object { + "images": Array [], + "liveModel": null, + "liveModelId": null, + "productSize": null, + "tag": null, + }, + "isAvailable": true, + "merchantId": 123, + "orderId": "ABC123", + "price": Object { + "discountExclTaxes": 0, + "discountInclTaxes": 0, + "discountRate": 0, + "formattedPrice": "string", + "formattedPriceWithoutCurrency": "string", + "formattedPriceWithoutDiscount": "string", + "formattedPriceWithoutDiscountAndCurrency": "string", + "priceExclTaxes": 0, + "priceInclTaxes": 0, + "priceInclTaxesWithoutDiscount": 0, + "tags": Array [ + "string", + ], + "taxType": "string", + "taxesRate": 0, + "taxesValue": 0, + }, + "productDescription": "description1", + "productId": 123, + "productName": "product1", + "purchasePrice": Object { + "discountExclTaxes": 0, + "discountInclTaxes": 0, + "discountRate": 0, + "formattedPrice": "string", + "formattedPriceWithoutCurrency": "string", + "formattedPriceWithoutDiscount": "string", + "formattedPriceWithoutDiscountAndCurrency": "string", + "priceExclTaxes": 0, + "priceInclTaxes": 0, + "priceInclTaxesWithoutDiscount": 0, + "tags": Array [ + "string", + ], + "taxType": "string", + "taxesRate": 0, + "taxesValue": 0, + }, + }, + ], + "number": 1, + "totalItems": 1, + "totalPages": 1, + }, + ], + "type": "@farfetch/blackout-redux/FETCH_USER_CLOSET_SUCCESS", +} +`; diff --git a/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap new file mode 100644 index 000000000..19dde64d1 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fetchUserClosets() action creator should create the correct actions for when the fetch user closets procedure is successful: fetch user closets success payload 1`] = ` +Object { + "payload": Array [ + Object { + "createdDate": "2023-08-17T11:52:35.941Z", + "id": "3455f70e-f756-4ad5-b8e3-46d32ac74def", + "totalItems": 1, + }, + ], + "type": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS", +} +`; diff --git a/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap new file mode 100644 index 000000000..7bdefebab --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`removeUserClosetItem() action creator should create the correct actions for when the remove user closet item procedure is successful: remove user closet item success payload 1`] = ` +Object { + "type": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS", +} +`; diff --git a/packages/redux/src/users/closets/actions/__tests__/fetchUserCloset.test.ts b/packages/redux/src/users/closets/actions/__tests__/fetchUserCloset.test.ts new file mode 100644 index 000000000..fbba674b9 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/fetchUserCloset.test.ts @@ -0,0 +1,89 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + closetId, + config, + expectedConfig, + mockGetUserClosetResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { fetchUserCloset } from '../index.js'; +import { find } from 'lodash-es'; +import { getUserCloset } from '@farfetch/blackout-client'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + getUserCloset: jest.fn(), +})); + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state); +const query = {}; + +describe('fetchUserCloset() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the fetch user closet procedure fails', async () => { + const expectedError = new Error('fetch user closet error'); + + (getUserCloset as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => + await fetchUserCloset(userId, closetId, query, config)(store.dispatch), + ).rejects.toThrow(expectedError); + + expect(getUserCloset).toHaveBeenCalledTimes(1); + expect(getUserCloset).toHaveBeenCalledWith( + userId, + closetId, + query, + expectedConfig, + ); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.FETCH_USER_CLOSET_REQUEST }, + { + type: actionTypes.FETCH_USER_CLOSET_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the fetch user closet procedure is successful', async () => { + (getUserCloset as jest.Mock).mockResolvedValueOnce( + mockGetUserClosetResponse, + ); + + await fetchUserCloset(userId, closetId, query, config)(store.dispatch); + + const actionResults = store.getActions(); + + expect(getUserCloset).toHaveBeenCalledTimes(1); + expect(getUserCloset).toHaveBeenCalledWith( + userId, + closetId, + query, + expectedConfig, + ); + expect(actionResults).toMatchObject([ + { type: actionTypes.FETCH_USER_CLOSET_REQUEST }, + { + payload: mockGetUserClosetResponse, + type: actionTypes.FETCH_USER_CLOSET_SUCCESS, + }, + ]); + expect( + find(actionResults, { + type: actionTypes.FETCH_USER_CLOSET_SUCCESS, + }), + ).toMatchSnapshot('fetch user closet success payload'); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts new file mode 100644 index 000000000..c05423ce5 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts @@ -0,0 +1,76 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + config, + expectedConfig, + mockGetUserClosetsResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { fetchUserClosets } from '../index.js'; +import { find } from 'lodash-es'; +import { getUserClosets } from '@farfetch/blackout-client'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + getUserClosets: jest.fn(), +})); + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state); + +describe('fetchUserClosets() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the fetch user closets procedure fails', async () => { + const expectedError = new Error('fetch user closets error'); + + (getUserClosets as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => await fetchUserClosets(userId, config)(store.dispatch), + ).rejects.toThrow(expectedError); + + expect(getUserClosets).toHaveBeenCalledTimes(1); + expect(getUserClosets).toHaveBeenCalledWith(userId, expectedConfig); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.FETCH_USER_CLOSETS_REQUEST }, + { + type: actionTypes.FETCH_USER_CLOSETS_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the fetch user closets procedure is successful', async () => { + (getUserClosets as jest.Mock).mockResolvedValueOnce( + mockGetUserClosetsResponse, + ); + + await fetchUserClosets(userId, config)(store.dispatch); + + const actionResults = store.getActions(); + + expect(getUserClosets).toHaveBeenCalledTimes(1); + expect(getUserClosets).toHaveBeenCalledWith(userId, expectedConfig); + expect(actionResults).toMatchObject([ + { type: actionTypes.FETCH_USER_CLOSETS_REQUEST }, + { + payload: mockGetUserClosetsResponse, + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + }, + ]); + expect( + find(actionResults, { + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + }), + ).toMatchSnapshot('fetch user closets success payload'); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts b/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts new file mode 100644 index 000000000..d1a0c958c --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts @@ -0,0 +1,95 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + closetId, + closetItemId, + config, + expectedConfig, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { deleteUserClosetItem } from '@farfetch/blackout-client'; +import { find } from 'lodash-es'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { removeUserClosetItem } from '../index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + deleteUserClosetItem: jest.fn(), +})); + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state); + +describe('removeUserClosetItem() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the remove user closet item procedure fails', async () => { + const expectedError = new Error('remove user closet item error'); + + (deleteUserClosetItem as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => + await removeUserClosetItem( + userId, + closetId, + closetItemId, + config, + )(store.dispatch), + ).rejects.toThrow(expectedError); + + expect(deleteUserClosetItem).toHaveBeenCalledTimes(1); + expect(deleteUserClosetItem).toHaveBeenCalledWith( + userId, + closetId, + closetItemId, + expectedConfig, + ); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST }, + { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the remove user closet item procedure is successful', async () => { + (deleteUserClosetItem as jest.Mock).mockResolvedValueOnce(200); + + await removeUserClosetItem( + userId, + closetId, + closetItemId, + config, + )(store.dispatch); + + const actionResults = store.getActions(); + + expect(deleteUserClosetItem).toHaveBeenCalledTimes(1); + expect(deleteUserClosetItem).toHaveBeenCalledWith( + userId, + closetId, + closetItemId, + expectedConfig, + ); + expect(actionResults).toMatchObject([ + { type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST }, + { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }, + ]); + expect( + find(actionResults, { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }), + ).toMatchSnapshot('remove user closet item success payload'); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/resetUserClosetSummaries.test.ts b/packages/redux/src/users/closets/actions/__tests__/resetUserClosetSummaries.test.ts new file mode 100644 index 000000000..6587be6e6 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/resetUserClosetSummaries.test.ts @@ -0,0 +1,23 @@ +import * as actionTypes from '../../actionTypes.js'; +import { INITIAL_STATE } from '../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { resetUserCloset } from '../index.js'; + +const closetsMockStore = (state = {}) => + mockStore({ closets: INITIAL_STATE }, state); +let store: ReturnType; + +describe('resetUserCloset() action creator', () => { + beforeEach(() => { + jest.clearAllMocks(); + store = closetsMockStore(); + }); + + it('should dispatch the correct action for when the reset user closet is called', async () => { + await resetUserCloset()(store.dispatch); + + expect(store.getActions()).toMatchObject([ + { type: actionTypes.RESET_USER_CLOSET_STATE }, + ]); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts b/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts new file mode 100644 index 000000000..aa6a4f68b --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts @@ -0,0 +1,23 @@ +import * as actionTypes from '../../actionTypes.js'; +import { INITIAL_STATE } from '../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { resetUserClosets } from '../index.js'; + +const closetsMockStore = (state = {}) => + mockStore({ closets: INITIAL_STATE }, state); +let store: ReturnType; + +describe('resetUserClosets() action creator', () => { + beforeEach(() => { + jest.clearAllMocks(); + store = closetsMockStore(); + }); + + it('should dispatch the correct action for when the reset user closets is called', async () => { + await resetUserClosets()(store.dispatch); + + expect(store.getActions()).toMatchObject([ + { type: actionTypes.RESET_USER_CLOSETS }, + ]); + }); +}); diff --git a/packages/redux/src/users/closets/actions/factories/fetchUserClosetFactory.ts b/packages/redux/src/users/closets/actions/factories/fetchUserClosetFactory.ts new file mode 100644 index 000000000..e2f3451bc --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/fetchUserClosetFactory.ts @@ -0,0 +1,55 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type Closet, + type ClosetSummary, + type Config, + type GetUserCloset, + type GetUserClosetQuery, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; +import type { FetchUserClosetAction } from '../../types/actions.types.js'; + +/** + * Method responsible for fetching an user specific closet. + * + * @param getUserCloset - Get user closet client. + * + * @returns Thunk factory. + */ +const fetchUserClosetFactory = + (getUserCloset: GetUserCloset) => + ( + userId: User['id'], + closetId: ClosetSummary['id'], + query?: GetUserClosetQuery, + config?: Config, + ) => + async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: actionTypes.FETCH_USER_CLOSET_REQUEST, + }); + + const result = await getUserCloset(userId, closetId, query, config); + + dispatch({ + type: actionTypes.FETCH_USER_CLOSET_SUCCESS, + payload: result, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.FETCH_USER_CLOSET_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default fetchUserClosetFactory; diff --git a/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts b/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts new file mode 100644 index 000000000..6929eb2fd --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts @@ -0,0 +1,50 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type ClosetSummary, + type Config, + type GetUserClosets, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; +import type { FetchUserClosetsAction } from '../../types/actions.types.js'; + +/** + * Method responsible for fetching the user closets. + * + * @param getUserClosets - Get user closets client. + * + * @returns Thunk factory. + */ +const fetchUserClosetsFactory = + (getUserClosets: GetUserClosets) => + (userId: User['id'], config?: Config) => + async ( + dispatch: Dispatch, + ): Promise => { + try { + dispatch({ + type: actionTypes.FETCH_USER_CLOSETS_REQUEST, + }); + + const result = await getUserClosets(userId, config); + + dispatch({ + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + payload: result, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.FETCH_USER_CLOSETS_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default fetchUserClosetsFactory; diff --git a/packages/redux/src/users/closets/actions/factories/index.ts b/packages/redux/src/users/closets/actions/factories/index.ts new file mode 100644 index 000000000..fcbec58ad --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/index.ts @@ -0,0 +1,3 @@ +export { default as fetchUserClosetFactory } from './fetchUserClosetFactory.js'; +export { default as fetchUserClosetsFactory } from './fetchUserClosetsFactory.js'; +export { default as removeUserClosetItemFactory } from './removeUserClosetItemFactory.js'; diff --git a/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts b/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts new file mode 100644 index 000000000..ffc112f3c --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts @@ -0,0 +1,58 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type ClosetItem, + type ClosetSummary, + type Config, + type DeleteUserClosetItem, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; +import type { RemoveUserClosetItemAction } from '../../types/actions.types.js'; + +/** + * Method responsible for removing a specific item from user closet. + * + * @param deleteUserClosetItem - Delete user closet item client. + * + * @returns Thunk factory. + */ +const removeUserClosetItemFactory = + (deleteUserClosetItem: DeleteUserClosetItem) => + ( + userId: User['id'], + closetId: ClosetSummary['id'], + itemId: ClosetItem['id'], + config?: Config, + ) => + async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST, + }); + + const result = await deleteUserClosetItem( + userId, + closetId, + itemId, + config, + ); + + dispatch({ + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default removeUserClosetItemFactory; diff --git a/packages/redux/src/users/closets/actions/fetchUserCloset.ts b/packages/redux/src/users/closets/actions/fetchUserCloset.ts new file mode 100644 index 000000000..2ac6a7c3e --- /dev/null +++ b/packages/redux/src/users/closets/actions/fetchUserCloset.ts @@ -0,0 +1,7 @@ +import { fetchUserClosetFactory } from './factories/index.js'; +import { getUserCloset } from '@farfetch/blackout-client'; + +/** + * Fetch user closet. + */ +export default fetchUserClosetFactory(getUserCloset); diff --git a/packages/redux/src/users/closets/actions/fetchUserClosets.ts b/packages/redux/src/users/closets/actions/fetchUserClosets.ts new file mode 100644 index 000000000..86c275f73 --- /dev/null +++ b/packages/redux/src/users/closets/actions/fetchUserClosets.ts @@ -0,0 +1,7 @@ +import { fetchUserClosetsFactory } from './factories/index.js'; +import { getUserClosets } from '@farfetch/blackout-client'; + +/** + * Fetch user closets. + */ +export default fetchUserClosetsFactory(getUserClosets); diff --git a/packages/redux/src/users/closets/actions/index.ts b/packages/redux/src/users/closets/actions/index.ts new file mode 100644 index 000000000..e07d676e4 --- /dev/null +++ b/packages/redux/src/users/closets/actions/index.ts @@ -0,0 +1,7 @@ +export { default as fetchUserCloset } from './fetchUserCloset.js'; +export { default as fetchUserClosets } from './fetchUserClosets.js'; +export { default as removeUserClosetItem } from './removeUserClosetItem.js'; +export { default as resetUserCloset } from './resetUserCloset.js'; +export { default as resetUserClosets } from './resetUserClosets.js'; + +export * from './factories/index.js'; diff --git a/packages/redux/src/users/closets/actions/removeUserClosetItem.ts b/packages/redux/src/users/closets/actions/removeUserClosetItem.ts new file mode 100644 index 000000000..c3c61b989 --- /dev/null +++ b/packages/redux/src/users/closets/actions/removeUserClosetItem.ts @@ -0,0 +1,7 @@ +import { deleteUserClosetItem } from '@farfetch/blackout-client'; +import { removeUserClosetItemFactory } from './factories/index.js'; + +/** + * Remove user closet item. + */ +export default removeUserClosetItemFactory(deleteUserClosetItem); diff --git a/packages/redux/src/users/closets/actions/resetUserCloset.ts b/packages/redux/src/users/closets/actions/resetUserCloset.ts new file mode 100644 index 000000000..8bbde9a9e --- /dev/null +++ b/packages/redux/src/users/closets/actions/resetUserCloset.ts @@ -0,0 +1,18 @@ +import * as actionTypes from '../actionTypes.js'; +import type { Dispatch } from 'redux'; +import type { ResetClosetStateAction } from '../types/index.js'; + +/** + * Reset user closet slice state only + * to its initial value. + * + * @returns - Thunk. + */ +const resetUserClosetState = + () => (dispatch: Dispatch) => { + dispatch({ + type: actionTypes.RESET_USER_CLOSET_STATE, + }); + }; + +export default resetUserClosetState; diff --git a/packages/redux/src/users/closets/actions/resetUserClosets.ts b/packages/redux/src/users/closets/actions/resetUserClosets.ts new file mode 100644 index 000000000..ec1e92b1f --- /dev/null +++ b/packages/redux/src/users/closets/actions/resetUserClosets.ts @@ -0,0 +1,17 @@ +import * as actionTypes from '../actionTypes.js'; +import type { Dispatch } from 'redux'; +import type { ResetClosetsStateAction } from '../types/index.js'; + +/** + * Reset user closets state to its initial value. + * + * @returns - Thunk. + */ +const resetUserClosets = + () => (dispatch: Dispatch) => { + dispatch({ + type: actionTypes.RESET_USER_CLOSETS, + }); + }; + +export default resetUserClosets; diff --git a/packages/redux/src/users/closets/reducer.ts b/packages/redux/src/users/closets/reducer.ts new file mode 100644 index 000000000..26ab82f74 --- /dev/null +++ b/packages/redux/src/users/closets/reducer.ts @@ -0,0 +1,95 @@ +import * as actionTypes from './actionTypes.js'; +import { type AnyAction, combineReducers, type Reducer } from 'redux'; +import { createReducerWithResult } from '../../helpers/reducerFactory.js'; +import type * as T from './types/index.js'; + +export const INITIAL_STATE: T.UserClosetsState = { + result: null, + error: null, + isLoading: false, + closet: { + result: null, + error: null, + isLoading: false, + }, +}; + +const isLoading = ( + state = INITIAL_STATE.isLoading, + action: AnyAction, +): T.UserClosetsState['isLoading'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_REQUEST: + return true; + case actionTypes.FETCH_USER_CLOSETS_FAILURE: + case actionTypes.FETCH_USER_CLOSETS_SUCCESS: + return INITIAL_STATE.isLoading; + default: + return state; + } +}; + +const error = ( + state = INITIAL_STATE.error, + action: AnyAction, +): T.UserClosetsState['error'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_REQUEST: + return INITIAL_STATE.error; + case actionTypes.FETCH_USER_CLOSETS_FAILURE: + return (action as T.FetchUserClosetsFailureAction).payload.error; + default: + return state; + } +}; + +const result = ( + state = INITIAL_STATE.result, + action: AnyAction, +): T.UserClosetsState['result'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_SUCCESS: + return (action as T.FetchUserClosetsSuccessAction).payload; + default: + return state; + } +}; + +export const closet = createReducerWithResult( + ['FETCH_USER_CLOSET', 'REMOVE_USER_CLOSET_ITEM'], + INITIAL_STATE.closet, + actionTypes, + false, + false, + actionTypes.RESET_USER_CLOSET_STATE, +); + +export const getError = (state: T.UserClosetsState) => state.error; +export const getIsLoading = (state: T.UserClosetsState) => state.isLoading; +export const getResult = (state: T.UserClosetsState) => state.result; +export const getUserCloset = (state: T.UserClosetsState) => state.closet; + +const reducer = combineReducers({ + isLoading, + result, + error, + closet, +}); + +/** + * Reducer for user closets state. + * + * @param state - Current redux state. + * @param action - Action dispatched. + * + * @returns New state. + */ +const closetsReducer: Reducer = (state, action) => { + if (action.type === actionTypes.RESET_USER_CLOSETS) { + return INITIAL_STATE; + } + + return reducer(state, action); +}; + +export default closetsReducer; diff --git a/packages/redux/src/users/closets/selectors.ts b/packages/redux/src/users/closets/selectors.ts new file mode 100644 index 000000000..9e5a470c2 --- /dev/null +++ b/packages/redux/src/users/closets/selectors.ts @@ -0,0 +1,94 @@ +import { getError, getIsLoading, getResult, getUserCloset } from './reducer.js'; +import { getUserClosets as getUserClosetsFromReducer } from '../reducer.js'; +import type { StoreState } from '../../types/storeState.types.js'; +import type { UsersState } from '../types/state.types.js'; + +/** + * Returns the user closets result from the application state. + * + * @param state - Application state. + * + * @returns Returns user closet result. + */ +export const getUserClosets = (state: StoreState) => + getResult(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Returns the user closets error. + * + * @param state - Application state. + * + * @returns User closet error. + */ +export const getUserClosetsError = (state: StoreState) => + getError(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Returns the loading status of the user closets area. + * + * @param state - Application state. + * + * @returns Loader status. + */ +export const areUserClosetsLoading = (state: StoreState) => + getIsLoading(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Retrieves if the user closets has been fetched. + * + * Will return true if an user closets request + * has been made that returned either successfully or failed + * and false otherwise. + * + * @param state - Application state. + * + * @returns isFetched status of the user closets. + */ +export const areUserClosetsFetched = (state: StoreState) => + (!!getUserClosets(state) || !!getUserClosetsError(state)) && + !areUserClosetsLoading(state); + +/** + * Returns the user closet result from the application state. + * + * @param state - Application state. + * + * @returns User closet result. + */ +export const getUserClosetResult = (state: StoreState) => + getUserCloset(getUserClosetsFromReducer(state.users as UsersState)).result; + +/** + * Returns the user closet error. + * + * @param state - Application state. + * + * @returns Error details. + */ +export const getUserClosetError = (state: StoreState) => + getUserCloset(getUserClosetsFromReducer(state.users as UsersState)).error; + +/** + * Returns the loading status of the user closet area. + * + * @param state - Application state. + * + * @returns Loader status. + */ +export const isUserClosetLoading = (state: StoreState) => + getUserCloset(getUserClosetsFromReducer(state.users as UsersState)).isLoading; + +/** + * Retrieves if the user closet has been fetched. + * + * Will return true if an user closet request + * has been made that returned either successfully or failed + * and false otherwise. + * + * @param state - Application state. + * + * @returns isFetched status of the user closet. + */ +export const isUserClosetFetched = (state: StoreState) => + (!!getUserClosetResult(state) || !!getUserClosetError(state)) && + !isUserClosetLoading(state); diff --git a/packages/redux/src/users/closets/types/actions.types.ts b/packages/redux/src/users/closets/types/actions.types.ts new file mode 100644 index 000000000..9f35a97c5 --- /dev/null +++ b/packages/redux/src/users/closets/types/actions.types.ts @@ -0,0 +1,82 @@ +import type * as actionTypes from '../actionTypes.js'; +import type { Action } from 'redux'; +import type { + BlackoutError, + Closet, + ClosetSummary, +} from '@farfetch/blackout-client'; + +// +// Fetch user closet +// +export interface FetchUserClosetFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.FETCH_USER_CLOSET_FAILURE; +} + +export interface FetchUserClosetRequestAction extends Action { + type: typeof actionTypes.FETCH_USER_CLOSET_REQUEST; +} + +export interface FetchUserClosetSuccessAction extends Action { + payload: Closet[]; + type: typeof actionTypes.FETCH_USER_CLOSET_SUCCESS; +} + +export type FetchUserClosetAction = + | FetchUserClosetRequestAction + | FetchUserClosetFailureAction + | FetchUserClosetSuccessAction; + +// +// Fetch user closets +// +export interface FetchUserClosetsFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.FETCH_USER_CLOSETS_FAILURE; +} + +export interface FetchUserClosetsRequestAction extends Action { + type: typeof actionTypes.FETCH_USER_CLOSETS_REQUEST; +} + +export interface FetchUserClosetsSuccessAction extends Action { + payload: ClosetSummary[]; + type: typeof actionTypes.FETCH_USER_CLOSETS_SUCCESS; +} + +export type FetchUserClosetsAction = + | FetchUserClosetsRequestAction + | FetchUserClosetsFailureAction + | FetchUserClosetsSuccessAction; + +// +// Remove user closet item +// +export interface RemoveUserClosetItemFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE; +} + +export interface RemoveUserClosetItemRequestAction extends Action { + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST; +} + +export interface RemoveUserClosetItemSuccessAction extends Action { + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS; +} + +export type RemoveUserClosetItemAction = + | RemoveUserClosetItemRequestAction + | RemoveUserClosetItemFailureAction + | RemoveUserClosetItemSuccessAction; + +/** Actions dispatched when the reset user closets state request is made. */ +export interface ResetClosetsStateAction extends Action { + type: typeof actionTypes.RESET_USER_CLOSETS; +} + +/** Actions dispatched when the reset user closet state request is made. */ +export interface ResetClosetStateAction extends Action { + type: typeof actionTypes.RESET_USER_CLOSET_STATE; +} diff --git a/packages/redux/src/users/closets/types/index.ts b/packages/redux/src/users/closets/types/index.ts new file mode 100644 index 000000000..592cd7a1b --- /dev/null +++ b/packages/redux/src/users/closets/types/index.ts @@ -0,0 +1,2 @@ +export * from './actions.types.js'; +export * from './state.types.js'; diff --git a/packages/redux/src/users/closets/types/state.types.ts b/packages/redux/src/users/closets/types/state.types.ts new file mode 100644 index 000000000..b09f66462 --- /dev/null +++ b/packages/redux/src/users/closets/types/state.types.ts @@ -0,0 +1,6 @@ +import type { Closet, ClosetSummary } from '@farfetch/blackout-client'; +import type { StateWithResult } from '../../../types/subAreaState.types.js'; + +export type UserClosetsState = StateWithResult & { + closet: StateWithResult; +}; diff --git a/packages/redux/src/users/reducer.ts b/packages/redux/src/users/reducer.ts index 727c4b8d6..ad57abf3a 100644 --- a/packages/redux/src/users/reducer.ts +++ b/packages/redux/src/users/reducer.ts @@ -15,6 +15,9 @@ import benefitsReducer, { entitiesMapper as benefitsEntitiesMapper, INITIAL_STATE as INITIAL_BENEFITS_STATE, } from './benefits/reducer.js'; +import closetsReducer, { + INITIAL_STATE as INITIAL_USER_CLOSETS_STATE, +} from './closets/reducer.js'; import contactsReducer, { entitiesMapper as contactsEntitiesMapper, INITIAL_STATE as INITIAL_CONTACTS_STATE, @@ -47,6 +50,7 @@ export const INITIAL_STATE: UsersState = { attributes: INITIAL_ATTRIBUTES_STATE, authentication: INITIAL_AUTHENTICATION_STATE, benefits: INITIAL_BENEFITS_STATE, + closets: INITIAL_USER_CLOSETS_STATE, contacts: INITIAL_CONTACTS_STATE, creditMovements: INITIAL_CREDITS_STATE.creditMovements, credits: INITIAL_CREDITS_STATE.credits, @@ -215,6 +219,8 @@ export const getAuthentication = ( ): UsersState['authentication'] => state.authentication; export const getPersonalIds = (state: UsersState): UsersState['personalIds'] => state.personalIds; +export const getUserClosets = (state: UsersState): UsersState['closets'] => + state.closets; const reducer: Reducer = combineReducers({ error, @@ -224,6 +230,7 @@ const reducer: Reducer = combineReducers({ attributes: attributesReducer, authentication: authenticationReducer, benefits: benefitsReducer, + closets: closetsReducer, contacts: contactsReducer, creditMovements: creditsReducers.creditMovements, credits: creditsReducers.credits, diff --git a/packages/redux/src/users/selectors.ts b/packages/redux/src/users/selectors.ts index e8ef53269..fae40b547 100644 --- a/packages/redux/src/users/selectors.ts +++ b/packages/redux/src/users/selectors.ts @@ -146,6 +146,7 @@ export * from './addresses/selectors.js'; export * from './attributes/selectors.js'; export * from './authentication/selectors.js'; export * from './benefits/selectors.js'; +export * from './closets/selectors.js'; export * from './contacts/selectors.js'; export * from './credits/selectors.js'; export * from './personalIds/selectors.js'; diff --git a/packages/redux/src/users/types/state.types.ts b/packages/redux/src/users/types/state.types.ts index 4b23f7a03..fa863fb32 100644 --- a/packages/redux/src/users/types/state.types.ts +++ b/packages/redux/src/users/types/state.types.ts @@ -18,6 +18,7 @@ import type { } from '../preferences/types/index.js'; import type { TitlesState } from '../titles/types/index.js'; import type { UserAddressesState } from '../addresses/types/index.js'; +import type { UserClosetsState } from '../closets/types/index.js'; import type { UserPersonalIdsState } from '../personalIds/types/index.js'; import type { UserReturnsState } from '../returns/types/index.js'; @@ -37,4 +38,5 @@ export type UsersState = CombinedState<{ returns: UserReturnsState; titles: TitlesState; updatePreferences: UpdatePreferencesState; + closets: UserClosetsState; }>; diff --git a/tests/__fixtures__/users/index.mts b/tests/__fixtures__/users/index.mts index 157fce886..c4b1c03e8 100644 --- a/tests/__fixtures__/users/index.mts +++ b/tests/__fixtures__/users/index.mts @@ -10,5 +10,6 @@ export * from './preferences.fixtures.mjs'; export * from './titles.fixtures.mjs'; export * from './userAttribute.fixtures.mjs'; export * from './userAttributes.fixtures.mjs'; +export * from './userCloset.fixtures.mjs'; export * from './userReturns.fixtures.mjs'; export * from './users.fixtures.mjs'; diff --git a/tests/__fixtures__/users/userCloset.fixtures.mts b/tests/__fixtures__/users/userCloset.fixtures.mts new file mode 100644 index 000000000..5a4be8c05 --- /dev/null +++ b/tests/__fixtures__/users/userCloset.fixtures.mts @@ -0,0 +1,120 @@ +import { + GenderCode, + ProductVariantAttributeType, + toBlackoutError, +} from '@farfetch/blackout-client'; + +export const closetId = '3455f70e-f756-4ad5-b8e3-46d32ac74def'; +export const closetItemId = '654321'; + +export const mockGetUserClosetResponse = [ + { + number: 1, + totalPages: 1, + totalItems: 1, + entries: [ + { + id: closetItemId, + orderId: 'ABC123', + merchantId: 123, + productId: 123, + productName: 'product1', + productDescription: 'description1', + attributes: [ + { + type: ProductVariantAttributeType.Size, + value: '10', + description: '', + }, + ], + images: { + images: [], + liveModelId: null, + liveModel: null, + productSize: null, + tag: null, + }, + categories: [ + { + id: 0, + name: 'string', + parentId: 0, + gender: GenderCode.Woman, + }, + ], + colors: [ + { + color: { + id: 112504, + name: 'Red', + }, + tags: ['MainColor'], + }, + ], + price: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + purchasePrice: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + brand: { + description: 'Yves Saint Laurent Beauty', + id: 45511224, + name: 'Yves Saint Laurent Beauty', + priceType: 0, + isActive: true, + }, + customAttributes: '', + isAvailable: true, + createdDate: '2023-08-17T11:52:35.941Z', + }, + ], + }, +]; + +export const mockGetUserClosetsResponse = [ + { + id: closetId, + createdDate: '2023-08-17T11:52:35.941Z', + totalItems: 1, + }, +]; + +export const mockState = { + closets: { + error: toBlackoutError(new Error('error')), + isLoading: false, + result: mockGetUserClosetsResponse, + closet: { + error: toBlackoutError(new Error('error')), + isLoading: false, + result: mockGetUserClosetResponse, + }, + }, +};