diff --git a/.travis.yml b/.travis.yml index b65955792..ca5fb7f87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ cache: - packages/core/utils/lib - packages/core/factories/node_modules - packages/core/factories/lib + - packages/core/interfaces/node_modules + - packages/core/interfaces/lib - packages/prismic/node_modules - packages/prismic/lib @@ -34,7 +36,7 @@ jobs: - stage: Build name: Build script: - - yarn build:prismic && yarn build:ct + - yarn build:prismic && yarn build:ct - stage: test commercetools script: yarn test:ct:api-client --coverage diff --git a/packages/commercetools/api-client/src/api/getMyOrders/defaultQuery.ts b/packages/commercetools/api-client/src/api/getMyOrders/defaultQuery.ts new file mode 100644 index 000000000..78a570c47 --- /dev/null +++ b/packages/commercetools/api-client/src/api/getMyOrders/defaultQuery.ts @@ -0,0 +1,16 @@ +import gql from 'graphql-tag'; +import { OrderFragment } from './../../fragments'; + +export default gql` + ${OrderFragment} + + query getMe($where: String, $sort: [String!], $limit: Int, $offset: Int, $locale: Locale!) { + me { + orders(where: $where, sort: $sort, limit: $limit, offset: $offset) { + results { + ...DefaultOrder + } + } + } + } +`; diff --git a/packages/commercetools/api-client/src/api/getMyOrders/index.ts b/packages/commercetools/api-client/src/api/getMyOrders/index.ts new file mode 100644 index 000000000..962a56de4 --- /dev/null +++ b/packages/commercetools/api-client/src/api/getMyOrders/index.ts @@ -0,0 +1,18 @@ +import { apolloClient, locale } from './../../index'; +import defaultQuery from './defaultQuery'; +import { buildOrderWhere } from './../../helpers/search'; +import { OrderSearch, ProfileResponse } from './../../types/Api'; + +export default async (search: OrderSearch): Promise => { + return await apolloClient.query({ + query: defaultQuery, + variables: { + where: buildOrderWhere(search), + sort: search.sort, + limit: search.limit, + offset: search.offset, + locale + }, + fetchPolicy: 'no-cache' + }); +}; diff --git a/packages/commercetools/api-client/src/fragments/index.ts b/packages/commercetools/api-client/src/fragments/index.ts index d80d973fb..48e151c11 100644 --- a/packages/commercetools/api-client/src/fragments/index.ts +++ b/packages/commercetools/api-client/src/fragments/index.ts @@ -144,6 +144,7 @@ export const OrderFragment = ` orderState id version + createdAt } `; diff --git a/packages/commercetools/api-client/src/helpers/search/index.ts b/packages/commercetools/api-client/src/helpers/search/index.ts index 8c9358131..0bbf8404b 100644 --- a/packages/commercetools/api-client/src/helpers/search/index.ts +++ b/packages/commercetools/api-client/src/helpers/search/index.ts @@ -1,4 +1,8 @@ -import { CategorySearch, ProductSearch } from './../../types/Api'; +import { + CategorySearch, + ProductSearch, + OrderSearch +} from './../../types/Api'; import { locale } from './../../index'; const buildProductWhere = (search: ProductSearch) => { @@ -32,4 +36,16 @@ const buildCategoryWhere = (search: CategorySearch) => { return ''; }; -export { buildProductWhere, buildCategoryWhere }; +const buildOrderWhere = (search: OrderSearch): string => { + if (search?.id) { + return `id="${search.id}"`; + } + + return null; +}; + +export { + buildProductWhere, + buildCategoryWhere, + buildOrderWhere +}; diff --git a/packages/commercetools/api-client/src/index.ts b/packages/commercetools/api-client/src/index.ts index 8427988df..8b82f0fe5 100644 --- a/packages/commercetools/api-client/src/index.ts +++ b/packages/commercetools/api-client/src/index.ts @@ -19,6 +19,7 @@ import customerSignMeUp from './api/customerSignMeUp'; import customerSignMeIn from './api/customerSignMeIn'; import customerSignOut from './api/customerSignOut'; import getStorage from './helpers/createCommerceToolsLink/getStorage'; +import getMyOrders from './api/getMyOrders'; import customerChangeMyPassword from './api/customerChangeMyPassword'; let apolloClient: ApolloClient = null; @@ -81,5 +82,6 @@ export { customerSignMeUp, customerSignMeIn, customerSignOut, + getMyOrders, customerChangeMyPassword }; diff --git a/packages/commercetools/api-client/src/types/Api.ts b/packages/commercetools/api-client/src/types/Api.ts index a5dac1303..97e237026 100644 --- a/packages/commercetools/api-client/src/types/Api.ts +++ b/packages/commercetools/api-client/src/types/Api.ts @@ -25,6 +25,10 @@ export interface CategorySearch extends BaseSearch { slug?: string; } +export interface OrderSearch extends BaseSearch { + id?: string; +} + export type QueryResponse = ApolloQueryResult> export type MutationResponse = FetchResult> export type ProfileResponse = QueryResponse<'me', Me> diff --git a/packages/commercetools/api-client/tests/api/getMyOrders/getMyOrders.spec.ts b/packages/commercetools/api-client/tests/api/getMyOrders/getMyOrders.spec.ts new file mode 100644 index 000000000..816bec988 --- /dev/null +++ b/packages/commercetools/api-client/tests/api/getMyOrders/getMyOrders.spec.ts @@ -0,0 +1,32 @@ +import getMyOrders from '../../../src/api/getMyOrders'; +import { apolloClient } from '../../../src/index'; +import defaultQuery from '../../../src/api/getMyOrders/defaultQuery'; +import { OrderSearch } from '../../../src/types/Api'; + +describe('[commercetools-api-client] getMyOrders', () => { + it('fetches current user data', async () => { + const search: OrderSearch = { + id: 'fvdrt8gaw4r', + limit: 10, + offset: 0 + }; + const givenVariables = { + locale: 'en', + where: 'id="fvdrt8gaw4r"', + limit: 10, + offset: 0, + sort: undefined + }; + + (apolloClient.query as any).mockImplementation(({ variables, query }) => { + expect(variables).toEqual(givenVariables); + expect(query).toEqual(defaultQuery); + + return { data: 'me response' }; + }); + + const { data } = await getMyOrders(search); + + expect(data).toBe('me response'); + }); +}); diff --git a/packages/commercetools/api-client/tests/helpers/search.spec.ts b/packages/commercetools/api-client/tests/helpers/search.spec.ts index a66d3464b..2a20f4e4b 100644 --- a/packages/commercetools/api-client/tests/helpers/search.spec.ts +++ b/packages/commercetools/api-client/tests/helpers/search.spec.ts @@ -1,4 +1,8 @@ -import { buildProductWhere, buildCategoryWhere } from './../../src/helpers/search'; +import { + buildProductWhere, + buildCategoryWhere, + buildOrderWhere +} from './../../src/helpers/search'; describe('[commercetools-api-client] search', () => { it('returns undefined when parameters are not supported', () => { @@ -9,6 +13,10 @@ describe('[commercetools-api-client] search', () => { expect(buildCategoryWhere(null)).toBe(''); }); + it('returns undefined string when parameters are not supported', () => { + expect(buildOrderWhere(null)).toBe(null); + }); + it('returns product search query by cat id', () => { expect(buildProductWhere({ catId: ['cat id'] })).toBe('masterData(current(categories(id in ("cat id"))))'); }); @@ -24,4 +32,9 @@ describe('[commercetools-api-client] search', () => { it('returns product search query by slug', () => { expect(buildProductWhere({ slug: 'product-slug' })).toBe('masterData(current(slug(en="product-slug")))'); }); + + it('returns order search query by id', () => { + expect(buildOrderWhere({ id: 'orderid' })).toBe('id="orderid"'); + }); + }); diff --git a/packages/commercetools/composables/src/index.ts b/packages/commercetools/composables/src/index.ts index 89b65b420..661ab7ec4 100644 --- a/packages/commercetools/composables/src/index.ts +++ b/packages/commercetools/composables/src/index.ts @@ -5,6 +5,7 @@ import useCart from './useCart'; import useCheckout from './useCheckout'; import useUser from './useUser'; import useLocale from './useLocale'; +import useUserOrders from './useUserOrders'; export { useCategory, @@ -12,6 +13,7 @@ export { useCart, useCheckout, useUser, - useLocale + useLocale, + useUserOrders }; diff --git a/packages/commercetools/composables/src/types/index.ts b/packages/commercetools/composables/src/types/index.ts new file mode 100644 index 000000000..7db1f33fc --- /dev/null +++ b/packages/commercetools/composables/src/types/index.ts @@ -0,0 +1,5 @@ +export type OrderSearchParams = { + id?: string; + page?: number; + perPage?: number; +}; diff --git a/packages/commercetools/composables/src/useUser/index.ts b/packages/commercetools/composables/src/useUser/index.ts index e110fb693..695836d1a 100644 --- a/packages/commercetools/composables/src/useUser/index.ts +++ b/packages/commercetools/composables/src/useUser/index.ts @@ -44,7 +44,8 @@ export default function useUser(): UseUser { try { const profile = await getMe({ customer: true }); user.value = profile.data.me.customer; - } catch (err) {} // eslint-disable-line + // eslint-disable-next-line no-empty + } catch (err) {} loading.value = false; }); diff --git a/packages/commercetools/composables/src/useUserOrders/index.ts b/packages/commercetools/composables/src/useUserOrders/index.ts new file mode 100644 index 000000000..0010ee194 --- /dev/null +++ b/packages/commercetools/composables/src/useUserOrders/index.ts @@ -0,0 +1,14 @@ +import { useUserOrdersFactory, UseUserOrdersFactoryParams, OrdersSearchResult } from '@vue-storefront/factories'; +import { Order } from '../types/GraphQL'; +import { OrderSearchParams } from '../types'; +import { getMyOrders } from '@vue-storefront/commercetools-api'; + +const params: UseUserOrdersFactoryParams = { + searchOrders: async (params: OrderSearchParams = {}): Promise> => { + const result = await getMyOrders(params); + const { results: data, total } = result.data?.me.orders || { results: [], total: 0 }; + return { data, total }; + } +}; + +export default useUserOrdersFactory(params); diff --git a/packages/commercetools/composables/tests/useUser/useUser.ts b/packages/commercetools/composables/tests/useUser/useUser.ts index 3c9881e8a..76734814a 100644 --- a/packages/commercetools/composables/tests/useUser/useUser.ts +++ b/packages/commercetools/composables/tests/useUser/useUser.ts @@ -6,8 +6,17 @@ jest.mock('@vue-storefront/commercetools-api', () => ({ customerSignMeUp: jest.fn(), customerSignMeIn: jest.fn(), customerSignOut: jest.fn(), - getMe: () => ({ data: { me: { customer: { firstName: 'loaded customer', lastName: 'loaded customer' } } } }), - customerChangeMyPassword: jest.fn() + customerChangeMyPassword: jest.fn(), + getMe: () => ({ + data: { + me: { + customer: { + firstName: 'loaded customer', + lastName: 'loaded customer' + } + } + } + }) })); describe.skip('[commercetools-composables] useUser', () => { @@ -22,8 +31,12 @@ describe.skip('[commercetools-composables] useUser', () => { }); it('registers new customer', async () => { - const user = { customer: { firstName: 'john', - lastName: 'doe' } }; + const user = { + customer: { + firstName: 'john', + lastName: 'doe' + } + }; (customerSignMeUp as any).mockReturnValue(Promise.resolve({ data: { user } })); const wrapper = mountComposable(useUser); @@ -39,15 +52,22 @@ describe.skip('[commercetools-composables] useUser', () => { }); it('login customer and log out', async () => { - const user = { customer: { firstName: 'john', lastName: 'doe' } }; + const user = { + customer: { + firstName: 'john', + lastName: 'doe' + } + }; (customerSignMeIn as any).mockReturnValue(Promise.resolve({ data: { user } })); const wrapper = mountComposable(useUser); await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick(); - wrapper.vm.$data.login({ email: 'john@doe.com', - password: '123' }); + wrapper.vm.$data.login({ + email: 'john@doe.com', + password: '123' + }); expect(wrapper.vm.$data.loading).toBeTruthy(); await wrapper.vm.$nextTick(); diff --git a/packages/commercetools/composables/tests/useUserOrders/useUserOrders.spec.ts b/packages/commercetools/composables/tests/useUserOrders/useUserOrders.spec.ts new file mode 100644 index 000000000..a0806692c --- /dev/null +++ b/packages/commercetools/composables/tests/useUserOrders/useUserOrders.spec.ts @@ -0,0 +1,47 @@ +import { useUserOrders } from '../../src'; +import { getMyOrders } from '@vue-storefront/commercetools-api'; + +jest.mock('@vue-storefront/commercetools-api', () => ({ + getMyOrders: jest.fn() +})); + +describe('[commercetools-composables] useUserOrders', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has proper initial state', () => { + const { orders, loading } = useUserOrders(); + expect(loading.value).toEqual(false); + expect(orders.data.value).toEqual([]); + expect(orders.total.value).toEqual(0); + }); + + describe('search orders', () => { + it('gets no params and resolves to orders list', async () => { + (getMyOrders as jest.Mock).mockResolvedValueOnce({ data: { me: { orders: { results: [{ id: 'first' }, { id: 'second' }], total: 10 } } } }); + const { orders, searchOrders } = useUserOrders(); + await searchOrders(); + expect(orders.data.value).toEqual([{ id: 'first' }, { id: 'second' }]); + expect(orders.total.value).toEqual(10); + expect(getMyOrders).toBeCalledWith({}); + }); + + it('gets order id and passes it to api client', async () => { + (getMyOrders as jest.Mock).mockResolvedValueOnce({ data: { me: { orders: { results: [{ id: 'first' }], total: 1 } } } }); + const { orders, searchOrders } = useUserOrders(); + await searchOrders({ id: 'first' }); + expect(getMyOrders).toBeCalledWith({ id: 'first' }); + expect(orders.data.value).toEqual([{ id: 'first' }]); + expect(orders.total.value).toEqual(1); + }); + + it('gets empty result from api client', async () => { + (getMyOrders as jest.Mock).mockResolvedValueOnce({}); + const { orders, searchOrders } = useUserOrders(); + await searchOrders(); + expect(orders.data.value).toEqual([]); + expect(orders.total.value).toEqual(0); + }); + }); +}); diff --git a/packages/commercetools/helpers/package.json b/packages/commercetools/helpers/package.json index 29952805d..13f1c18ca 100644 --- a/packages/commercetools/helpers/package.json +++ b/packages/commercetools/helpers/package.json @@ -11,7 +11,8 @@ "test": "jest" }, "dependencies": { - "@vue-storefront/commercetools-api": "^0.0.3" + "@vue-storefront/commercetools-api": "^0.0.3", + "@vue-storefront/interfaces": "^0.0.3" }, "files": [ "lib/**/*" diff --git a/packages/commercetools/helpers/src/index.ts b/packages/commercetools/helpers/src/index.ts index b8b1bc278..7ae9db841 100644 --- a/packages/commercetools/helpers/src/index.ts +++ b/packages/commercetools/helpers/src/index.ts @@ -1,11 +1,25 @@ import { - UiMediaGalleryItem, - UiCategory, + AgnosticOrderStatus, AgnosticProductAttribute, - AgnosticTotals + AgnosticTotals, + UiCategory, + UiMediaGalleryItem } from '@vue-storefront/interfaces'; -import { ProductVariant, Image, Category, Cart, LineItem, ShippingMethod, Customer } from './types/GraphQL'; -import { formatAttributeList, getVariantByAttributes } from './_utils'; +import { + Cart, + Category, + Customer, + Image, + LineItem, + Order, + OrderState, + ProductVariant, + ShippingMethod +} from './types/GraphQL'; +import { + formatAttributeList, + getVariantByAttributes +} from './_utils'; interface ProductVariantFilters { master?: boolean; @@ -70,8 +84,10 @@ export const getProductAttributes = (products: ProductVariant[] | ProductVariant ...prev, [curr.name]: isSingleProduct ? curr.value : [ ...(prev[curr.name] || []), - { value: curr.value, - label: curr.label } + { + value: curr.value, + label: curr.label + } ] }); @@ -195,3 +211,20 @@ export const getUserFirstName = (user: Customer): string => user ? user.firstNam export const getUserLastName = (user: Customer): string => user ? user.lastName : ''; export const getUserFullName = (user: Customer): string => user ? `${user.firstName} ${user.lastName}` : ''; + +// Order + +export const getOrderDate = (order: Order): string => order?.createdAt || ''; + +export const getOrderNumber = (order: Order): string => order?.id || ''; + +const orderStatusMap = { + [OrderState.Open]: AgnosticOrderStatus.Open, + [OrderState.Confirmed]: AgnosticOrderStatus.Confirmed, + [OrderState.Complete]: AgnosticOrderStatus.Complete, + [OrderState.Cancelled]: AgnosticOrderStatus.Cancelled +}; + +export const getOrderStatus = (order: Order): AgnosticOrderStatus | '' => order?.orderState ? orderStatusMap[order.orderState] : ''; + +export const getOrderTotal = (order: Order): number | null => order ? order.totalPrice.centAmount / 100 : null; diff --git a/packages/commercetools/helpers/tests/orderHelpers.spec.ts b/packages/commercetools/helpers/tests/orderHelpers.spec.ts new file mode 100644 index 000000000..ad5d9a4e6 --- /dev/null +++ b/packages/commercetools/helpers/tests/orderHelpers.spec.ts @@ -0,0 +1,42 @@ +import { + getOrderDate, + getOrderNumber, + getOrderStatus, + getOrderTotal +} from '../src'; +import { OrderState, Order } from '../src/types/GraphQL'; + +const order: Order = { + createdAt: 123456789, + id: '645ygdf', + orderState: OrderState.Complete, + totalPrice: { + centAmount: 12345, + currencyCode: 'USD' + } +} as any; + +describe('[commercetools-helpers] order helpers', () => { + it('returns default values', () => { + expect(getOrderDate(null)).toBe(''); + expect(getOrderNumber(null)).toBe(''); + expect(getOrderStatus(null)).toBe(''); + expect(getOrderTotal(null)).toBe(null); + }); + + it('returns date', () => { + expect(getOrderDate(order)).toEqual(123456789); + }); + + it('returns order number', () => { + expect(getOrderNumber(order)).toEqual('645ygdf'); + }); + + it('returns status', () => { + expect(getOrderStatus(order)).toEqual(OrderState.Complete); + }); + + it('returns total gross', () => { + expect(getOrderTotal(order)).toEqual(123.45); + }); +}); diff --git a/packages/commercetools/theme/nuxt.config.js b/packages/commercetools/theme/nuxt.config.js index f62439a3f..dc0f1dda9 100644 --- a/packages/commercetools/theme/nuxt.config.js +++ b/packages/commercetools/theme/nuxt.config.js @@ -61,7 +61,6 @@ export default { '@vue-storefront/prismic', '@vue-storefront/utils', '@vue-storefront/factories' - ] } }], diff --git a/packages/core/factories/.gitignore b/packages/core/factories/.gitignore index 7951405f8..a65b41774 100644 --- a/packages/core/factories/.gitignore +++ b/packages/core/factories/.gitignore @@ -1 +1 @@ -lib \ No newline at end of file +lib diff --git a/packages/core/factories/src/index.ts b/packages/core/factories/src/index.ts index 73aac7bee..44dc6505f 100644 --- a/packages/core/factories/src/index.ts +++ b/packages/core/factories/src/index.ts @@ -1,4 +1,8 @@ +import Vue from 'vue'; +import CompositionApi from '@vue/composition-api'; +Vue.use(CompositionApi); export * from './useCartFactory'; export * from './useCategoryFactory'; export * from './useProductFactory'; +export * from './useUserOrdersFactory'; diff --git a/packages/core/factories/src/useUserOrdersFactory.ts b/packages/core/factories/src/useUserOrdersFactory.ts new file mode 100644 index 000000000..bf62e8a4f --- /dev/null +++ b/packages/core/factories/src/useUserOrdersFactory.ts @@ -0,0 +1,37 @@ +import { ref, Ref, computed } from '@vue/composition-api'; +import { UseUserOrders } from '@vue-storefront/interfaces'; + +export type OrdersSearchResult = { + data: ORDER[]; + total: number; +}; + +export type UseUserOrdersFactoryParams = { + searchOrders: (params: ORDER_SEARCH_PARAMS) => Promise>; +}; + +export function useUserOrdersFactory(factoryParams: UseUserOrdersFactoryParams) { + return function useUserOrders(): UseUserOrders { + const orders: Ref> = ref({ data: [], total: 0 }); + const loading: Ref = ref(false); + + const searchOrders = async (params?: ORDER_SEARCH_PARAMS): Promise => { + loading.value = true; + try { + orders.value = await factoryParams.searchOrders(params); + } catch (err) { + console.error(err.graphQLErrors ? err.graphQLErrors[0].message : err); + } + loading.value = false; + }; + + return { + orders: { + data: computed(() => orders.value.data), + total: computed(() => orders.value.total) + }, + searchOrders, + loading: computed(() => loading.value) + }; + }; +} diff --git a/packages/core/factories/tests/useUserOrdersFactory.spec.ts b/packages/core/factories/tests/useUserOrdersFactory.spec.ts new file mode 100644 index 000000000..8b25ec08c --- /dev/null +++ b/packages/core/factories/tests/useUserOrdersFactory.spec.ts @@ -0,0 +1,40 @@ +import { UseUserOrders } from '@vue-storefront/interfaces'; +import { UseUserOrdersFactoryParams, useUserOrdersFactory, OrdersSearchResult } from '../src'; +import { Ref } from '@vue/composition-api'; + +let useUserOrders: () => UseUserOrders>>>>; +let params: UseUserOrdersFactoryParams; + +function createComposable(): void { + params = { + searchOrders: jest.fn().mockResolvedValueOnce({ data: ['first', 'second'], total: 10 }) + }; + useUserOrders = useUserOrdersFactory(params); +} + +describe('[CORE - factories] useUserOrderFactory', () => { + beforeEach(() => { + jest.clearAllMocks(); + createComposable(); + }); + + describe('initial setup', () => { + it('should have proper initial props', () => { + const { loading, orders } = useUserOrders(); + expect(loading.value).toEqual(false); + expect(orders.data.value).toEqual([]); + expect(orders.total.value).toEqual(0); + }); + }); + + describe('methods', () => { + describe('search', () => { + it('should set search results', async () => { + const { searchOrders, orders } = useUserOrders(); + await searchOrders(); + expect(orders.data.value).toEqual(['first', 'second']); + expect(orders.total.value).toEqual(10); + }); + }); + }); +}); diff --git a/packages/core/interfaces/src/index.ts b/packages/core/interfaces/src/index.ts index 82de2a0d4..add896258 100644 --- a/packages/core/interfaces/src/index.ts +++ b/packages/core/interfaces/src/index.ts @@ -195,3 +195,13 @@ export interface AgnosticProductAttribute { value: string | Record; label: string; } + +export enum AgnosticOrderStatus { + Open = 'Open', + Pending = 'Pending', + Confirmed = 'Confirmed', + Shipped = 'Shipped', + Complete = 'Complete', + Cancelled = 'Cancelled', + Refunded = 'Refunded' +} diff --git a/packages/core/theme-module/theme/pages/Checkout.vue b/packages/core/theme-module/theme/pages/Checkout.vue index ad927a2e3..9ff4bcbb1 100644 --- a/packages/core/theme-module/theme/pages/Checkout.vue +++ b/packages/core/theme-module/theme/pages/Checkout.vue @@ -51,7 +51,7 @@ export default { CartPreview, OrderReview }, - setup(context) { + setup(props, context) { const showCartPreview = ref(true); const currentStep = ref(0); diff --git a/packages/core/theme-module/theme/pages/MyAccount.vue b/packages/core/theme-module/theme/pages/MyAccount.vue index f38027f09..dfa7d08d4 100644 --- a/packages/core/theme-module/theme/pages/MyAccount.vue +++ b/packages/core/theme-module/theme/pages/MyAccount.vue @@ -32,7 +32,7 @@ - + diff --git a/packages/core/theme-module/theme/pages/MyAccount/OrderHistory.vue b/packages/core/theme-module/theme/pages/MyAccount/OrderHistory.vue index 4506bc09f..6dc125701 100644 --- a/packages/core/theme-module/theme/pages/MyAccount/OrderHistory.vue +++ b/packages/core/theme-module/theme/pages/MyAccount/OrderHistory.vue @@ -1,5 +1,5 @@ + +