Browse Source

feat(core): Implement createFulfillment mutation

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

+ 43 - 19
admin-ui/src/app/common/generated-types.ts

@@ -449,6 +449,13 @@ export type CreateFacetValueWithFacetInput = {
   translations: Array<FacetValueTranslationInput>,
   translations: Array<FacetValueTranslationInput>,
 };
 };
 
 
+export type CreateFulfillmentInput = {
+  orderItemIds?: Maybe<Array<Scalars['ID']>>,
+  orderId?: Maybe<Scalars['ID']>,
+  method: Scalars['String'],
+  trackingCode?: Maybe<Scalars['String']>,
+};
+
 export type CreateProductInput = {
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>,
   featuredAssetId?: Maybe<Scalars['ID']>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
@@ -1034,6 +1041,16 @@ export type FacetValueTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>,
   customFields?: Maybe<Scalars['JSON']>,
 };
 };
 
 
+export type Fulfillment = Node & {
+  __typename?: 'Fulfillment',
+  id: Scalars['ID'],
+  createdAt: Scalars['DateTime'],
+  updatedAt: Scalars['DateTime'],
+  orderItems: Array<OrderItem>,
+  method: Scalars['String'],
+  trackingCode?: Maybe<Scalars['String']>,
+};
+
 export type GlobalSettings = {
 export type GlobalSettings = {
   __typename?: 'GlobalSettings',
   __typename?: 'GlobalSettings',
   id: Scalars['ID'],
   id: Scalars['ID'],
@@ -1523,6 +1540,7 @@ export type Mutation = {
   updateGlobalSettings: GlobalSettings,
   updateGlobalSettings: GlobalSettings,
   importProducts?: Maybe<ImportInfo>,
   importProducts?: Maybe<ImportInfo>,
   settlePayment?: Maybe<Payment>,
   settlePayment?: Maybe<Payment>,
+  createFulfillment?: Maybe<Fulfillment>,
   /** Update an existing PaymentMethod */
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod,
   updatePaymentMethod: PaymentMethod,
   /** Create a new ProductOptionGroup */
   /** Create a new ProductOptionGroup */
@@ -1555,14 +1573,14 @@ export type Mutation = {
   createShippingMethod: ShippingMethod,
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
   updateShippingMethod: ShippingMethod,
-  /** Create a new TaxCategory */
-  createTaxCategory: TaxCategory,
-  /** Update an existing TaxCategory */
-  updateTaxCategory: TaxCategory,
   /** Create a new TaxRate */
   /** Create a new TaxRate */
   createTaxRate: TaxRate,
   createTaxRate: TaxRate,
   /** Update an existing TaxRate */
   /** Update an existing TaxRate */
   updateTaxRate: TaxRate,
   updateTaxRate: TaxRate,
+  /** Create a new TaxCategory */
+  createTaxCategory: TaxCategory,
+  /** Update an existing TaxCategory */
+  updateTaxCategory: TaxCategory,
   /** Create a new Zone */
   /** Create a new Zone */
   createZone: Zone,
   createZone: Zone,
   /** Update an existing Zone */
   /** Update an existing Zone */
@@ -1750,6 +1768,11 @@ export type MutationSettlePaymentArgs = {
 };
 };
 
 
 
 
+export type MutationCreateFulfillmentArgs = {
+  input: CreateFulfillmentInput
+};
+
+
 export type MutationUpdatePaymentMethodArgs = {
 export type MutationUpdatePaymentMethodArgs = {
   input: UpdatePaymentMethodInput
   input: UpdatePaymentMethodInput
 };
 };
@@ -1840,23 +1863,23 @@ export type MutationUpdateShippingMethodArgs = {
 };
 };
 
 
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
 };
 };
 
 
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
 };
 };
 
 
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
 };
 };
 
 
 
 
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
 };
 };
 
 
 
 
@@ -1984,6 +2007,7 @@ export type OrderItem = Node & {
   unitPriceIncludesTax: Scalars['Boolean'],
   unitPriceIncludesTax: Scalars['Boolean'],
   taxRate: Scalars['Float'],
   taxRate: Scalars['Float'],
   adjustments: Array<Adjustment>,
   adjustments: Array<Adjustment>,
+  fulfillment?: Maybe<Fulfillment>,
 };
 };
 
 
 export type OrderLine = Node & {
 export type OrderLine = Node & {
@@ -2422,10 +2446,10 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
-  taxCategories: Array<TaxCategory>,
-  taxCategory?: Maybe<TaxCategory>,
   taxRates: TaxRateList,
   taxRates: TaxRateList,
   taxRate?: Maybe<TaxRate>,
   taxRate?: Maybe<TaxRate>,
+  taxCategories: Array<TaxCategory>,
+  taxCategory?: Maybe<TaxCategory>,
   zones: Array<Zone>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
   zone?: Maybe<Zone>,
   networkStatus: NetworkStatus,
   networkStatus: NetworkStatus,
@@ -2598,11 +2622,6 @@ export type QueryShippingMethodArgs = {
 };
 };
 
 
 
 
-export type QueryTaxCategoryArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryTaxRatesArgs = {
 export type QueryTaxRatesArgs = {
   options?: Maybe<TaxRateListOptions>
   options?: Maybe<TaxRateListOptions>
 };
 };
@@ -2613,6 +2632,11 @@ export type QueryTaxRateArgs = {
 };
 };
 
 
 
 
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryZoneArgs = {
 export type QueryZoneArgs = {
   id: Scalars['ID']
   id: Scalars['ID']
 };
 };

+ 11 - 0
packages/common/src/generated-shop-types.ts

@@ -739,6 +739,16 @@ export type FacetValueTranslation = {
     name: Scalars['String'];
     name: Scalars['String'];
 };
 };
 
 
+export type Fulfillment = Node & {
+    __typename?: 'Fulfillment';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
 export type GlobalSettings = {
 export type GlobalSettings = {
     __typename?: 'GlobalSettings';
     __typename?: 'GlobalSettings';
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -1362,6 +1372,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
     adjustments: Array<Adjustment>;
+    fulfillment?: Maybe<Fulfillment>;
 };
 };
 
 
 export type OrderLine = Node & {
 export type OrderLine = Node & {

+ 43 - 19
packages/common/src/generated-types.ts

@@ -448,6 +448,13 @@ export type CreateFacetValueWithFacetInput = {
   translations: Array<FacetValueTranslationInput>,
   translations: Array<FacetValueTranslationInput>,
 };
 };
 
 
+export type CreateFulfillmentInput = {
+  orderItemIds?: Maybe<Array<Scalars['ID']>>,
+  orderId?: Maybe<Scalars['ID']>,
+  method: Scalars['String'],
+  trackingCode?: Maybe<Scalars['String']>,
+};
+
 export type CreateProductInput = {
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>,
   featuredAssetId?: Maybe<Scalars['ID']>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
@@ -1033,6 +1040,16 @@ export type FacetValueTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>,
   customFields?: Maybe<Scalars['JSON']>,
 };
 };
 
 
+export type Fulfillment = Node & {
+  __typename?: 'Fulfillment',
+  id: Scalars['ID'],
+  createdAt: Scalars['DateTime'],
+  updatedAt: Scalars['DateTime'],
+  orderItems: Array<OrderItem>,
+  method: Scalars['String'],
+  trackingCode?: Maybe<Scalars['String']>,
+};
+
 export type GlobalSettings = {
 export type GlobalSettings = {
   __typename?: 'GlobalSettings',
   __typename?: 'GlobalSettings',
   id: Scalars['ID'],
   id: Scalars['ID'],
@@ -1522,6 +1539,7 @@ export type Mutation = {
   updateGlobalSettings: GlobalSettings,
   updateGlobalSettings: GlobalSettings,
   importProducts?: Maybe<ImportInfo>,
   importProducts?: Maybe<ImportInfo>,
   settlePayment?: Maybe<Payment>,
   settlePayment?: Maybe<Payment>,
+  createFulfillment?: Maybe<Fulfillment>,
   /** Update an existing PaymentMethod */
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod,
   updatePaymentMethod: PaymentMethod,
   /** Create a new ProductOptionGroup */
   /** Create a new ProductOptionGroup */
@@ -1554,14 +1572,14 @@ export type Mutation = {
   createShippingMethod: ShippingMethod,
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
   updateShippingMethod: ShippingMethod,
-  /** Create a new TaxCategory */
-  createTaxCategory: TaxCategory,
-  /** Update an existing TaxCategory */
-  updateTaxCategory: TaxCategory,
   /** Create a new TaxRate */
   /** Create a new TaxRate */
   createTaxRate: TaxRate,
   createTaxRate: TaxRate,
   /** Update an existing TaxRate */
   /** Update an existing TaxRate */
   updateTaxRate: TaxRate,
   updateTaxRate: TaxRate,
+  /** Create a new TaxCategory */
+  createTaxCategory: TaxCategory,
+  /** Update an existing TaxCategory */
+  updateTaxCategory: TaxCategory,
   /** Create a new Zone */
   /** Create a new Zone */
   createZone: Zone,
   createZone: Zone,
   /** Update an existing Zone */
   /** Update an existing Zone */
@@ -1744,6 +1762,11 @@ export type MutationSettlePaymentArgs = {
 };
 };
 
 
 
 
+export type MutationCreateFulfillmentArgs = {
+  input: CreateFulfillmentInput
+};
+
+
 export type MutationUpdatePaymentMethodArgs = {
 export type MutationUpdatePaymentMethodArgs = {
   input: UpdatePaymentMethodInput
   input: UpdatePaymentMethodInput
 };
 };
@@ -1834,23 +1857,23 @@ export type MutationUpdateShippingMethodArgs = {
 };
 };
 
 
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
 };
 };
 
 
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
 };
 };
 
 
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
 };
 };
 
 
 
 
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
 };
 };
 
 
 
 
@@ -1962,6 +1985,7 @@ export type OrderItem = Node & {
   unitPriceIncludesTax: Scalars['Boolean'],
   unitPriceIncludesTax: Scalars['Boolean'],
   taxRate: Scalars['Float'],
   taxRate: Scalars['Float'],
   adjustments: Array<Adjustment>,
   adjustments: Array<Adjustment>,
+  fulfillment?: Maybe<Fulfillment>,
 };
 };
 
 
 export type OrderLine = Node & {
 export type OrderLine = Node & {
@@ -2400,10 +2424,10 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
-  taxCategories: Array<TaxCategory>,
-  taxCategory?: Maybe<TaxCategory>,
   taxRates: TaxRateList,
   taxRates: TaxRateList,
   taxRate?: Maybe<TaxRate>,
   taxRate?: Maybe<TaxRate>,
+  taxCategories: Array<TaxCategory>,
+  taxCategory?: Maybe<TaxCategory>,
   zones: Array<Zone>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
   zone?: Maybe<Zone>,
 };
 };
@@ -2573,11 +2597,6 @@ export type QueryShippingMethodArgs = {
 };
 };
 
 
 
 
-export type QueryTaxCategoryArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryTaxRatesArgs = {
 export type QueryTaxRatesArgs = {
   options?: Maybe<TaxRateListOptions>
   options?: Maybe<TaxRateListOptions>
 };
 };
@@ -2588,6 +2607,11 @@ export type QueryTaxRateArgs = {
 };
 };
 
 
 
 
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryZoneArgs = {
 export type QueryZoneArgs = {
   id: Scalars['ID']
   id: Scalars['ID']
 };
 };

+ 12 - 5
packages/core/e2e/graphql/fragments.ts

@@ -322,6 +322,16 @@ export const ORDER_FRAGMENT = gql`
     }
     }
 `;
 `;
 
 
+export const ORDER_ITEM_FRAGMENT = gql`
+    fragment OrderItem on OrderItem {
+        id
+        unitPrice
+        unitPriceIncludesTax
+        unitPriceWithTax
+        taxRate
+    }
+`;
+
 export const ORDER_WITH_LINES_FRAGMENT = gql`
 export const ORDER_WITH_LINES_FRAGMENT = gql`
     fragment OrderWithLines on Order {
     fragment OrderWithLines on Order {
         id
         id
@@ -349,11 +359,7 @@ export const ORDER_WITH_LINES_FRAGMENT = gql`
             unitPriceWithTax
             unitPriceWithTax
             quantity
             quantity
             items {
             items {
-                id
-                unitPrice
-                unitPriceIncludesTax
-                unitPriceWithTax
-                taxRate
+                ...OrderItem
             }
             }
             totalPrice
             totalPrice
         }
         }
@@ -385,6 +391,7 @@ export const ORDER_WITH_LINES_FRAGMENT = gql`
     }
     }
     ${ADJUSTMENT_FRAGMENT}
     ${ADJUSTMENT_FRAGMENT}
     ${SHIPPING_ADDRESS_FRAGMENT}
     ${SHIPPING_ADDRESS_FRAGMENT}
+    ${ORDER_ITEM_FRAGMENT}
 `;
 `;
 
 
 export const PROMOTION_FRAGMENT = gql`
 export const PROMOTION_FRAGMENT = gql`

+ 74 - 26
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -448,6 +448,13 @@ export type CreateFacetValueWithFacetInput = {
     translations: Array<FacetValueTranslationInput>;
     translations: Array<FacetValueTranslationInput>;
 };
 };
 
 
+export type CreateFulfillmentInput = {
+    orderItemIds?: Maybe<Array<Scalars['ID']>>;
+    orderId?: Maybe<Scalars['ID']>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
 export type CreateProductInput = {
 export type CreateProductInput = {
     featuredAssetId?: Maybe<Scalars['ID']>;
     featuredAssetId?: Maybe<Scalars['ID']>;
     assetIds?: Maybe<Array<Scalars['ID']>>;
     assetIds?: Maybe<Array<Scalars['ID']>>;
@@ -1031,6 +1038,16 @@ export type FacetValueTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type Fulfillment = Node & {
+    __typename?: 'Fulfillment';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
 export type GlobalSettings = {
 export type GlobalSettings = {
     __typename?: 'GlobalSettings';
     __typename?: 'GlobalSettings';
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -1519,6 +1536,7 @@ export type Mutation = {
     updateGlobalSettings: GlobalSettings;
     updateGlobalSettings: GlobalSettings;
     importProducts?: Maybe<ImportInfo>;
     importProducts?: Maybe<ImportInfo>;
     settlePayment?: Maybe<Payment>;
     settlePayment?: Maybe<Payment>;
+    createFulfillment?: Maybe<Fulfillment>;
     /** Update an existing PaymentMethod */
     /** Update an existing PaymentMethod */
     updatePaymentMethod: PaymentMethod;
     updatePaymentMethod: PaymentMethod;
     /** Create a new ProductOptionGroup */
     /** Create a new ProductOptionGroup */
@@ -1551,14 +1569,14 @@ export type Mutation = {
     createShippingMethod: ShippingMethod;
     createShippingMethod: ShippingMethod;
     /** Update an existing ShippingMethod */
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
     updateShippingMethod: ShippingMethod;
-    /** Create a new TaxCategory */
-    createTaxCategory: TaxCategory;
-    /** Update an existing TaxCategory */
-    updateTaxCategory: TaxCategory;
     /** Create a new TaxRate */
     /** Create a new TaxRate */
     createTaxRate: TaxRate;
     createTaxRate: TaxRate;
     /** Update an existing TaxRate */
     /** Update an existing TaxRate */
     updateTaxRate: TaxRate;
     updateTaxRate: TaxRate;
+    /** Create a new TaxCategory */
+    createTaxCategory: TaxCategory;
+    /** Update an existing TaxCategory */
+    updateTaxCategory: TaxCategory;
     /** Create a new Zone */
     /** Create a new Zone */
     createZone: Zone;
     createZone: Zone;
     /** Update an existing Zone */
     /** Update an existing Zone */
@@ -1708,6 +1726,10 @@ export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
+export type MutationCreateFulfillmentArgs = {
+    input: CreateFulfillmentInput;
+};
+
 export type MutationUpdatePaymentMethodArgs = {
 export type MutationUpdatePaymentMethodArgs = {
     input: UpdatePaymentMethodInput;
     input: UpdatePaymentMethodInput;
 };
 };
@@ -1781,14 +1803,6 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
     input: UpdateShippingMethodInput;
 };
 };
 
 
-export type MutationCreateTaxCategoryArgs = {
-    input: CreateTaxCategoryInput;
-};
-
-export type MutationUpdateTaxCategoryArgs = {
-    input: UpdateTaxCategoryInput;
-};
-
 export type MutationCreateTaxRateArgs = {
 export type MutationCreateTaxRateArgs = {
     input: CreateTaxRateInput;
     input: CreateTaxRateInput;
 };
 };
@@ -1797,6 +1811,14 @@ export type MutationUpdateTaxRateArgs = {
     input: UpdateTaxRateInput;
     input: UpdateTaxRateInput;
 };
 };
 
 
+export type MutationCreateTaxCategoryArgs = {
+    input: CreateTaxCategoryInput;
+};
+
+export type MutationUpdateTaxCategoryArgs = {
+    input: UpdateTaxCategoryInput;
+};
+
 export type MutationCreateZoneArgs = {
 export type MutationCreateZoneArgs = {
     input: CreateZoneInput;
     input: CreateZoneInput;
 };
 };
@@ -1901,6 +1923,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
     adjustments: Array<Adjustment>;
+    fulfillment?: Maybe<Fulfillment>;
 };
 };
 
 
 export type OrderLine = Node & {
 export type OrderLine = Node & {
@@ -2338,10 +2361,10 @@ export type Query = {
     shippingMethod?: Maybe<ShippingMethod>;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
-    taxCategories: Array<TaxCategory>;
-    taxCategory?: Maybe<TaxCategory>;
     taxRates: TaxRateList;
     taxRates: TaxRateList;
     taxRate?: Maybe<TaxRate>;
     taxRate?: Maybe<TaxRate>;
+    taxCategories: Array<TaxCategory>;
+    taxCategory?: Maybe<TaxCategory>;
     zones: Array<Zone>;
     zones: Array<Zone>;
     zone?: Maybe<Zone>;
     zone?: Maybe<Zone>;
 };
 };
@@ -2479,10 +2502,6 @@ export type QueryShippingMethodArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
-export type QueryTaxCategoryArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryTaxRatesArgs = {
 export type QueryTaxRatesArgs = {
     options?: Maybe<TaxRateListOptions>;
     options?: Maybe<TaxRateListOptions>;
 };
 };
@@ -2491,6 +2510,10 @@ export type QueryTaxRateArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
+export type QueryTaxCategoryArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryZoneArgs = {
 export type QueryZoneArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
@@ -3527,6 +3550,11 @@ export type OrderFragment = { __typename?: 'Order' } & Pick<
     'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'total' | 'currencyCode'
     'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'total' | 'currencyCode'
 > & { customer: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id' | 'firstName' | 'lastName'>> };
 > & { customer: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id' | 'firstName' | 'lastName'>> };
 
 
+export type OrderItemFragment = { __typename?: 'OrderItem' } & Pick<
+    OrderItem,
+    'id' | 'unitPrice' | 'unitPriceIncludesTax' | 'unitPriceWithTax' | 'taxRate'
+>;
+
 export type OrderWithLinesFragment = { __typename?: 'Order' } & Pick<
 export type OrderWithLinesFragment = { __typename?: 'Order' } & Pick<
     Order,
     Order,
     | 'id'
     | 'id'
@@ -3553,12 +3581,7 @@ export type OrderWithLinesFragment = { __typename?: 'Order' } & Pick<
                         ProductVariant,
                         ProductVariant,
                         'id' | 'name' | 'sku'
                         'id' | 'name' | 'sku'
                     >;
                     >;
-                    items: Array<
-                        { __typename?: 'OrderItem' } & Pick<
-                            OrderItem,
-                            'id' | 'unitPrice' | 'unitPriceIncludesTax' | 'unitPriceWithTax' | 'taxRate'
-                        >
-                    >;
+                    items: Array<{ __typename?: 'OrderItem' } & OrderItemFragment>;
                 }
                 }
         >;
         >;
         adjustments: Array<{ __typename?: 'Adjustment' } & AdjustmentFragment>;
         adjustments: Array<{ __typename?: 'Adjustment' } & AdjustmentFragment>;
@@ -3894,7 +3917,19 @@ export type SettlePaymentMutationVariables = {
 };
 };
 
 
 export type SettlePaymentMutation = { __typename?: 'Mutation' } & {
 export type SettlePaymentMutation = { __typename?: 'Mutation' } & {
-    settlePayment: Maybe<{ __typename?: 'Payment' } & Pick<Payment, 'id' | 'state'>>;
+    settlePayment: Maybe<{ __typename?: 'Payment' } & Pick<Payment, 'id' | 'state' | 'metadata'>>;
+};
+
+export type CreateFulfillmentMutationVariables = {
+    input: CreateFulfillmentInput;
+};
+
+export type CreateFulfillmentMutation = { __typename?: 'Mutation' } & {
+    createFulfillment: Maybe<
+        { __typename?: 'Fulfillment' } & Pick<Fulfillment, 'id' | 'method' | 'trackingCode'> & {
+                orderItems: Array<{ __typename?: 'OrderItem' } & Pick<OrderItem, 'id'>>;
+            }
+    >;
 };
 };
 
 
 export type AddOptionGroupToProductMutationVariables = {
 export type AddOptionGroupToProductMutationVariables = {
@@ -4570,6 +4605,10 @@ export namespace Order {
     export type Customer = NonNullable<OrderFragment['customer']>;
     export type Customer = NonNullable<OrderFragment['customer']>;
 }
 }
 
 
+export namespace OrderItem {
+    export type Fragment = OrderItemFragment;
+}
+
 export namespace OrderWithLines {
 export namespace OrderWithLines {
     export type Fragment = OrderWithLinesFragment;
     export type Fragment = OrderWithLinesFragment;
     export type Customer = NonNullable<OrderWithLinesFragment['customer']>;
     export type Customer = NonNullable<OrderWithLinesFragment['customer']>;
@@ -4578,7 +4617,7 @@ export namespace OrderWithLines {
         (NonNullable<OrderWithLinesFragment['lines'][0]>)['featuredAsset']
         (NonNullable<OrderWithLinesFragment['lines'][0]>)['featuredAsset']
     >;
     >;
     export type ProductVariant = (NonNullable<OrderWithLinesFragment['lines'][0]>)['productVariant'];
     export type ProductVariant = (NonNullable<OrderWithLinesFragment['lines'][0]>)['productVariant'];
-    export type Items = NonNullable<(NonNullable<OrderWithLinesFragment['lines'][0]>)['items'][0]>;
+    export type Items = OrderItemFragment;
     export type Adjustments = AdjustmentFragment;
     export type Adjustments = AdjustmentFragment;
     export type ShippingMethod = NonNullable<OrderWithLinesFragment['shippingMethod']>;
     export type ShippingMethod = NonNullable<OrderWithLinesFragment['shippingMethod']>;
     export type ShippingAddress = ShippingAddressFragment;
     export type ShippingAddress = ShippingAddressFragment;
@@ -4828,6 +4867,15 @@ export namespace SettlePayment {
     export type SettlePayment = NonNullable<SettlePaymentMutation['settlePayment']>;
     export type SettlePayment = NonNullable<SettlePaymentMutation['settlePayment']>;
 }
 }
 
 
+export namespace CreateFulfillment {
+    export type Variables = CreateFulfillmentMutationVariables;
+    export type Mutation = CreateFulfillmentMutation;
+    export type CreateFulfillment = NonNullable<CreateFulfillmentMutation['createFulfillment']>;
+    export type OrderItems = NonNullable<
+        (NonNullable<CreateFulfillmentMutation['createFulfillment']>)['orderItems'][0]
+    >;
+}
+
 export namespace AddOptionGroupToProduct {
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;
     export type Mutation = AddOptionGroupToProductMutation;

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

@@ -739,6 +739,16 @@ export type FacetValueTranslation = {
     name: Scalars['String'];
     name: Scalars['String'];
 };
 };
 
 
+export type Fulfillment = Node & {
+    __typename?: 'Fulfillment';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
 export type GlobalSettings = {
 export type GlobalSettings = {
     __typename?: 'GlobalSettings';
     __typename?: 'GlobalSettings';
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -1362,6 +1372,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
     adjustments: Array<Adjustment>;
+    fulfillment?: Maybe<Fulfillment>;
 };
 };
 
 
 export type OrderLine = Node & {
 export type OrderLine = Node & {

+ 126 - 3
packages/core/e2e/order.e2e-spec.ts

@@ -1,13 +1,22 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
+import { expand } from 'rxjs/operators';
 
 
 import { ID } from '../../common/lib/shared-types';
 import { ID } from '../../common/lib/shared-types';
 import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
 import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
 
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
-import { GetCustomerList, GetOrder, GetOrderList, OrderFragment, SettlePayment } from './graphql/generated-e2e-admin-types';
+import {
+    CreateFulfillment,
+    GetCustomerList,
+    GetOrder,
+    GetOrderList,
+    OrderFragment,
+    OrderItemFragment,
+    SettlePayment,
+} from './graphql/generated-e2e-admin-types';
 import {
 import {
     AddItemToOrder,
     AddItemToOrder,
     AddPaymentToOrder,
     AddPaymentToOrder,
@@ -27,13 +36,13 @@ import {
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
 import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Orders resolver', () => {
 describe('Orders resolver', () => {
     const adminClient = new TestAdminClient();
     const adminClient = new TestAdminClient();
     const shopClient = new TestShopClient();
     const shopClient = new TestShopClient();
     const server = new TestServer();
     const server = new TestServer();
     let customers: GetCustomerList.Items[];
     let customers: GetCustomerList.Items[];
-    let orders: OrderFragment[];
     const password = 'test';
     const password = 'test';
 
 
     beforeAll(async () => {
     beforeAll(async () => {
@@ -68,11 +77,19 @@ describe('Orders resolver', () => {
             productVariantId: 'T_1',
             productVariantId: 'T_1',
             quantity: 1,
             quantity: 1,
         });
         });
+        await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_2',
+            quantity: 1,
+        });
         await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
         await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
         await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
         await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
             productVariantId: 'T_2',
             productVariantId: 'T_2',
             quantity: 1,
             quantity: 1,
         });
         });
+        await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_3',
+            quantity: 1,
+        });
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -82,7 +99,6 @@ describe('Orders resolver', () => {
     it('orders', async () => {
     it('orders', async () => {
         const result = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
         const result = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
         expect(result.orders.items.map(o => o.id)).toEqual(['T_1', 'T_2']);
         expect(result.orders.items.map(o => o.id)).toEqual(['T_1', 'T_2']);
-        orders = result.orders.items;
     });
     });
 
 
     it('order', async () => {
     it('order', async () => {
@@ -162,6 +178,100 @@ describe('Orders resolver', () => {
             expect(result.order!.payments![0].state).toBe('Settled');
             expect(result.order!.payments![0].state).toBe('Settled');
         });
         });
     });
     });
+
+    describe('fulfillment', () => {
+
+        it('throws if Order is not in "PaymentSettled" state', assertThrowsWithMessage(async () => {
+                const { orders } = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
+                const nonSettledOrder = orders.items.find(o => o.state !== 'PaymentSettled');
+                if (!nonSettledOrder) {
+                    fail('Could not find an Order not in the PaymentSettled state');
+                    return;
+                }
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+                    input: {
+                        orderId: nonSettledOrder.id,
+                        method: 'Test',
+                    },
+                });
+            },
+            'One or more OrderItems belong to an Order which is in an invalid state',
+            ),
+        );
+
+        it('throws if neither orderId not orderItemIds are specified', assertThrowsWithMessage(async () => {
+                const { orders } = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
+                const nonSettledOrder = orders.items.find(o => o.state !== 'PaymentSettled');
+                if (!nonSettledOrder) {
+                    fail('Could not find an Order not in the PaymentSettled state');
+                    return;
+                }
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+                    input: {
+                        method: 'Test',
+                    },
+                });
+            },
+            'Please specify either orderId or orderItemIds',
+            ),
+        );
+
+        it('creates a partial fulfillment', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            expect(order!.state).toBe('PaymentSettled');
+
+            const orderItems = order!.lines.reduce((items, line) => [...items, ...line.items], [] as OrderItemFragment[]);
+
+            const { createFulfillment } = await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+                input: {
+                    orderItemIds: [orderItems[0].id],
+                    method: 'Test',
+                    trackingCode: '123456',
+                },
+            });
+
+            expect(createFulfillment!.method).toBe('Test');
+            expect(createFulfillment!.trackingCode).toBe('123456');
+            expect(createFulfillment!.orderItems).toEqual([{ id: orderItems[0].id }]);
+
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            expect(result.order!.state).toBe('PartiallyFulfilled');
+        });
+
+        it('throws if an OrderItem already part of a Fulfillment', assertThrowsWithMessage(async () => {
+                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+                    input: {
+                        method: 'Test',
+                        orderId: 'T_2',
+                    },
+                });
+            },
+            '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' });
+            expect(order!.state).toBe('PartiallyFulfilled');
+
+            const orderItems = order!.lines.reduce((items, line) => [...items, ...line.items], [] as OrderItemFragment[]);
+
+            const { createFulfillment } = await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
+                input: {
+                    orderItemIds: [orderItems[1].id],
+                    method: 'Test2',
+                    trackingCode: '56789',
+                },
+            });
+
+            expect(createFulfillment!.method).toBe('Test2');
+            expect(createFulfillment!.trackingCode).toBe('56789');
+            expect(createFulfillment!.orderItems).toEqual([{ id: orderItems[1].id }]);
+
+            const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
+            expect(result.order!.state).toBe('Fulfilled');
+        });
+    });
 });
 });
 
 
 /**
 /**
@@ -275,3 +385,16 @@ export const SETTLE_PAYMENT = gql`
         }
         }
     }
     }
 `;
 `;
+
+export const CREATE_FULFILLMENT = gql`
+    mutation CreateFulfillment($input: CreateFulfillmentInput!) {
+        createFulfillment(input: $input) {
+            id
+            method
+            trackingCode
+            orderItems {
+                id
+            }
+        }
+    }
+`;

+ 15 - 1
packages/core/src/api/resolvers/admin/order.resolver.ts

@@ -1,5 +1,11 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { MutationSettlePaymentArgs, Permission, QueryOrderArgs, QueryOrdersArgs } from '@vendure/common/lib/generated-types';
+import {
+    MutationCreateFulfillmentArgs,
+    MutationSettlePaymentArgs,
+    Permission,
+    QueryOrderArgs,
+    QueryOrdersArgs
+} from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
@@ -7,6 +13,7 @@ import { OrderService } from '../../../service/services/order.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
 import { RequestContext } from '../../common/request-context';
 import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
 import { Allow } from '../../decorators/allow.decorator';
+import { Decode } from '../../decorators/decode.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 
 @Resolver()
 @Resolver()
@@ -30,4 +37,11 @@ export class OrderResolver {
     async settlePayment(@Ctx() ctx: RequestContext, @Args() args: MutationSettlePaymentArgs) {
     async settlePayment(@Ctx() ctx: RequestContext, @Args() args: MutationSettlePaymentArgs) {
         return this.orderService.settlePayment(ctx, args.id);
         return this.orderService.settlePayment(ctx, args.id);
     }
     }
+
+    @Mutation()
+    @Decode('orderId', 'orderItemIds')
+    @Allow(Permission.UpdateOrder)
+    async createFulfillment(@Ctx() ctx: RequestContext, @Args() args: MutationCreateFulfillmentArgs) {
+        return this.orderService.createFulfillment(ctx, args.input);
+    }
 }
 }

+ 8 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -5,7 +5,15 @@ type Query {
 
 
 type Mutation {
 type Mutation {
     settlePayment(id: ID!): Payment
     settlePayment(id: ID!): Payment
+    createFulfillment(input: CreateFulfillmentInput!): Fulfillment
 }
 }
 
 
 # generated by generateListOptions function
 # generated by generateListOptions function
 input OrderListOptions
 input OrderListOptions
+
+input CreateFulfillmentInput {
+    orderItemIds: [ID!]
+    orderId: ID
+    method: String!
+    trackingCode: String
+}

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

@@ -55,6 +55,7 @@ type OrderItem implements Node {
     unitPriceIncludesTax: Boolean!
     unitPriceIncludesTax: Boolean!
     taxRate: Float!
     taxRate: Float!
     adjustments: [Adjustment!]!
     adjustments: [Adjustment!]!
+    fulfillment: Fulfillment
 }
 }
 
 
 type OrderLine implements Node {
 type OrderLine implements Node {
@@ -82,3 +83,12 @@ type Payment implements Node {
     transactionId: String
     transactionId: String
     metadata: JSON
     metadata: JSON
 }
 }
+
+type Fulfillment implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    orderItems: [OrderItem!]!
+    method: String!
+    trackingCode: String
+}

+ 2 - 0
packages/core/src/entity/entities.ts

@@ -12,6 +12,7 @@ import { FacetValueTranslation } from './facet-value/facet-value-translation.ent
 import { FacetValue } from './facet-value/facet-value.entity';
 import { FacetValue } from './facet-value/facet-value.entity';
 import { FacetTranslation } from './facet/facet-translation.entity';
 import { FacetTranslation } from './facet/facet-translation.entity';
 import { Facet } from './facet/facet.entity';
 import { Facet } from './facet/facet.entity';
+import { Fulfillment } from './fulfillment/fulfillment.entity';
 import { GlobalSettings } from './global-settings/global-settings.entity';
 import { GlobalSettings } from './global-settings/global-settings.entity';
 import { OrderItem } from './order-item/order-item.entity';
 import { OrderItem } from './order-item/order-item.entity';
 import { OrderLine } from './order-line/order-line.entity';
 import { OrderLine } from './order-line/order-line.entity';
@@ -64,6 +65,7 @@ export const coreEntitiesMap = {
     FacetTranslation,
     FacetTranslation,
     FacetValue,
     FacetValue,
     FacetValueTranslation,
     FacetValueTranslation,
+    Fulfillment,
     GlobalSettings,
     GlobalSettings,
     Order,
     Order,
     OrderItem,
     OrderItem,

+ 28 - 0
packages/core/src/entity/fulfillment/fulfillment.entity.ts

@@ -0,0 +1,28 @@
+import { Column, Entity, OneToMany } from 'typeorm';
+
+import { DeepPartial } from '../../../../common/lib/shared-types';
+import { VendureEntity } from '../base/base.entity';
+import { OrderItem } from '../order-item/order-item.entity';
+
+/**
+ * @description
+ * This entity represents a fulfillment of an Order or part of it, i.e. the {@link OrderItem}s have been
+ * delivered to the Customer after successful payment.
+ *
+ * @docsCategory entities
+ */
+@Entity()
+export class Fulfillment extends VendureEntity {
+    constructor(input?: DeepPartial<Fulfillment>) {
+        super(input);
+    }
+
+    @Column({ default: '' })
+    trackingCode: string;
+
+    @Column()
+    method: string;
+
+    @OneToMany(type => OrderItem, orderItem => orderItem.fulfillment)
+    orderItems: OrderItem[];
+}

+ 4 - 0
packages/core/src/entity/order-item/order-item.entity.ts

@@ -4,6 +4,7 @@ import { Column, Entity, ManyToOne } from 'typeorm';
 
 
 import { Calculated } from '../../common/calculated-decorator';
 import { Calculated } from '../../common/calculated-decorator';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
+import { Fulfillment } from '../fulfillment/fulfillment.entity';
 import { OrderLine } from '../order-line/order-line.entity';
 import { OrderLine } from '../order-line/order-line.entity';
 
 
 /**
 /**
@@ -29,6 +30,9 @@ export class OrderItem extends VendureEntity {
 
 
     @Column('simple-json') pendingAdjustments: Adjustment[];
     @Column('simple-json') pendingAdjustments: Adjustment[];
 
 
+    @ManyToOne(type => Fulfillment)
+    fulfillment: Fulfillment;
+
     @Calculated()
     @Calculated()
     get unitPriceWithTax(): number {
     get unitPriceWithTax(): number {
         if (this.unitPriceIncludesTax) {
         if (this.unitPriceIncludesTax) {

+ 3 - 0
packages/core/src/i18n/messages/en.json

@@ -8,6 +8,9 @@
     "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",
     "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
     "country-code-not-valid":  "The countryCode \"{ countryCode }\" was not recognized",
     "country-code-not-valid":  "The countryCode \"{ countryCode }\" was not recognized",
+    "create-fulfillment-items-already-fulfilled": "One or more OrderItems have already been fulfilled",
+    "create-fulfillment-orders-must-be-settled": "One or more OrderItems belong to an Order which is in an invalid state",
+    "create-fulfillment-specify-order-id-or-order-item-ids": "Please specify either orderId or orderItemIds",
     "email-address-not-available": "This email address is not available",
     "email-address-not-available": "This email address is not available",
     "email-address-not-verified": "Please verify this email address before logging in",
     "email-address-not-verified": "Please verify this email address before logging in",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",

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

@@ -14,7 +14,8 @@ export type OrderState =
     | 'ArrangingPayment'
     | 'ArrangingPayment'
     | 'PaymentAuthorized'
     | 'PaymentAuthorized'
     | 'PaymentSettled'
     | 'PaymentSettled'
-    | 'OrderComplete'
+    | 'PartiallyFulfilled'
+    | 'Fulfilled'
     | 'Cancelled';
     | 'Cancelled';
 
 
 export const orderStateTransitions: Transitions<OrderState> = {
 export const orderStateTransitions: Transitions<OrderState> = {
@@ -28,9 +29,12 @@ export const orderStateTransitions: Transitions<OrderState> = {
         to: ['PaymentSettled'],
         to: ['PaymentSettled'],
     },
     },
     PaymentSettled: {
     PaymentSettled: {
-        to: ['OrderComplete', 'Cancelled'],
+        to: ['PartiallyFulfilled', 'Fulfilled', 'Cancelled'],
     },
     },
-    OrderComplete: {
+    PartiallyFulfilled: {
+        to: ['Cancelled', 'Fulfilled'],
+    },
+    Fulfilled: {
         to: ['Cancelled'],
         to: ['Cancelled'],
     },
     },
     Cancelled: {
     Cancelled: {

+ 71 - 2
packages/core/src/service/services/order.service.ts

@@ -1,16 +1,17 @@
 import { InjectConnection } from '@nestjs/typeorm';
 import { InjectConnection } from '@nestjs/typeorm';
 import { PaymentInput } from '@vendure/common/lib/generated-shop-types';
 import { PaymentInput } from '@vendure/common/lib/generated-shop-types';
-import { CreateAddressInput, ShippingMethodQuote } from '@vendure/common/lib/generated-types';
+import { CreateAddressInput, CreateFulfillmentInput, ShippingMethodQuote } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
-import { EntityNotFoundError, IllegalOperationError, OrderItemsLimitError, UserInputError, } from '../../common/error/errors';
+import { EntityNotFoundError, IllegalOperationError, InternalServerError, OrderItemsLimitError, UserInputError } from '../../common/error/errors';
 import { generatePublicId } from '../../common/generate-public-id';
 import { generatePublicId } from '../../common/generate-public-id';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { idsAreEqual } from '../../common/utils';
 import { idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Customer } from '../../entity/customer/customer.entity';
+import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
@@ -70,6 +71,7 @@ export class OrderService {
                 'lines.productVariant.taxCategory',
                 'lines.productVariant.taxCategory',
                 'lines.featuredAsset',
                 'lines.featuredAsset',
                 'lines.items',
                 'lines.items',
+                'lines.items.fulfillment',
                 'lines.taxCategory',
                 'lines.taxCategory',
             ],
             ],
         });
         });
@@ -316,6 +318,73 @@ export class OrderService {
         return payment;
         return payment;
     }
     }
 
 
+    async createFulfillment(ctx: RequestContext, input: CreateFulfillmentInput) {
+        if (!input.orderId && (!input.orderItemIds || input.orderItemIds.length === 0)) {
+            throw new UserInputError('error.create-fulfillment-specify-order-id-or-order-item-ids');
+        }
+        const relatedOrders = new Map<ID, Order>();
+        const orderItems = new Map<ID, OrderItem>();
+
+        if (input.orderItemIds && input.orderItemIds.length) {
+            const items = await this.connection.getRepository(OrderItem).findByIds(input.orderItemIds, {
+                relations: ['line', 'line.order', 'fulfillment'],
+            });
+            for (const item of items) {
+                const order = item.line.order;
+                if (!relatedOrders.has(order.id)) {
+                    relatedOrders.set(order.id, order);
+                }
+                orderItems.set(item.id, item);
+            }
+        }
+        if (input.orderId) {
+            const order = await this.findOne(ctx, input.orderId);
+            if (order) {
+                relatedOrders.set(order.id, order);
+                order.lines.reduce((items, line) => [...items, ...line.items], [] as OrderItem[]).forEach(item => {
+                    orderItems.set(item.id, item);
+                });
+            }
+        }
+        for (const item of Array.from(orderItems.values())) {
+            if (!!item.fulfillment) {
+                throw new IllegalOperationError('error.create-fulfillment-items-already-fulfilled');
+            }
+        }
+        const relatedOrdersArray = Array.from(relatedOrders.values());
+        for (const order of relatedOrdersArray) {
+            if (order.state !== 'PaymentSettled' && order.state !== 'PartiallyFulfilled') {
+                throw new IllegalOperationError('error.create-fulfillment-orders-must-be-settled');
+            }
+        }
+
+        const fulfillment = await this.connection.getRepository(Fulfillment).save(new Fulfillment({
+            trackingCode: input.trackingCode,
+            method: input.method,
+            orderItems: Array.from(orderItems.values()),
+        }));
+
+        for (const order of relatedOrdersArray) {
+            const orderWithFulfillments = await this.connection.getRepository(Order).findOne(order.id, {
+                relations: ['lines', 'lines.items', 'lines.items.fulfillment'],
+            });
+            if (!orderWithFulfillments) {
+                throw new InternalServerError('error.could-not-find-order');
+            }
+            const allOrderItemsFulfilled = orderWithFulfillments.lines
+                .reduce((items, line) => [...items, ...line.items], [] as OrderItem[])
+                .every(orderItem => {
+                    return !!orderItem.fulfillment;
+                });
+            if (allOrderItemsFulfilled) {
+                await this.transitionToState(ctx, order.id, 'Fulfilled');
+            } else {
+                await this.transitionToState(ctx, order.id, 'PartiallyFulfilled');
+            }
+        }
+        return fulfillment;
+    }
+
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {
         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