Browse Source

feat(core): Rework Order shipping to support multiple shipping lines

Relates to #580. This commit changes the data model & API of the Order type so that
shipping is stored as multiple ShippingLine entities. For the time being, we assume a
single ShippingLine per order, but in future we can add the ability to deal with multiple
shipping lines per order.

BREAKING CHANGE: The way shipping charges on Orders are represented has been changed - an Order
now contains multiple ShippingLine entities, each of which has a reference to a ShippingMethod.
This will require a database migration with manual queries to preserve existing order data. See
release blog post for details.
Michael Bromley 5 years ago
parent
commit
a711780758
32 changed files with 328 additions and 154 deletions
  1. 30 18
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 1 1
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  3. 8 6
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  4. 1 1
      packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.html
  5. 3 2
      packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts
  6. 1 1
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  7. 20 13
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  8. 9 1
      packages/common/src/generated-shop-types.ts
  9. 22 14
      packages/common/src/generated-types.ts
  10. 7 4
      packages/core/e2e/graphql/fragments.ts
  11. 26 15
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  12. 13 3
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  13. 6 4
      packages/core/e2e/graphql/shop-definitions.ts
  14. 3 3
      packages/core/e2e/shipping-method-eligibility.e2e-spec.ts
  15. 9 5
      packages/core/e2e/shop-order.e2e-spec.ts
  16. 2 0
      packages/core/src/api/api-internal-modules.ts
  17. 0 12
      packages/core/src/api/resolvers/entity/order-entity.resolver.ts
  18. 23 0
      packages/core/src/api/resolvers/entity/shipping-line-entity.resolver.ts
  19. 8 1
      packages/core/src/api/schema/common/order.type.graphql
  20. 1 1
      packages/core/src/config/shipping-method/default-shipping-eligibility-checker.ts
  21. 2 0
      packages/core/src/entity/entities.ts
  22. 6 3
      packages/core/src/entity/order-item/order-item.entity.ts
  23. 1 1
      packages/core/src/entity/order-line/order-line.entity.ts
  24. 14 19
      packages/core/src/entity/order/order.entity.ts
  25. 39 0
      packages/core/src/entity/shipping-line/shipping-line.entity.ts
  26. 21 10
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  27. 2 1
      packages/core/src/service/helpers/utils/order-utils.ts
  28. 8 0
      packages/core/src/service/services/order-testing.service.ts
  29. 22 2
      packages/core/src/service/services/order.service.ts
  30. 20 13
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  31. 0 0
      schema-admin.json
  32. 0 0
      schema-shop.json

+ 30 - 18
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1287,19 +1287,6 @@ export type UpdateFacetValueInput = {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type Fulfillment = Node & {
-  __typename?: 'Fulfillment';
-  nextStates: Array<Scalars['String']>;
-  id: Scalars['ID'];
-  createdAt: Scalars['DateTime'];
-  updatedAt: Scalars['DateTime'];
-  orderItems: Array<OrderItem>;
-  state: Scalars['String'];
-  method: Scalars['String'];
-  trackingCode?: Maybe<Scalars['String']>;
-  customFields?: Maybe<Scalars['JSON']>;
-};
-
 export type UpdateGlobalSettingsInput = {
   availableLanguages?: Maybe<Array<LanguageCode>>;
   trackInventory?: Maybe<Scalars['Boolean']>;
@@ -1457,9 +1444,9 @@ export type Order = Node & {
   /** Same as subTotal, but inclusive of tax */
   subTotalWithTax: Scalars['Int'];
   currencyCode: CurrencyCode;
+  shippingLines: Array<ShippingLine>;
   shipping: Scalars['Int'];
   shippingWithTax: Scalars['Int'];
-  shippingMethod?: Maybe<ShippingMethod>;
   /** Equal to subTotal plus shipping */
   total: Scalars['Int'];
   /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1475,6 +1462,19 @@ export type OrderHistoryArgs = {
   options?: Maybe<HistoryEntryListOptions>;
 };
 
+export type Fulfillment = Node & {
+  __typename?: 'Fulfillment';
+  nextStates: Array<Scalars['String']>;
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  orderItems: Array<OrderItem>;
+  state: Scalars['String'];
+  method: Scalars['String'];
+  trackingCode?: Maybe<Scalars['String']>;
+  customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateOrderInput = {
   id: Scalars['ID'];
   customFields?: Maybe<Scalars['JSON']>;
@@ -3562,6 +3562,14 @@ export type ShippingMethodQuote = {
   metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+  __typename?: 'ShippingLine';
+  shippingMethod: ShippingMethod;
+  price: Scalars['Int'];
+  priceWithTax: Scalars['Int'];
+  discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
   __typename?: 'OrderItem';
   id: Scalars['ID'];
@@ -5316,9 +5324,12 @@ export type OrderDetailFragment = (
   )>, promotions: Array<(
     { __typename?: 'Promotion' }
     & Pick<Promotion, 'id' | 'couponCode'>
-  )>, shippingMethod?: Maybe<(
-    { __typename?: 'ShippingMethod' }
-    & Pick<ShippingMethod, 'id' | 'code' | 'name' | 'fulfillmentHandlerCode' | 'description'>
+  )>, shippingLines: Array<(
+    { __typename?: 'ShippingLine' }
+    & { shippingMethod: (
+      { __typename?: 'ShippingMethod' }
+      & Pick<ShippingMethod, 'id' | 'code' | 'name' | 'fulfillmentHandlerCode' | 'description'>
+    ) }
   )>, taxSummary: Array<(
     { __typename?: 'OrderTaxSummary' }
     & Pick<OrderTaxSummary, 'taxBase' | 'taxRate' | 'taxTotal'>
@@ -7874,7 +7885,8 @@ export namespace OrderDetail {
   export type Lines = NonNullable<(NonNullable<OrderDetailFragment['lines']>)[number]>;
   export type Discounts = NonNullable<(NonNullable<OrderDetailFragment['discounts']>)[number]>;
   export type Promotions = NonNullable<(NonNullable<OrderDetailFragment['promotions']>)[number]>;
-  export type ShippingMethod = (NonNullable<OrderDetailFragment['shippingMethod']>);
+  export type ShippingLines = NonNullable<(NonNullable<OrderDetailFragment['shippingLines']>)[number]>;
+  export type ShippingMethod = (NonNullable<NonNullable<(NonNullable<OrderDetailFragment['shippingLines']>)[number]>['shippingMethod']>);
   export type TaxSummary = NonNullable<(NonNullable<OrderDetailFragment['taxSummary']>)[number]>;
   export type ShippingAddress = (NonNullable<OrderDetailFragment['shippingAddress']>);
   export type BillingAddress = (NonNullable<OrderDetailFragment['billingAddress']>);

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -81,10 +81,10 @@ const result: PossibleTypesResultData = {
             'Collection',
             'Customer',
             'Facet',
-            'Fulfillment',
             'HistoryEntry',
             'Job',
             'Order',
+            'Fulfillment',
             'PaymentMethod',
             'Product',
             'ProductVariant',

+ 8 - 6
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -142,12 +142,14 @@ export const ORDER_DETAIL_FRAGMENT = gql`
         currencyCode
         shipping
         shippingWithTax
-        shippingMethod {
-            id
-            code
-            name
-            fulfillmentHandlerCode
-            description
+        shippingLines {
+            shippingMethod {
+                id
+                code
+                name
+                fulfillmentHandlerCode
+                description
+            }
         }
         taxSummary {
             taxBase

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.html

@@ -41,7 +41,7 @@
     <div class="shipping-details">
         <vdr-formatted-address [address]="order.shippingAddress"></vdr-formatted-address>
         <h6>{{ 'order.shipping-method' | translate }}</h6>
-        {{ order.shippingMethod?.name }}
+        {{ order.shippingLines[0]?.shippingMethod?.name }}
         <strong>{{ order.shipping / 100 | currency: order.currencyCode }}</strong>
         <vdr-configurable-input
             [operationDefinition]="fulfillmentHandlerDef"

+ 3 - 2
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts

@@ -48,8 +48,9 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
             .mapSingle(data => data.fulfillmentHandlers)
             .subscribe(handlers => {
                 this.fulfillmentHandlerDef =
-                    handlers.find(h => h.code === this.order.shippingMethod?.fulfillmentHandlerCode) ||
-                    handlers[0];
+                    handlers.find(
+                        h => h.code === this.order.shippingLines[0]?.shippingMethod?.fulfillmentHandlerCode,
+                    ) || handlers[0];
                 this.fulfillmentHandler = configurableDefinitionToInstance(this.fulfillmentHandlerDef);
                 this.fulfillmentHandlerControl.patchValue(this.fulfillmentHandler);
                 this.changeDetector.markForCheck();

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -203,7 +203,7 @@
                 </tr>
                 <tr class="shipping">
                     <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
-                    <td class="clr-align-middle">{{ order.shippingMethod?.name }}</td>
+                    <td class="clr-align-middle">{{ order.shippingLines[0]?.shippingMethod?.name }}</td>
                     <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">

+ 20 - 13
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1111,18 +1111,6 @@ export type UpdateFacetValueInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type Fulfillment = Node & {
-    nextStates: Array<Scalars['String']>;
-    id: Scalars['ID'];
-    createdAt: Scalars['DateTime'];
-    updatedAt: Scalars['DateTime'];
-    orderItems: Array<OrderItem>;
-    state: Scalars['String'];
-    method: Scalars['String'];
-    trackingCode?: Maybe<Scalars['String']>;
-    customFields?: Maybe<Scalars['JSON']>;
-};
-
 export type UpdateGlobalSettingsInput = {
     availableLanguages?: Maybe<Array<LanguageCode>>;
     trackInventory?: Maybe<Scalars['Boolean']>;
@@ -1269,9 +1257,9 @@ export type Order = Node & {
     /** Same as subTotal, but inclusive of tax */
     subTotalWithTax: Scalars['Int'];
     currencyCode: CurrencyCode;
+    shippingLines: Array<ShippingLine>;
     shipping: Scalars['Int'];
     shippingWithTax: Scalars['Int'];
-    shippingMethod?: Maybe<ShippingMethod>;
     /** Equal to subTotal plus shipping */
     total: Scalars['Int'];
     /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1286,6 +1274,18 @@ export type OrderHistoryArgs = {
     options?: Maybe<HistoryEntryListOptions>;
 };
 
+export type Fulfillment = Node & {
+    nextStates: Array<Scalars['String']>;
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    state: Scalars['String'];
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateOrderInput = {
     id: Scalars['ID'];
     customFields?: Maybe<Scalars['JSON']>;
@@ -3325,6 +3325,13 @@ export type ShippingMethodQuote = {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+    shippingMethod: ShippingMethod;
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];

+ 9 - 1
packages/common/src/generated-shop-types.ts

@@ -1742,9 +1742,9 @@ export type Order = Node & {
     /** Same as subTotal, but inclusive of tax */
     subTotalWithTax: Scalars['Int'];
     currencyCode: CurrencyCode;
+    shippingLines: Array<ShippingLine>;
     shipping: Scalars['Int'];
     shippingWithTax: Scalars['Int'];
-    shippingMethod?: Maybe<ShippingMethod>;
     /** Equal to subTotal plus shipping */
     total: Scalars['Int'];
     /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1803,6 +1803,14 @@ export type ShippingMethodQuote = {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+    __typename?: 'ShippingLine';
+    shippingMethod: ShippingMethod;
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
     __typename?: 'OrderItem';
     id: Scalars['ID'];

+ 22 - 14
packages/common/src/generated-types.ts

@@ -1256,19 +1256,6 @@ export type UpdateFacetValueInput = {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type Fulfillment = Node & {
-  __typename?: 'Fulfillment';
-  nextStates: Array<Scalars['String']>;
-  id: Scalars['ID'];
-  createdAt: Scalars['DateTime'];
-  updatedAt: Scalars['DateTime'];
-  orderItems: Array<OrderItem>;
-  state: Scalars['String'];
-  method: Scalars['String'];
-  trackingCode?: Maybe<Scalars['String']>;
-  customFields?: Maybe<Scalars['JSON']>;
-};
-
 export type UpdateGlobalSettingsInput = {
   availableLanguages?: Maybe<Array<LanguageCode>>;
   trackInventory?: Maybe<Scalars['Boolean']>;
@@ -1426,9 +1413,9 @@ export type Order = Node & {
   /** Same as subTotal, but inclusive of tax */
   subTotalWithTax: Scalars['Int'];
   currencyCode: CurrencyCode;
+  shippingLines: Array<ShippingLine>;
   shipping: Scalars['Int'];
   shippingWithTax: Scalars['Int'];
-  shippingMethod?: Maybe<ShippingMethod>;
   /** Equal to subTotal plus shipping */
   total: Scalars['Int'];
   /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1444,6 +1431,19 @@ export type OrderHistoryArgs = {
   options?: Maybe<HistoryEntryListOptions>;
 };
 
+export type Fulfillment = Node & {
+  __typename?: 'Fulfillment';
+  nextStates: Array<Scalars['String']>;
+  id: Scalars['ID'];
+  createdAt: Scalars['DateTime'];
+  updatedAt: Scalars['DateTime'];
+  orderItems: Array<OrderItem>;
+  state: Scalars['String'];
+  method: Scalars['String'];
+  trackingCode?: Maybe<Scalars['String']>;
+  customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateOrderInput = {
   id: Scalars['ID'];
   customFields?: Maybe<Scalars['JSON']>;
@@ -3530,6 +3530,14 @@ export type ShippingMethodQuote = {
   metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+  __typename?: 'ShippingLine';
+  shippingMethod: ShippingMethod;
+  price: Scalars['Int'];
+  priceWithTax: Scalars['Int'];
+  discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
   __typename?: 'OrderItem';
   id: Scalars['ID'];

+ 7 - 4
packages/core/e2e/graphql/fragments.ts

@@ -379,10 +379,13 @@ export const ORDER_WITH_LINES_FRAGMENT = gql`
         totalWithTax
         currencyCode
         shipping
-        shippingMethod {
-            id
-            code
-            description
+        shippingWithTax
+        shippingLines {
+            shippingMethod {
+                id
+                code
+                description
+            }
         }
         shippingAddress {
             ...ShippingAddress

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

@@ -1111,18 +1111,6 @@ export type UpdateFacetValueInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type Fulfillment = Node & {
-    nextStates: Array<Scalars['String']>;
-    id: Scalars['ID'];
-    createdAt: Scalars['DateTime'];
-    updatedAt: Scalars['DateTime'];
-    orderItems: Array<OrderItem>;
-    state: Scalars['String'];
-    method: Scalars['String'];
-    trackingCode?: Maybe<Scalars['String']>;
-    customFields?: Maybe<Scalars['JSON']>;
-};
-
 export type UpdateGlobalSettingsInput = {
     availableLanguages?: Maybe<Array<LanguageCode>>;
     trackInventory?: Maybe<Scalars['Boolean']>;
@@ -1269,9 +1257,9 @@ export type Order = Node & {
     /** Same as subTotal, but inclusive of tax */
     subTotalWithTax: Scalars['Int'];
     currencyCode: CurrencyCode;
+    shippingLines: Array<ShippingLine>;
     shipping: Scalars['Int'];
     shippingWithTax: Scalars['Int'];
-    shippingMethod?: Maybe<ShippingMethod>;
     /** Equal to subTotal plus shipping */
     total: Scalars['Int'];
     /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1286,6 +1274,18 @@ export type OrderHistoryArgs = {
     options?: Maybe<HistoryEntryListOptions>;
 };
 
+export type Fulfillment = Node & {
+    nextStates: Array<Scalars['String']>;
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    state: Scalars['String'];
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateOrderInput = {
     id: Scalars['ID'];
     customFields?: Maybe<Scalars['JSON']>;
@@ -3325,6 +3325,13 @@ export type ShippingMethodQuote = {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+    shippingMethod: ShippingMethod;
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -4787,6 +4794,7 @@ export type OrderWithLinesFragment = Pick<
     | 'totalWithTax'
     | 'currencyCode'
     | 'shipping'
+    | 'shippingWithTax'
 > & {
     customer?: Maybe<Pick<Customer, 'id' | 'firstName' | 'lastName'>>;
     lines: Array<
@@ -4796,7 +4804,7 @@ export type OrderWithLinesFragment = Pick<
             items: Array<OrderItemFragment>;
         }
     >;
-    shippingMethod?: Maybe<Pick<ShippingMethod, 'id' | 'code' | 'description'>>;
+    shippingLines: Array<{ shippingMethod: Pick<ShippingMethod, 'id' | 'code' | 'description'> }>;
     shippingAddress?: Maybe<ShippingAddressFragment>;
     payments?: Maybe<
         Array<Pick<Payment, 'id' | 'transactionId' | 'amount' | 'method' | 'state' | 'metadata'>>
@@ -6693,7 +6701,10 @@ export namespace OrderWithLines {
     export type Items = NonNullable<
         NonNullable<NonNullable<NonNullable<OrderWithLinesFragment['lines']>[number]>['items']>[number]
     >;
-    export type ShippingMethod = NonNullable<OrderWithLinesFragment['shippingMethod']>;
+    export type ShippingLines = NonNullable<NonNullable<OrderWithLinesFragment['shippingLines']>[number]>;
+    export type ShippingMethod = NonNullable<
+        NonNullable<NonNullable<OrderWithLinesFragment['shippingLines']>[number]>['shippingMethod']
+    >;
     export type ShippingAddress = NonNullable<OrderWithLinesFragment['shippingAddress']>;
     export type Payments = NonNullable<NonNullable<OrderWithLinesFragment['payments']>[number]>;
 }

+ 13 - 3
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1691,9 +1691,9 @@ export type Order = Node & {
     /** Same as subTotal, but inclusive of tax */
     subTotalWithTax: Scalars['Int'];
     currencyCode: CurrencyCode;
+    shippingLines: Array<ShippingLine>;
     shipping: Scalars['Int'];
     shippingWithTax: Scalars['Int'];
-    shippingMethod?: Maybe<ShippingMethod>;
     /** Equal to subTotal plus shipping */
     total: Scalars['Int'];
     /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1748,6 +1748,13 @@ export type ShippingMethodQuote = {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+    shippingMethod: ShippingMethod;
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -2596,7 +2603,7 @@ export type TestOrderFragmentFragment = Pick<
             discounts: Array<Pick<Adjustment, 'adjustmentSource' | 'amount' | 'description' | 'type'>>;
         }
     >;
-    shippingMethod?: Maybe<Pick<ShippingMethod, 'id' | 'code' | 'description'>>;
+    shippingLines: Array<{ shippingMethod: Pick<ShippingMethod, 'id' | 'code' | 'description'> }>;
     customer?: Maybe<Pick<Customer, 'id'> & { user?: Maybe<Pick<User, 'id' | 'identifier'>> }>;
     history: { items: Array<Pick<HistoryEntry, 'id' | 'type' | 'data'>> };
 };
@@ -3049,7 +3056,10 @@ export namespace TestOrderFragment {
     export type _Discounts = NonNullable<
         NonNullable<NonNullable<NonNullable<TestOrderFragmentFragment['lines']>[number]>['discounts']>[number]
     >;
-    export type ShippingMethod = NonNullable<TestOrderFragmentFragment['shippingMethod']>;
+    export type ShippingLines = NonNullable<NonNullable<TestOrderFragmentFragment['shippingLines']>[number]>;
+    export type ShippingMethod = NonNullable<
+        NonNullable<NonNullable<TestOrderFragmentFragment['shippingLines']>[number]>['shippingMethod']
+    >;
     export type Customer = NonNullable<TestOrderFragmentFragment['customer']>;
     export type User = NonNullable<NonNullable<TestOrderFragmentFragment['customer']>['user']>;
     export type History = NonNullable<TestOrderFragmentFragment['history']>;

+ 6 - 4
packages/core/e2e/graphql/shop-definitions.ts

@@ -31,10 +31,12 @@ export const TEST_ORDER_FRAGMENT = gql`
             }
         }
         shipping
-        shippingMethod {
-            id
-            code
-            description
+        shippingLines {
+            shippingMethod {
+                id
+                code
+                description
+            }
         }
         customer {
             id

+ 3 - 3
packages/core/e2e/shipping-method-eligibility.e2e-spec.ts

@@ -87,7 +87,7 @@ const calculator = new ShippingCalculator({
     },
 });
 
-describe('ShippingMethod resolver', () => {
+describe('ShippingMethod eligibility', () => {
     const { server, adminClient, shopClient } = createTestEnvironment({
         ...testConfig,
         shippingOptions: {
@@ -310,7 +310,7 @@ describe('ShippingMethod resolver', () => {
 
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
             // multiLineShippingMethod assigned as a fallback
-            expect(activeOrder?.shippingMethod?.id).toBe(multiLineShippingMethod.id);
+            expect(activeOrder?.shippingLines?.[0]?.shippingMethod?.id).toBe(multiLineShippingMethod.id);
 
             await shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>(
                 ADJUST_ITEM_QUANTITY,
@@ -340,7 +340,7 @@ describe('ShippingMethod resolver', () => {
             expect(check2Spy).toHaveBeenCalledTimes(3);
 
             // Falls back to the first eligible shipping method
-            expect(removeOrderLine.shippingMethod?.id).toBe(singleLineShippingMethod.id);
+            expect(removeOrderLine.shippingLines[0].shippingMethod?.id).toBe(singleLineShippingMethod.id);
         });
     });
 

+ 9 - 5
packages/core/e2e/shop-order.e2e-spec.ts

@@ -853,7 +853,7 @@ describe('Shop orders', () => {
                 const result = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
 
                 expect(result.activeOrder!.shipping).toEqual(0);
-                expect(result.activeOrder!.shippingMethod).toEqual(null);
+                expect(result.activeOrder!.shippingLines).toEqual([]);
             });
 
             it('setOrderShippingMethod sets the shipping method', async () => {
@@ -869,8 +869,10 @@ describe('Shop orders', () => {
                 const order = activeOrderResult.activeOrder!;
 
                 expect(order.shipping).toBe(shippingMethods[1].price);
-                expect(order.shippingMethod!.id).toBe(shippingMethods[1].id);
-                expect(order.shippingMethod!.description).toBe(shippingMethods[1].description);
+                expect(order.shippingLines[0].shippingMethod!.id).toBe(shippingMethods[1].id);
+                expect(order.shippingLines[0].shippingMethod!.description).toBe(
+                    shippingMethods[1].description,
+                );
             });
 
             it('shipping method is preserved after adjustOrderLine', async () => {
@@ -885,8 +887,10 @@ describe('Shop orders', () => {
                 });
                 orderResultGuard.assertSuccess(adjustOrderLine);
                 expect(adjustOrderLine!.shipping).toBe(shippingMethods[1].price);
-                expect(adjustOrderLine!.shippingMethod!.id).toBe(shippingMethods[1].id);
-                expect(adjustOrderLine!.shippingMethod!.description).toBe(shippingMethods[1].description);
+                expect(adjustOrderLine!.shippingLines[0].shippingMethod!.id).toBe(shippingMethods[1].id);
+                expect(adjustOrderLine!.shippingLines[0].shippingMethod!.description).toBe(
+                    shippingMethods[1].description,
+                );
             });
         });
 

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

@@ -60,6 +60,7 @@ import {
 } from './resolvers/entity/product-variant-entity.resolver';
 import { RefundEntityResolver } from './resolvers/entity/refund-entity.resolver';
 import { RoleEntityResolver } from './resolvers/entity/role-entity.resolver';
+import { ShippingLineEntityResolver } from './resolvers/entity/shipping-line-entity.resolver';
 import { UserEntityResolver } from './resolvers/entity/user-entity.resolver';
 import { ShopAuthResolver } from './resolvers/shop/shop-auth.resolver';
 import { ShopCustomerResolver } from './resolvers/shop/shop-customer.resolver';
@@ -117,6 +118,7 @@ export const entityResolvers = [
     ProductVariantEntityResolver,
     RefundEntityResolver,
     RoleEntityResolver,
+    ShippingLineEntityResolver,
     UserEntityResolver,
 ];
 

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

@@ -27,18 +27,6 @@ export class OrderEntityResolver {
         return this.orderService.getOrderPayments(ctx, order.id);
     }
 
-    @ResolveField()
-    async shippingMethod(@Ctx() ctx: RequestContext, @Parent() order: Order) {
-        if (order.shippingMethodId) {
-            // Does not need to be decoded because it is an internal property
-            // which is never exposed to the outside world.
-            const shippingMethodId = order.shippingMethodId;
-            return this.shippingMethodService.findOne(ctx, shippingMethodId);
-        } else {
-            return null;
-        }
-    }
-
     @ResolveField()
     async fulfillments(@Ctx() ctx: RequestContext, @Parent() order: Order) {
         return this.orderService.getOrderFulfillments(ctx, order);

+ 23 - 0
packages/core/src/api/resolvers/entity/shipping-line-entity.resolver.ts

@@ -0,0 +1,23 @@
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+
+import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
+import { ShippingMethodService } from '../../../service/services/shipping-method.service';
+import { RequestContext } from '../../common/request-context';
+import { Ctx } from '../../decorators/request-context.decorator';
+
+@Resolver('ShippingLine')
+export class ShippingLineEntityResolver {
+    constructor(private shippingMethodService: ShippingMethodService) {}
+
+    @ResolveField()
+    async shippingMethod(@Ctx() ctx: RequestContext, @Parent() shippingLine: ShippingLine) {
+        if (shippingLine.shippingMethodId) {
+            // Does not need to be decoded because it is an internal property
+            // which is never exposed to the outside world.
+            const shippingMethodId = shippingLine.shippingMethodId;
+            return this.shippingMethodService.findOne(ctx, shippingMethodId);
+        } else {
+            return null;
+        }
+    }
+}

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

@@ -36,9 +36,9 @@ type Order implements Node {
     "Same as subTotal, but inclusive of tax"
     subTotalWithTax: Int!
     currencyCode: CurrencyCode!
+    shippingLines: [ShippingLine!]!
     shipping: Int!
     shippingWithTax: Int!
-    shippingMethod: ShippingMethod
     """
     Equal to subTotal plus shipping
     """
@@ -94,6 +94,13 @@ type ShippingMethodQuote {
     metadata: JSON
 }
 
+type ShippingLine {
+    shippingMethod: ShippingMethod!
+    price: Int!
+    priceWithTax: Int!
+    discounts: [Adjustment!]!
+}
+
 type OrderItem implements Node {
     id: ID!
     createdAt: DateTime!

+ 1 - 1
packages/core/src/config/shipping-method/default-shipping-eligibility-checker.ts

@@ -19,6 +19,6 @@ export const defaultShippingEligibilityChecker = new ShippingEligibilityChecker(
         },
     },
     check: (ctx, order, args) => {
-        return order.total >= args.orderMinimum;
+        return order.subTotalWithTax >= args.orderMinimum;
     },
 });

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

@@ -43,6 +43,7 @@ import { Role } from './role/role.entity';
 import { AnonymousSession } from './session/anonymous-session.entity';
 import { AuthenticatedSession } from './session/authenticated-session.entity';
 import { Session } from './session/session.entity';
+import { ShippingLine } from './shipping-line/shipping-line.entity';
 import { ShippingMethodTranslation } from './shipping-method/shipping-method-translation.entity';
 import { ShippingMethod } from './shipping-method/shipping-method.entity';
 import { Allocation } from './stock-movement/allocation.entity';
@@ -109,6 +110,7 @@ export const coreEntitiesMap = {
     Role,
     Sale,
     Session,
+    ShippingLine,
     ShippingMethod,
     ShippingMethodTranslation,
     StockAdjustment,

+ 6 - 3
packages/core/src/entity/order-item/order-item.entity.ts

@@ -32,16 +32,19 @@ export class OrderItem extends VendureEntity {
      * This is the price as listed by the ProductVariant, which, depending on the
      * current Channel, may or may not include tax.
      */
-    @Column() readonly listPrice: number;
+    @Column()
+    readonly listPrice: number;
 
     /**
      * @description
      * Whether or not the listPrice includes tax, which depends on the settings
      * of the current Channel.
      */
-    @Column() readonly listPriceIncludesTax: boolean;
+    @Column()
+    readonly listPriceIncludesTax: boolean;
 
-    @Column('simple-json') adjustments: Adjustment[];
+    @Column('simple-json')
+    adjustments: Adjustment[];
 
     @Column('simple-json')
     taxLines: TaxLine[];

+ 1 - 1
packages/core/src/entity/order-line/order-line.entity.ts

@@ -82,7 +82,7 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
     @Calculated()
     get adjustments(): Adjustment[] {
         return this.activeItems.reduce(
-            (adjustments, item) => [...adjustments, ...item.adjustments],
+            (adjustments, item) => [...adjustments, ...(item.adjustments || [])],
             [] as Adjustment[],
         );
     }

+ 14 - 19
packages/core/src/entity/order/order.entity.ts

@@ -16,6 +16,7 @@ import { OrderItem } from '../order-item/order-item.entity';
 import { OrderLine } from '../order-line/order-line.entity';
 import { Payment } from '../payment/payment.entity';
 import { Promotion } from '../promotion/promotion.entity';
+import { ShippingLine } from '../shipping-line/shipping-line.entity';
 import { ShippingMethod } from '../shipping-method/shipping-method.entity';
 
 /**
@@ -68,18 +69,6 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
     @Column('varchar')
     currencyCode: CurrencyCode;
 
-    @EntityId({ nullable: true })
-    shippingMethodId: ID | null;
-
-    @ManyToOne(type => ShippingMethod)
-    shippingMethod: ShippingMethod | null;
-
-    @Column({ default: 0 })
-    shipping: number;
-
-    @Column({ default: 0 })
-    shippingWithTax: number;
-
     @Column(type => CustomOrderFields)
     customFields: CustomOrderFields;
 
@@ -90,14 +79,20 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
     @JoinTable()
     channels: Channel[];
 
-    /**
-     * @description
-     * The subTotal is the total of the OrderLines, before order-level promotions
-     * and shipping has been applied.
-     */
-    @Column() subTotal: number;
+    @Column()
+    subTotal: number;
+
+    @Column()
+    subTotalWithTax: number;
+
+    @OneToMany(type => ShippingLine, shippingLine => shippingLine.order)
+    shippingLines: ShippingLine[];
+
+    @Column({ default: 0 })
+    shipping: number;
 
-    @Column() subTotalWithTax: number;
+    @Column({ default: 0 })
+    shippingWithTax: number;
 
     @Calculated()
     get discounts(): Adjustment[] {

+ 39 - 0
packages/core/src/entity/shipping-line/shipping-line.entity.ts

@@ -0,0 +1,39 @@
+import { Adjustment } from '@vendure/common/lib/generated-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+import { Calculated } from '../../common/calculated-decorator';
+import { VendureEntity } from '../base/base.entity';
+import { EntityId } from '../entity-id.decorator';
+import { Order } from '../order/order.entity';
+import { ShippingMethod } from '../shipping-method/shipping-method.entity';
+
+@Entity()
+export class ShippingLine extends VendureEntity {
+    constructor(input?: DeepPartial<ShippingLine>) {
+        super(input);
+    }
+
+    @EntityId()
+    shippingMethodId: ID | null;
+
+    @ManyToOne(type => ShippingMethod)
+    shippingMethod: ShippingMethod | null;
+
+    @ManyToOne(type => Order, order => order.shippingLines)
+    order: Order;
+
+    @Column()
+    price: number;
+
+    @Column()
+    priceWithTax: number;
+
+    @Column('simple-json')
+    adjustments: Adjustment[];
+
+    @Calculated()
+    get discounts(): Adjustment[] {
+        return this.adjustments;
+    }
+}

+ 21 - 10
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -10,11 +10,11 @@ import { ConfigService } from '../../../config/config.service';
 import { OrderItem, OrderLine, TaxCategory, TaxRate } from '../../../entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Promotion } from '../../../entity/promotion/promotion.entity';
+import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
 import { Zone } from '../../../entity/zone/zone.entity';
 import { ShippingMethodService } from '../../services/shipping-method.service';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { ZoneService } from '../../services/zone.service';
-import { TransactionalConnection } from '../../transaction/transactional-connection';
 import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
 import { TaxCalculator } from '../tax-calculator/tax-calculator';
 
@@ -23,7 +23,6 @@ import { prorate } from './prorate';
 @Injectable()
 export class OrderCalculator {
     constructor(
-        private connection: TransactionalConnection,
         private configService: ConfigService,
         private zoneService: ZoneService,
         private taxRateService: TaxRateService,
@@ -72,11 +71,11 @@ export class OrderCalculator {
             }
 
             // Then test and apply promotions
-            const totalBeforePromotions = order.total;
+            const totalBeforePromotions = order.subTotal;
             const itemsModifiedByPromotions = await this.applyPromotions(ctx, order, promotions);
             itemsModifiedByPromotions.forEach(item => updatedOrderItems.add(item));
 
-            if (order.total !== totalBeforePromotions || itemsModifiedByPromotions.length) {
+            if (order.subTotal !== totalBeforePromotions || itemsModifiedByPromotions.length) {
                 // Finally, re-calculate taxes because the promotions may have
                 // altered the unit prices, which in turn will alter the tax payable.
                 this.applyTaxes(ctx, order, activeTaxZone);
@@ -304,8 +303,10 @@ export class OrderCalculator {
     }
 
     private async applyShipping(ctx: RequestContext, order: Order) {
+        const shippingLine: ShippingLine | undefined = order.shippingLines[0];
         const currentShippingMethod =
-            order.shippingMethodId && (await this.shippingMethodService.findOne(ctx, order.shippingMethodId));
+            shippingLine?.shippingMethodId &&
+            (await this.shippingMethodService.findOne(ctx, shippingLine.shippingMethodId));
         if (!currentShippingMethod) {
             return;
         }
@@ -313,8 +314,8 @@ export class OrderCalculator {
         if (currentMethodStillEligible) {
             const result = await currentShippingMethod.apply(ctx, order);
             if (result) {
-                order.shipping = result.price;
-                order.shippingWithTax = result.priceWithTax;
+                shippingLine.price = result.price;
+                shippingLine.priceWithTax = result.priceWithTax;
             }
             return;
         }
@@ -323,9 +324,9 @@ export class OrderCalculator {
         ]);
         if (results && results.length) {
             const cheapest = results[0];
-            order.shipping = cheapest.result.price;
-            order.shippingWithTax = cheapest.result.priceWithTax;
-            order.shippingMethod = cheapest.method;
+            shippingLine.price = cheapest.result.price;
+            shippingLine.priceWithTax = cheapest.result.priceWithTax;
+            shippingLine.shippingMethod = cheapest.method;
         }
     }
 
@@ -340,5 +341,15 @@ export class OrderCalculator {
 
         order.subTotal = totalPrice;
         order.subTotalWithTax = totalPriceWithTax;
+
+        let shippingPrice = 0;
+        let shippingPriceWithTax = 0;
+        for (const shippingLine of order.shippingLines) {
+            shippingPrice += shippingLine.price;
+            shippingPriceWithTax += shippingLine.priceWithTax;
+        }
+
+        order.shipping = shippingPrice;
+        order.shippingWithTax = shippingPriceWithTax;
     }
 }

+ 2 - 1
packages/core/src/service/helpers/utils/order-utils.ts

@@ -7,7 +7,8 @@ import { PaymentState } from '../payment-state-machine/payment-state';
  */
 export function orderTotalIsCovered(order: Order, state: PaymentState): boolean {
     return (
-        order.payments.filter(p => p.state === state).reduce((sum, p) => sum + p.amount, 0) === order.total
+        order.payments.filter(p => p.state === state).reduce((sum, p) => sum + p.amount, 0) ===
+        order.totalWithTax
     );
 }
 

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

@@ -14,6 +14,7 @@ import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
@@ -127,6 +128,13 @@ export class OrderTestingService {
                 orderLine.items.push(orderItem);
             }
         }
+        mockOrder.shippingLines = [
+            new ShippingLine({
+                price: 0,
+                priceWithTax: 0,
+                adjustments: [],
+            }),
+        ];
         await this.orderCalculator.applyPriceAdjustments(ctx, mockOrder, []);
         return mockOrder;
     }

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

@@ -73,6 +73,7 @@ import { Payment } from '../../entity/payment/payment.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Refund } from '../../entity/refund/refund.entity';
+import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
 import { User } from '../../entity/user/user.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
@@ -160,6 +161,7 @@ export class OrderService {
             .createQueryBuilder('order')
             .leftJoin('order.channels', 'channel')
             .leftJoinAndSelect('order.customer', 'customer')
+            .leftJoinAndSelect('order.shippingLines', 'shippingLines')
             .leftJoinAndSelect('customer.user', 'user')
             .leftJoinAndSelect('order.lines', 'lines')
             .leftJoinAndSelect('lines.productVariant', 'productVariant')
@@ -210,6 +212,7 @@ export class OrderService {
                     'lines.productVariant.options',
                     'customer',
                     'channels',
+                    'shippingLines',
                 ],
                 channelId: ctx.channelId,
                 ctx,
@@ -264,6 +267,7 @@ export class OrderService {
                     channelId: ctx.channelId,
                 })
                 .leftJoinAndSelect('order.customer', 'customer')
+                .leftJoinAndSelect('order.shippingLines', 'shippingLines')
                 .where('active = :active', { active: true })
                 .andWhere('order.customer.id = :customerId', { customerId: customer.id })
                 .orderBy('order.createdAt', 'DESC')
@@ -546,7 +550,22 @@ export class OrderService {
         if (!shippingMethod) {
             return new IneligibleShippingMethodError();
         }
-        order.shippingMethod = shippingMethod;
+        let shippingLine: ShippingLine | undefined = order.shippingLines[0];
+        if (shippingLine) {
+            shippingLine.shippingMethod = shippingMethod;
+        } else {
+            shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
+                new ShippingLine({
+                    shippingMethod,
+                    order,
+                    adjustments: [],
+                    price: 0,
+                    priceWithTax: 0,
+                }),
+            );
+            order.shippingLines = [shippingLine];
+        }
+        await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         await this.applyPriceAdjustments(ctx, order);
         return this.connection.getRepository(ctx, Order).save(order);
@@ -678,7 +697,7 @@ export class OrderService {
             this.eventBus.publish(
                 new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
             );
-            const orderTotalSettled = payment.amount === payment.order.total;
+            const orderTotalSettled = payment.amount === payment.order.totalWithTax;
             if (
                 orderTotalSettled &&
                 this.orderStateMachine.canTransition(payment.order.state, 'PaymentSettled')
@@ -1132,6 +1151,7 @@ export class OrderService {
         );
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         await this.connection.getRepository(ctx, OrderItem).save(updatedItems, { reload: false });
+        await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
         return order;
     }
 

+ 20 - 13
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1111,18 +1111,6 @@ export type UpdateFacetValueInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type Fulfillment = Node & {
-    nextStates: Array<Scalars['String']>;
-    id: Scalars['ID'];
-    createdAt: Scalars['DateTime'];
-    updatedAt: Scalars['DateTime'];
-    orderItems: Array<OrderItem>;
-    state: Scalars['String'];
-    method: Scalars['String'];
-    trackingCode?: Maybe<Scalars['String']>;
-    customFields?: Maybe<Scalars['JSON']>;
-};
-
 export type UpdateGlobalSettingsInput = {
     availableLanguages?: Maybe<Array<LanguageCode>>;
     trackInventory?: Maybe<Scalars['Boolean']>;
@@ -1269,9 +1257,9 @@ export type Order = Node & {
     /** Same as subTotal, but inclusive of tax */
     subTotalWithTax: Scalars['Int'];
     currencyCode: CurrencyCode;
+    shippingLines: Array<ShippingLine>;
     shipping: Scalars['Int'];
     shippingWithTax: Scalars['Int'];
-    shippingMethod?: Maybe<ShippingMethod>;
     /** Equal to subTotal plus shipping */
     total: Scalars['Int'];
     /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
@@ -1286,6 +1274,18 @@ export type OrderHistoryArgs = {
     options?: Maybe<HistoryEntryListOptions>;
 };
 
+export type Fulfillment = Node & {
+    nextStates: Array<Scalars['String']>;
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    state: Scalars['String'];
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateOrderInput = {
     id: Scalars['ID'];
     customFields?: Maybe<Scalars['JSON']>;
@@ -3325,6 +3325,13 @@ export type ShippingMethodQuote = {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type ShippingLine = {
+    shippingMethod: ShippingMethod;
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    discounts: Array<Adjustment>;
+};
+
 export type OrderItem = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];

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