Browse Source

feat(core): Implement resolver for Order.fulfillments

Relates to #119
Michael Bromley 6 years ago
parent
commit
ff0bb0a90e

+ 114 - 34
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1493,18 +1493,18 @@ export type Mutation = {
     createChannel: Channel;
     /** Update an existing Channel */
     updateChannel: Channel;
-    /** Create a new Collection */
-    createCollection: Collection;
-    /** Update an existing Collection */
-    updateCollection: Collection;
-    /** Move a Collection to a different parent or index */
-    moveCollection: Collection;
     /** Create a new Country */
     createCountry: Country;
     /** Update an existing Country */
     updateCountry: Country;
     /** Delete a Country */
     deleteCountry: DeletionResponse;
+    /** Create a new Collection */
+    createCollection: Collection;
+    /** Update an existing Collection */
+    updateCollection: Collection;
+    /** Move a Collection to a different parent or index */
+    moveCollection: Collection;
     /** Create a new CustomerGroup */
     createCustomerGroup: CustomerGroup;
     /** Update an existing CustomerGroup */
@@ -1537,8 +1537,8 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
-    updateGlobalSettings: GlobalSettings;
     importProducts?: Maybe<ImportInfo>;
+    updateGlobalSettings: GlobalSettings;
     settlePayment?: Maybe<Payment>;
     createFulfillment?: Maybe<Fulfillment>;
     /** Update an existing PaymentMethod */
@@ -1624,18 +1624,6 @@ export type MutationUpdateChannelArgs = {
     input: UpdateChannelInput;
 };
 
-export type MutationCreateCollectionArgs = {
-    input: CreateCollectionInput;
-};
-
-export type MutationUpdateCollectionArgs = {
-    input: UpdateCollectionInput;
-};
-
-export type MutationMoveCollectionArgs = {
-    input: MoveCollectionInput;
-};
-
 export type MutationCreateCountryArgs = {
     input: CreateCountryInput;
 };
@@ -1648,6 +1636,18 @@ export type MutationDeleteCountryArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationCreateCollectionArgs = {
+    input: CreateCollectionInput;
+};
+
+export type MutationUpdateCollectionArgs = {
+    input: UpdateCollectionInput;
+};
+
+export type MutationMoveCollectionArgs = {
+    input: MoveCollectionInput;
+};
+
 export type MutationCreateCustomerGroupArgs = {
     input: CreateCustomerGroupInput;
 };
@@ -1718,14 +1718,14 @@ export type MutationDeleteFacetValuesArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationUpdateGlobalSettingsArgs = {
-    input: UpdateGlobalSettingsInput;
-};
-
 export type MutationImportProductsArgs = {
     csvFile: Scalars['Upload'];
 };
 
+export type MutationUpdateGlobalSettingsArgs = {
+    input: UpdateGlobalSettingsInput;
+};
+
 export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
 };
@@ -1878,6 +1878,7 @@ export type Order = Node & {
     lines: Array<OrderLine>;
     adjustments: Array<Adjustment>;
     payments?: Maybe<Array<Payment>>;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     subTotalBeforeTax: Scalars['Int'];
     subTotal: Scalars['Int'];
     currencyCode: CurrencyCode;
@@ -2332,20 +2333,20 @@ export type Query = {
     channels: Array<Channel>;
     channel?: Maybe<Channel>;
     activeChannel: Channel;
+    countries: CountryList;
+    country?: Maybe<Country>;
     collections: CollectionList;
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperation>;
-    countries: CountryList;
-    country?: Maybe<Country>;
     customerGroups: Array<CustomerGroup>;
     customerGroup?: Maybe<CustomerGroup>;
     customers: CustomerList;
     customer?: Maybe<Customer>;
     facets: FacetList;
     facet?: Maybe<Facet>;
-    globalSettings: GlobalSettings;
     job?: Maybe<JobInfo>;
     jobs: Array<JobInfo>;
+    globalSettings: GlobalSettings;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethods: PaymentMethodList;
@@ -2393,22 +2394,22 @@ export type QueryChannelArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryCollectionsArgs = {
-    languageCode?: Maybe<LanguageCode>;
-    options?: Maybe<CollectionListOptions>;
+export type QueryCountriesArgs = {
+    options?: Maybe<CountryListOptions>;
 };
 
-export type QueryCollectionArgs = {
+export type QueryCountryArgs = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
 };
 
-export type QueryCountriesArgs = {
-    options?: Maybe<CountryListOptions>;
+export type QueryCollectionsArgs = {
+    languageCode?: Maybe<LanguageCode>;
+    options?: Maybe<CollectionListOptions>;
 };
 
-export type QueryCountryArgs = {
+export type QueryCollectionArgs = {
     id: Scalars['ID'];
+    languageCode?: Maybe<LanguageCode>;
 };
 
 export type QueryCustomerGroupArgs = {
@@ -3936,6 +3937,52 @@ export type CreateFulfillmentMutation = { __typename?: 'Mutation' } & {
     >;
 };
 
+export type GetOrderFulfillmentsQueryVariables = {
+    id: Scalars['ID'];
+};
+
+export type GetOrderFulfillmentsQuery = { __typename?: 'Query' } & {
+    order: Maybe<
+        { __typename?: 'Order' } & Pick<Order, 'id'> & {
+                fulfillments: Maybe<
+                    Array<{ __typename?: 'Fulfillment' } & Pick<Fulfillment, 'id' | 'method'>>
+                >;
+            }
+    >;
+};
+
+export type GetOrderListFulfillmentsQueryVariables = {};
+
+export type GetOrderListFulfillmentsQuery = { __typename?: 'Query' } & {
+    orders: { __typename?: 'OrderList' } & {
+        items: Array<
+            { __typename?: 'Order' } & Pick<Order, 'id'> & {
+                    fulfillments: Maybe<
+                        Array<{ __typename?: 'Fulfillment' } & Pick<Fulfillment, 'id' | 'method'>>
+                    >;
+                }
+        >;
+    };
+};
+
+export type GetOrderFulfillmentItemsQueryVariables = {
+    id: Scalars['ID'];
+};
+
+export type GetOrderFulfillmentItemsQuery = { __typename?: 'Query' } & {
+    order: Maybe<
+        { __typename?: 'Order' } & Pick<Order, 'id'> & {
+                fulfillments: Maybe<
+                    Array<
+                        { __typename?: 'Fulfillment' } & Pick<Fulfillment, 'id'> & {
+                                orderItems: Array<{ __typename?: 'OrderItem' } & Pick<OrderItem, 'id'>>;
+                            }
+                    >
+                >;
+            }
+    >;
+};
+
 export type AddOptionGroupToProductMutationVariables = {
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -4881,6 +4928,39 @@ export namespace CreateFulfillment {
     >;
 }
 
+export namespace GetOrderFulfillments {
+    export type Variables = GetOrderFulfillmentsQueryVariables;
+    export type Query = GetOrderFulfillmentsQuery;
+    export type Order = NonNullable<GetOrderFulfillmentsQuery['order']>;
+    export type Fulfillments = NonNullable<
+        (NonNullable<(NonNullable<GetOrderFulfillmentsQuery['order']>)['fulfillments']>)[0]
+    >;
+}
+
+export namespace GetOrderListFulfillments {
+    export type Variables = GetOrderListFulfillmentsQueryVariables;
+    export type Query = GetOrderListFulfillmentsQuery;
+    export type Orders = GetOrderListFulfillmentsQuery['orders'];
+    export type Items = NonNullable<GetOrderListFulfillmentsQuery['orders']['items'][0]>;
+    export type Fulfillments = NonNullable<
+        (NonNullable<(NonNullable<GetOrderListFulfillmentsQuery['orders']['items'][0]>)['fulfillments']>)[0]
+    >;
+}
+
+export namespace GetOrderFulfillmentItems {
+    export type Variables = GetOrderFulfillmentItemsQueryVariables;
+    export type Query = GetOrderFulfillmentItemsQuery;
+    export type Order = NonNullable<GetOrderFulfillmentItemsQuery['order']>;
+    export type Fulfillments = NonNullable<
+        (NonNullable<(NonNullable<GetOrderFulfillmentItemsQuery['order']>)['fulfillments']>)[0]
+    >;
+    export type OrderItems = NonNullable<
+        (NonNullable<
+            (NonNullable<(NonNullable<GetOrderFulfillmentItemsQuery['order']>)['fulfillments']>)[0]
+        >)['orderItems'][0]
+    >;
+}
+
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;

+ 1 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1323,6 +1323,7 @@ export type Order = Node & {
     lines: Array<OrderLine>;
     adjustments: Array<Adjustment>;
     payments?: Maybe<Array<Payment>>;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     subTotalBeforeTax: Scalars['Int'];
     subTotal: Scalars['Int'];
     currencyCode: CurrencyCode;

+ 250 - 95
packages/core/e2e/order.e2e-spec.ts

@@ -1,7 +1,6 @@
 /* tslint:disable:no-non-null-assertion */
 import gql from 'graphql-tag';
 import path from 'path';
-import { expand } from 'rxjs/operators';
 
 import { ID } from '../../common/lib/shared-types';
 import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
@@ -12,8 +11,10 @@ import {
     CreateFulfillment,
     GetCustomerList,
     GetOrder,
+    GetOrderFulfillmentItems,
+    GetOrderFulfillments,
     GetOrderList,
-    OrderFragment,
+    GetOrderListFulfillments,
     OrderItemFragment,
     SettlePayment,
 } from './graphql/generated-e2e-admin-types';
@@ -53,10 +54,7 @@ describe('Orders resolver', () => {
             },
             {
                 paymentOptions: {
-                    paymentMethodHandlers: [
-                        twoStagePaymentMethod,
-                        failsToSettlePaymentMethod,
-                    ],
+                    paymentMethodHandlers: [twoStagePaymentMethod, failsToSettlePaymentMethod],
                 },
             },
         );
@@ -88,7 +86,7 @@ describe('Orders resolver', () => {
         });
         await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
             productVariantId: 'T_3',
-            quantity: 2,
+            quantity: 3,
         });
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -107,7 +105,6 @@ describe('Orders resolver', () => {
     });
 
     describe('payments', () => {
-
         it('settlePayment fails', async () => {
             await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
             await proceedToArrangingPayment(shopClient);
@@ -128,14 +125,19 @@ describe('Orders resolver', () => {
             expect(order.state).toBe('PaymentAuthorized');
 
             const payment = order.payments![0];
-            const { settlePayment } = await adminClient.query<SettlePayment.Mutation, SettlePayment.Variables>(SETTLE_PAYMENT, {
+            const { settlePayment } = await adminClient.query<
+                SettlePayment.Mutation,
+                SettlePayment.Variables
+                >(SETTLE_PAYMENT, {
                 id: payment.id,
             });
 
             expect(settlePayment!.id).toBe(payment.id);
             expect(settlePayment!.state).toBe('Authorized');
 
-            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: order.id });
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: order.id,
+            });
 
             expect(result.order!.state).toBe('PaymentAuthorized');
         });
@@ -160,7 +162,10 @@ describe('Orders resolver', () => {
             expect(order.state).toBe('PaymentAuthorized');
 
             const payment = order.payments![0];
-            const { settlePayment } = await adminClient.query<SettlePayment.Mutation, SettlePayment.Variables>(SETTLE_PAYMENT, {
+            const { settlePayment } = await adminClient.query<
+                SettlePayment.Mutation,
+                SettlePayment.Variables
+                >(SETTLE_PAYMENT, {
                 id: payment.id,
             });
 
@@ -172,7 +177,9 @@ describe('Orders resolver', () => {
                 moreData: 42,
             });
 
-            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: order.id });
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: order.id,
+            });
 
             expect(result.order!.state).toBe('PaymentSettled');
             expect(result.order!.payments![0].state).toBe('Settled');
@@ -180,119 +187,233 @@ describe('Orders resolver', () => {
     });
 
     describe('fulfillment', () => {
-
-        it('throws if Order is not in "PaymentSettled" state', assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_1' });
+        it(
+            'throws if Order is not in "PaymentSettled" state',
+            assertThrowsWithMessage(async () => {
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: 'T_1',
+                });
                 expect(order!.state).toBe('PaymentAuthorized');
 
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
-                    input: {
-                        lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
-                        method: 'Test',
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                    CREATE_FULFILLMENT,
+                    {
+                        input: {
+                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
+                            method: 'Test',
+                        },
                     },
-                });
-            },
-            'One or more OrderItems belong to an Order which is in an invalid state',
-            ),
+                );
+            }, 'One or more OrderItems belong to an Order which is in an invalid state'),
         );
 
-        it('throws if lines is empty', assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+        it(
+            'throws if lines is empty',
+            assertThrowsWithMessage(async () => {
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: 'T_2',
+                });
                 expect(order!.state).toBe('PaymentSettled');
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
-                    input: {
-                        lines: [],
-                        method: 'Test',
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                    CREATE_FULFILLMENT,
+                    {
+                        input: {
+                            lines: [],
+                            method: 'Test',
+                        },
                     },
-                });
-            },
-            'Nothing to fulfill',
-            ),
+                );
+            }, 'Nothing to fulfill'),
         );
 
-        it('throws if all quantities are zero', assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+        it(
+            'throws if all quantities are zero',
+            assertThrowsWithMessage(async () => {
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: 'T_2',
+                });
                 expect(order!.state).toBe('PaymentSettled');
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
-                    input: {
-                        lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
-                        method: 'Test',
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                    CREATE_FULFILLMENT,
+                    {
+                        input: {
+                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
+                            method: 'Test',
+                        },
                     },
-                });
-            },
-            'Nothing to fulfill',
-            ),
+                );
+            }, 'Nothing to fulfill'),
         );
 
         it('creates a partial fulfillment', async () => {
-            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
             expect(order!.state).toBe('PaymentSettled');
             const lines = order!.lines;
 
-            const { createFulfillment } = await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+            const { createFulfillment } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+                >(CREATE_FULFILLMENT, {
                 input: {
                     lines: lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
-                    method: 'Test',
-                    trackingCode: '123456',
+                    method: 'Test1',
+                    trackingCode: '111',
                 },
             });
 
-            expect(createFulfillment!.method).toBe('Test');
-            expect(createFulfillment!.trackingCode).toBe('123456');
+            expect(createFulfillment!.method).toBe('Test1');
+            expect(createFulfillment!.trackingCode).toBe('111');
             expect(createFulfillment!.orderItems).toEqual([
                 { id: lines[0].items[0].id },
                 { id: lines[1].items[0].id },
             ]);
 
-            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
-            // expect(result.order!.lines).toEqual({});
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+
             expect(result.order!.state).toBe('PartiallyFulfilled');
             expect(result.order!.lines[0].items[0].fulfillment!.id).toBe(createFulfillment!.id);
-            expect(result.order!.lines[1].items[1].fulfillment!.id).toBe(createFulfillment!.id);
-            expect(result.order!.lines[1].items[0].fulfillment).toBe(null);
+            expect(result.order!.lines[1].items[2].fulfillment!.id).toBe(createFulfillment!.id);
+            expect(result.order!.lines[1].items[1].fulfillment).toBeNull();
+            expect(result.order!.lines[1].items[0].fulfillment).toBeNull();
         });
 
-        it('throws if an OrderItem already part of a Fulfillment', assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+        it('creates a second partial fulfillment', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+            expect(order!.state).toBe('PartiallyFulfilled');
+            const lines = order!.lines;
+
+            const { createFulfillment } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+                >(CREATE_FULFILLMENT, {
+                input: {
+                    lines: [{ orderLineId: lines[1].id, quantity: 1 }],
+                    method: 'Test2',
+                    trackingCode: '222',
+                },
+            });
+
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+            // expect(result.order!.lines).toEqual({});
+            expect(result.order!.state).toBe('PartiallyFulfilled');
+            expect(result.order!.lines[1].items[2].fulfillment).not.toBeNull();
+            expect(result.order!.lines[1].items[1].fulfillment).not.toBeNull();
+            expect(result.order!.lines[1].items[0].fulfillment).toBeNull();
+        });
+
+        it(
+            'throws if an OrderItem already part of a Fulfillment',
+            assertThrowsWithMessage(async () => {
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: 'T_2',
+                });
                 expect(order!.state).toBe('PartiallyFulfilled');
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
-                    input: {
-                        method: 'Test',
-                        lines: [{
-                            orderLineId: order!.lines[0].id,
-                            quantity: 1,
-                        }],
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                    CREATE_FULFILLMENT,
+                    {
+                        input: {
+                            method: 'Test',
+                            lines: [
+                                {
+                                    orderLineId: order!.lines[0].id,
+                                    quantity: 1,
+                                },
+                            ],
+                        },
                     },
-                });
-            },
-            'One or more OrderItems have already been fulfilled',
-            ),
+                );
+            }, 'One or more OrderItems have already been fulfilled'),
         );
 
         it('completes fulfillment', async () => {
-            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
             expect(order!.state).toBe('PartiallyFulfilled');
 
-            const orderItems = order!.lines.reduce((items, line) => [...items, ...line.items], [] as OrderItemFragment[]);
+            const orderItems = order!.lines.reduce(
+                (items, line) => [...items, ...line.items],
+                [] as OrderItemFragment[],
+            );
 
-            const { createFulfillment } = await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+            const { createFulfillment } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+                >(CREATE_FULFILLMENT, {
                 input: {
-                    lines: [{
-                        orderLineId: order!.lines[1].id,
-                        quantity: 1,
-                    }],
-                    method: 'Test2',
-                    trackingCode: '56789',
+                    lines: [
+                        {
+                            orderLineId: order!.lines[1].id,
+                            quantity: 1,
+                        },
+                    ],
+                    method: 'Test3',
+                    trackingCode: '333',
                 },
             });
 
-            expect(createFulfillment!.method).toBe('Test2');
-            expect(createFulfillment!.trackingCode).toBe('56789');
+            expect(createFulfillment!.method).toBe('Test3');
+            expect(createFulfillment!.trackingCode).toBe('333');
             expect(createFulfillment!.orderItems).toEqual([{ id: orderItems[1].id }]);
 
-            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
             expect(result.order!.state).toBe('Fulfilled');
         });
+
+        it('order.fullfillments resolver for single order', async () => {
+            const { order } = await adminClient.query<
+                GetOrderFulfillments.Query,
+                GetOrderFulfillments.Variables
+                >(GET_ORDER_FULFILLMENTS, {
+                id: 'T_2',
+            });
+
+            expect(order!.fulfillments).toEqual([
+                { id: 'T_1', method: 'Test1' },
+                { id: 'T_2', method: 'Test2' },
+                { id: 'T_3', method: 'Test3' },
+            ]);
+        });
+
+        it('order.fullfillments resolver for order list', async () => {
+            const { orders } = await adminClient.query<GetOrderListFulfillments.Query>(
+                GET_ORDER_LIST_FULFILLMENTS,
+            );
+
+            expect(orders.items[0].fulfillments).toEqual([]);
+            expect(orders.items[1].fulfillments).toEqual([
+                { id: 'T_1', method: 'Test1' },
+                { id: 'T_2', method: 'Test2' },
+                { id: 'T_3', method: 'Test3' },
+            ]);
+        });
+
+        it('order.fullfillments.orderItems resolver', async () => {
+            const { order } = await adminClient.query<
+                GetOrderFulfillmentItems.Query,
+                GetOrderFulfillmentItems.Variables
+                >(GET_ORDER_FULFILLMENT_ITEMS, {
+                id: 'T_2',
+            });
+
+            expect(order!.fulfillments![0].orderItems).toEqual([
+                { id: 'T_3' },
+                { id: 'T_4' },
+            ]);
+            expect(order!.fulfillments![1].orderItems).toEqual([
+                { id: 'T_5' },
+            ]);
+        });
     });
 });
 
@@ -345,29 +466,23 @@ const failsToSettlePaymentMethod = new PaymentMethodHandler({
 });
 
 async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID> {
-    await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
-        SET_SHIPPING_ADDRESS,
-        {
-            input: {
-                fullName: 'name',
-                streetLine1: '12 the street',
-                city: 'foo',
-                postalCode: '123456',
-                countryCode: 'US',
-            },
+    await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
+        input: {
+            fullName: 'name',
+            streetLine1: '12 the street',
+            city: 'foo',
+            postalCode: '123456',
+            countryCode: 'US',
         },
-    );
+    });
 
     const { eligibleShippingMethods } = await shopClient.query<GetShippingMethods.Query>(
         GET_ELIGIBLE_SHIPPING_METHODS,
     );
 
-    await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
-        SET_SHIPPING_METHOD,
-        {
-            id: eligibleShippingMethods[1].id,
-        },
-    );
+    await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(SET_SHIPPING_METHOD, {
+        id: eligibleShippingMethods[1].id,
+    });
 
     const { transitionOrderToState } = await shopClient.query<
         TransitionToState.Mutation,
@@ -420,3 +535,43 @@ export const CREATE_FULFILLMENT = gql`
         }
     }
 `;
+
+export const GET_ORDER_FULFILLMENTS = gql`
+    query GetOrderFulfillments($id: ID!) {
+        order(id: $id) {
+            id
+            fulfillments {
+                id
+                method
+            }
+        }
+    }
+`;
+
+export const GET_ORDER_LIST_FULFILLMENTS = gql`
+    query GetOrderListFulfillments {
+        orders {
+            items {
+                id
+                fulfillments {
+                    id
+                    method
+                }
+            }
+        }
+    }
+`;
+
+export const GET_ORDER_FULFILLMENT_ITEMS = gql`
+    query GetOrderFulfillmentItems($id: ID!) {
+        order(id: $id) {
+            id
+            fulfillments {
+                id
+                orderItems {
+                    id
+                }
+            }
+        }
+    }
+`;

+ 2 - 0
packages/core/src/api/api-internal-modules.ts

@@ -31,6 +31,7 @@ import { TaxRateResolver } from './resolvers/admin/tax-rate.resolver';
 import { ZoneResolver } from './resolvers/admin/zone.resolver';
 import { CollectionEntityResolver } from './resolvers/entity/collection-entity.resolver';
 import { CustomerEntityResolver } from './resolvers/entity/customer-entity.resolver';
+import { FulfillmentEntityResolver } from './resolvers/entity/fulfillment-entity.resolver';
 import { OrderEntityResolver } from './resolvers/entity/order-entity.resolver';
 import { OrderLineEntityResolver } from './resolvers/entity/order-line-entity.resolver';
 import { ProductEntityResolver } from './resolvers/entity/product-entity.resolver';
@@ -79,6 +80,7 @@ const shopResolvers = [
 export const entityResolvers = [
     CollectionEntityResolver,
     CustomerEntityResolver,
+    FulfillmentEntityResolver,
     OrderEntityResolver,
     OrderLineEntityResolver,
     ProductEntityResolver,

+ 14 - 0
packages/core/src/api/resolvers/entity/fulfillment-entity.resolver.ts

@@ -0,0 +1,14 @@
+import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
+
+import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
+import { OrderService } from '../../../service/services/order.service';
+
+@Resolver('Fulfillment')
+export class FulfillmentEntityResolver {
+    constructor(private orderService: OrderService) {}
+
+    @ResolveProperty()
+    async orderItems(@Parent() fulfillment: Fulfillment) {
+        return this.orderService.getFulfillmentOrderItems(fulfillment.id);
+    }
+}

+ 5 - 0
packages/core/src/api/resolvers/entity/order-entity.resolver.ts

@@ -24,4 +24,9 @@ export class OrderEntityResolver {
             return null;
         }
     }
+
+    @ResolveProperty()
+    async fulfillments(@Parent() order: Order) {
+        return this.orderService.getOrderFulfillments(order);
+    }
 }

+ 1 - 0
packages/core/src/api/schema/type/order.type.graphql

@@ -11,6 +11,7 @@ type Order implements Node {
     lines: [OrderLine!]!
     adjustments: [Adjustment!]!
     payments: [Payment!]
+    fulfillments: [Fulfillment!]
     subTotalBeforeTax: Int!
     subTotal: Int!
     currencyCode: CurrencyCode!

+ 1 - 1
packages/core/src/service/helpers/order-state-machine/order-state.ts

@@ -32,7 +32,7 @@ export const orderStateTransitions: Transitions<OrderState> = {
         to: ['PartiallyFulfilled', 'Fulfilled', 'Cancelled'],
     },
     PartiallyFulfilled: {
-        to: ['Cancelled', 'Fulfilled'],
+        to: ['Cancelled', 'Fulfilled', 'PartiallyFulfilled'],
     },
     Fulfilled: {
         to: ['Cancelled'],

+ 25 - 0
packages/core/src/service/services/order.service.ts

@@ -2,6 +2,8 @@ import { InjectConnection } from '@nestjs/typeorm';
 import { PaymentInput } from '@vendure/common/lib/generated-shop-types';
 import { CreateAddressInput, CreateFulfillmentInput, ShippingMethodQuote } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { unique } from '@vendure/common/lib/unique';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -385,6 +387,29 @@ export class OrderService {
         return fulfillment;
     }
 
+    async getOrderFulfillments(order: Order): Promise<Fulfillment[]> {
+        let lines: OrderLine[];
+        if (order.lines && order.lines[0] && order.lines[0].items && order.lines[0].items[0].fulfillment !== undefined) {
+            lines = order.lines;
+        } else {
+            lines = await this.connection.getRepository(OrderLine).find({
+                where: {
+                    order: order.id,
+                },
+                relations: ['items', 'items.fulfillment'],
+            });
+        }
+        const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
+        return unique(items.map(i => i.fulfillment).filter(notNullOrUndefined), 'id');
+    }
+
+    async getFulfillmentOrderItems(id: ID): Promise<OrderItem[]> {
+        const fulfillment = await getEntityOrThrow(this.connection, Fulfillment, id,  {
+            relations: ['orderItems'],
+        });
+        return fulfillment.orderItems;
+    }
+
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff