فهرست منبع

feat(core): Create underlying APIs to support multivendor Orders

Relates to #1329
Michael Bromley 3 سال پیش
والد
کامیت
3d9f7e8934
100فایلهای تغییر یافته به همراه3548 افزوده شده و 1253 حذف شده
  1. 29 29
      packages/admin-ui/i18n-coverage.json
  2. 1 0
      packages/admin-ui/src/lib/core/src/common/component-registry-types.ts
  3. 211 1
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  4. 5 1
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  5. 3 11
      packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.ts
  6. 7 0
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts
  7. 14 0
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  8. 65 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  9. 49 1
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  10. 37 0
      packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts
  11. 1 0
      packages/admin-ui/src/lib/core/src/public_api.ts
  12. 9 3
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  13. 4 0
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  14. 20 4
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  15. 22 0
      packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.html
  16. 0 0
      packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.scss
  17. 49 0
      packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.ts
  18. 25 0
      packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.graphql.ts
  19. 2 0
      packages/admin-ui/src/lib/order/src/order.module.ts
  20. 47 7
      packages/admin-ui/src/lib/order/src/order.routes.ts
  21. 2 0
      packages/admin-ui/src/lib/order/src/providers/routing/order-resolver.ts
  22. 2 0
      packages/admin-ui/src/lib/order/src/public_api.ts
  23. 12 0
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.html
  24. 10 2
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts
  25. 52 0
      packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.html
  26. 4 0
      packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.scss
  27. 133 0
      packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.ts
  28. 47 0
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html
  29. 0 0
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.scss
  30. 52 0
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts
  31. 1 0
      packages/admin-ui/src/lib/settings/src/providers/routing/channel-resolver.ts
  32. 25 0
      packages/admin-ui/src/lib/settings/src/providers/routing/seller-resolver.ts
  33. 3 0
      packages/admin-ui/src/lib/settings/src/public_api.ts
  34. 4 0
      packages/admin-ui/src/lib/settings/src/settings.module.ts
  35. 28 0
      packages/admin-ui/src/lib/settings/src/settings.routes.ts
  36. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  37. 11 3
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  38. 11 3
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  39. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  40. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  41. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  42. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  43. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  44. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  45. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  46. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  47. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  48. 10 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  49. 537 504
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  50. 645 627
      packages/common/src/generated-shop-types.ts
  51. 210 0
      packages/common/src/generated-types.ts
  52. 4 0
      packages/core/e2e/__snapshots__/administrator.e2e-spec.ts.snap
  53. 1 0
      packages/core/e2e/custom-field-relations.e2e-spec.ts
  54. 205 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  55. 35 2
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  56. 1 1
      packages/core/e2e/graphql/shop-definitions.ts
  57. 4 0
      packages/core/src/api/api-internal-modules.ts
  58. 1 1
      packages/core/src/api/resolvers/admin/draft-order.resolver.ts
  59. 59 0
      packages/core/src/api/resolvers/admin/seller.resolver.ts
  60. 19 0
      packages/core/src/api/resolvers/entity/channel-entity.resolver.ts
  61. 30 1
      packages/core/src/api/resolvers/entity/order-entity.resolver.ts
  62. 2 0
      packages/core/src/api/schema/admin-api/channel.api.graphql
  63. 4 0
      packages/core/src/api/schema/admin-api/order-admin.type.graphql
  64. 29 0
      packages/core/src/api/schema/admin-api/seller.api.graphql
  65. 1 0
      packages/core/src/api/schema/common/channel.type.graphql
  66. 7 0
      packages/core/src/api/schema/common/order.type.graphql
  67. 6 0
      packages/core/src/api/schema/common/seller.type.graphql
  68. 1 1
      packages/core/src/api/schema/shop-api/shop.api.graphql
  69. 1 0
      packages/core/src/common/constants.ts
  70. 5 1
      packages/core/src/config/config.module.ts
  71. 1 0
      packages/core/src/config/custom-field/custom-field-types.ts
  72. 5 0
      packages/core/src/config/default-config.ts
  73. 4 0
      packages/core/src/config/index.ts
  74. 5 0
      packages/core/src/config/order/default-order-seller-strategy.ts
  75. 1 0
      packages/core/src/config/order/order-item-price-calculation-strategy.ts
  76. 28 0
      packages/core/src/config/order/order-seller-strategy.ts
  77. 14 0
      packages/core/src/config/shipping-method/default-shipping-line-assignment-strategy.ts
  78. 11 0
      packages/core/src/config/shipping-method/shipping-line-assignment-strategy.ts
  79. 4 1
      packages/core/src/config/vendure-config.ts
  80. 12 1
      packages/core/src/entity/channel/channel.entity.ts
  81. 1 0
      packages/core/src/entity/custom-entity-fields.ts
  82. 2 0
      packages/core/src/entity/entities.ts
  83. 17 1
      packages/core/src/entity/order-line/order-line.entity.ts
  84. 15 0
      packages/core/src/entity/order/aggregate-order.entity.ts
  85. 13 1
      packages/core/src/entity/order/order.entity.ts
  86. 14 0
      packages/core/src/entity/order/seller-order.entity.ts
  87. 2 0
      packages/core/src/entity/register-custom-entity-fields.ts
  88. 28 0
      packages/core/src/entity/seller/seller.entity.ts
  89. 1 1
      packages/core/src/entity/shipping-line/shipping-line.entity.ts
  90. 10 0
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  91. 141 0
      packages/core/src/service/helpers/order-splitter/order-splitter.ts
  92. 2 0
      packages/core/src/service/index.ts
  93. 3 0
      packages/core/src/service/initializer.service.ts
  94. 4 0
      packages/core/src/service/service.module.ts
  95. 31 12
      packages/core/src/service/services/channel.service.ts
  96. 109 32
      packages/core/src/service/services/order.service.ts
  97. 75 0
      packages/core/src/service/services/seller.service.ts
  98. 3 1
      packages/dev-server/dev-config.ts
  99. 65 0
      packages/dev-server/test-plugins/multivendor-plugin/config/mv-fulfillment-process.ts
  100. 13 0
      packages/dev-server/test-plugins/multivendor-plugin/config/mv-order-process.ts

+ 29 - 29
packages/admin-ui/i18n-coverage.json

@@ -1,71 +1,71 @@
 {
-  "generatedOn": "2022-10-18T18:49:58.336Z",
-  "lastCommit": "43421c67f52ac66880b606a17fe62b19d6385d85",
+  "generatedOn": "2023-01-12T12:03:15.456Z",
+  "lastCommit": "0e2e4d4b9b3308477154f60876c7755c57afd2e1",
   "translationStatus": {
     "cs": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 593,
-      "percentage": 86
+      "percentage": 85
     },
     "de": {
-      "tokenCount": 687,
-      "translatedCount": 572,
-      "percentage": 83
+      "tokenCount": 700,
+      "translatedCount": 574,
+      "percentage": 82
     },
     "en": {
-      "tokenCount": 687,
-      "translatedCount": 685,
+      "tokenCount": 700,
+      "translatedCount": 699,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 624,
-      "percentage": 91
+      "percentage": 89
     },
     "fr": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 614,
-      "percentage": 89
+      "percentage": 88
     },
     "it": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 622,
-      "percentage": 91
+      "percentage": 89
     },
     "pl": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 407,
-      "percentage": 59
+      "percentage": 58
     },
     "pt_BR": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 591,
-      "percentage": 86
+      "percentage": 84
     },
     "pt_PT": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 635,
-      "percentage": 92
+      "percentage": 91
     },
     "ru": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 621,
-      "percentage": 90
+      "percentage": 89
     },
     "uk": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 621,
-      "percentage": 90
+      "percentage": 89
     },
     "zh_Hans": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 559,
-      "percentage": 81
+      "percentage": 80
     },
     "zh_Hant": {
-      "tokenCount": 687,
+      "tokenCount": 700,
       "translatedCount": 387,
-      "percentage": 56
+      "percentage": 55
     }
   }
 }

+ 1 - 0
packages/admin-ui/src/lib/core/src/common/component-registry-types.ts

@@ -104,6 +104,7 @@ export type CustomDetailComponentLocationId =
     | 'payment-method-detail'
     | 'product-detail'
     | 'promotion-detail'
+    | 'seller-detail'
     | 'shipping-method-detail'
     | 'tax-category-detail'
     | 'tax-rate-detail';

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 211 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts


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

@@ -173,6 +173,7 @@
       "Return",
       "Role",
       "Sale",
+      "Seller",
       "ShippingMethod",
       "StockAdjustment",
       "Surcharge",
@@ -180,6 +181,7 @@
       "TaxCategory",
       "TaxRate",
       "User",
+      "Vendor",
       "Zone"
     ],
     "PaginatedList": [
@@ -199,9 +201,11 @@
       "ProductVariantList",
       "PromotionList",
       "RoleList",
+      "SellerList",
       "ShippingMethodList",
       "TagList",
-      "TaxRateList"
+      "TaxRateList",
+      "VendorList"
     ],
     "RefundOrderResult": [
       "AlreadyRefundedError",

+ 3 - 11
packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.ts

@@ -1,14 +1,12 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
-import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { combineLatest, Observable } from 'rxjs';
 import { filter, map, startWith } from 'rxjs/operators';
 
 import { CurrentUserChannel } from '../../common/generated-types';
 import { DataService } from '../../data/providers/data.service';
-import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
+import { ChannelService } from '../../providers/channel/channel.service';
 
 @Component({
     selector: 'vdr-channel-switcher',
@@ -22,7 +20,7 @@ export class ChannelSwitcherComponent implements OnInit {
     channelCount$: Observable<number>;
     filterControl = new FormControl('');
     activeChannelCode$: Observable<string>;
-    constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
+    constructor(private dataService: DataService, private channelService: ChannelService) {}
 
     ngOnInit() {
         const channels$ = this.dataService.client.userStatus().mapStream(data => data.userStatus.channels);
@@ -45,12 +43,6 @@ export class ChannelSwitcherComponent implements OnInit {
     }
 
     setActiveChannel(channelId: string) {
-        this.dataService.client.setActiveChannel(channelId).subscribe(({ setActiveChannel }) => {
-            const activeChannel = setActiveChannel.channels.find(c => c.id === channelId);
-            if (activeChannel) {
-                this.localStorageService.set('activeChannelToken', activeChannel.token);
-            }
-            this.filterControl.patchValue('');
-        });
+        this.channelService.setActiveChannel(channelId).subscribe(() => this.filterControl.patchValue(''));
     }
 }

+ 7 - 0
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts

@@ -185,6 +185,13 @@ export class MainNavComponent implements OnInit, OnDestroy {
                 collapsible: true,
                 collapsedByDefault: true,
                 items: [
+                    {
+                        requiresPermission: allow(Permission.ReadSeller),
+                        id: 'sellers',
+                        label: _('nav.sellers'),
+                        routerLink: ['/settings', 'sellers'],
+                        icon: 'store',
+                    },
                     {
                         requiresPermission: allow(Permission.ReadChannel),
                         id: 'channels',

+ 14 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -55,6 +55,7 @@ export const ORDER_FRAGMENT = gql`
         id
         createdAt
         updatedAt
+        type
         orderPlacedAt
         code
         state
@@ -135,6 +136,19 @@ export const ORDER_DETAIL_FRAGMENT = gql`
         id
         createdAt
         updatedAt
+        type
+        aggregateOrder {
+            id
+            code
+        }
+        sellerOrders {
+            id
+            code
+            channels {
+                id
+                code
+            }
+        }
         code
         state
         nextStates

+ 65 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -337,9 +337,23 @@ export const CHANNEL_FRAGMENT = gql`
             id
             name
         }
+        seller {
+            id
+            name
+        }
+    }
+`;
+
+export const SELLER_FRAGMENT = gql`
+    fragment Seller on Seller {
+        id
+        createdAt
+        updatedAt
+        name
     }
 `;
 
+// TODO v2: change this to paginated list
 export const GET_CHANNELS = gql`
     query GetChannels {
         channels {
@@ -358,6 +372,54 @@ export const GET_CHANNEL = gql`
     ${CHANNEL_FRAGMENT}
 `;
 
+export const GET_SELLERS = gql`
+    query GetSellers($options: SellerListOptions) {
+        sellers(options: $options) {
+            items {
+                ...Seller
+            }
+            totalItems
+        }
+    }
+    ${SELLER_FRAGMENT}
+`;
+
+export const GET_SELLER = gql`
+    query GetSeller($id: ID!) {
+        seller(id: $id) {
+            ...Seller
+        }
+    }
+    ${SELLER_FRAGMENT}
+`;
+
+export const CREATE_SELLER = gql`
+    mutation CreateSeller($input: CreateSellerInput!) {
+        createSeller(input: $input) {
+            ...Seller
+        }
+    }
+    ${SELLER_FRAGMENT}
+`;
+
+export const UPDATE_SELLER = gql`
+    mutation UpdateSeller($input: UpdateSellerInput!) {
+        updateSeller(input: $input) {
+            ...Seller
+        }
+    }
+    ${SELLER_FRAGMENT}
+`;
+
+export const DELETE_SELLER = gql`
+    mutation DeleteSeller($id: ID!) {
+        deleteSeller(id: $id) {
+            result
+            message
+        }
+    }
+`;
+
 export const GET_ACTIVE_CHANNEL = gql`
     query GetActiveChannel {
         activeChannel {
@@ -717,6 +779,9 @@ export const GET_SERVER_CONFIG = gql`
                     Promotion {
                         ...CustomFields
                     }
+                    Seller {
+                        ...CustomFields
+                    }
                     ShippingMethod {
                         ...CustomFields
                     }

+ 49 - 1
packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts

@@ -2,19 +2,21 @@ import { FetchPolicy, WatchQueryFetchPolicy } from '@apollo/client/core';
 import { pick } from '@vendure/common/lib/pick';
 
 import * as Codegen from '../../common/generated-types';
-import { JobListOptions, JobState } from '../../common/generated-types';
+import { JobListOptions, JobState, SellerListOptions } from '../../common/generated-types';
 import {
     ADD_MEMBERS_TO_ZONE,
     CANCEL_JOB,
     CREATE_CHANNEL,
     CREATE_COUNTRY,
     CREATE_PAYMENT_METHOD,
+    CREATE_SELLER,
     CREATE_TAX_CATEGORY,
     CREATE_TAX_RATE,
     CREATE_ZONE,
     DELETE_CHANNEL,
     DELETE_COUNTRY,
     DELETE_PAYMENT_METHOD,
+    DELETE_SELLER,
     DELETE_TAX_CATEGORY,
     DELETE_TAX_RATE,
     DELETE_ZONE,
@@ -32,6 +34,8 @@ import {
     GET_PAYMENT_METHOD,
     GET_PAYMENT_METHOD_LIST,
     GET_PAYMENT_METHOD_OPERATIONS,
+    GET_SELLER,
+    GET_SELLERS,
     GET_TAX_CATEGORIES,
     GET_TAX_CATEGORY,
     GET_TAX_RATE,
@@ -43,6 +47,7 @@ import {
     UPDATE_COUNTRY,
     UPDATE_GLOBAL_SETTINGS,
     UPDATE_PAYMENT_METHOD,
+    UPDATE_SELLER,
     UPDATE_TAX_CATEGORY,
     UPDATE_TAX_RATE,
     UPDATE_ZONE,
@@ -281,6 +286,49 @@ export class SettingsDataService {
         );
     }
 
+    getSellers(options?: SellerListOptions) {
+        return this.baseDataService.query<Codegen.GetSellersQuery, Codegen.GetSellersQueryVariables>(
+            GET_SELLERS,
+            { options },
+        );
+    }
+
+    getSeller(id: string) {
+        return this.baseDataService.query<Codegen.GetSellerQuery, Codegen.GetSellerQueryVariables>(
+            GET_SELLER,
+            {
+                id,
+            },
+        );
+    }
+
+    createSeller(input: Codegen.CreateSellerInput) {
+        return this.baseDataService.mutate<
+            Codegen.CreateSellerMutation,
+            Codegen.CreateSellerMutationVariables
+        >(CREATE_SELLER, {
+            input,
+        });
+    }
+
+    updateSeller(input: Codegen.UpdateSellerInput) {
+        return this.baseDataService.mutate<
+            Codegen.UpdateSellerMutation,
+            Codegen.UpdateSellerMutationVariables
+        >(UPDATE_SELLER, {
+            input,
+        });
+    }
+
+    deleteSeller(id: string) {
+        return this.baseDataService.mutate<
+            Codegen.DeleteSellerMutation,
+            Codegen.DeleteSellerMutationVariables
+        >(DELETE_SELLER, {
+            id,
+        });
+    }
+
     getActiveChannel(fetchPolicy?: FetchPolicy) {
         return this.baseDataService.query<
             Codegen.GetActiveChannelQuery,

+ 37 - 0
packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts

@@ -0,0 +1,37 @@
+import { Injectable } from '@angular/core';
+import { SetActiveChannelMutation, UserStatusFragment } from '@vendure/admin-ui/core';
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
+import { Observable } from 'rxjs';
+import { map, shareReplay, tap } from 'rxjs/operators';
+
+import { DataService } from '../../data/providers/data.service';
+import { LocalStorageService } from '../local-storage/local-storage.service';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class ChannelService {
+    defaultChannelIsActive$: Observable<boolean>;
+
+    constructor(private dataService: DataService, private localStorageService: LocalStorageService) {
+        this.defaultChannelIsActive$ = this.dataService.client
+            .userStatus()
+            .mapStream(({ userStatus }) => {
+                const activeChannel = userStatus.channels.find(c => c.id === userStatus.activeChannelId);
+                return activeChannel ? activeChannel.code === DEFAULT_CHANNEL_CODE : false;
+            })
+            .pipe(shareReplay(1));
+    }
+
+    setActiveChannel(channelId: string): Observable<UserStatusFragment> {
+        return this.dataService.client.setActiveChannel(channelId).pipe(
+            map(({ setActiveChannel }) => setActiveChannel),
+            tap(userStatus => {
+                const activeChannel = userStatus.channels.find(c => c.id === channelId);
+                if (activeChannel) {
+                    this.localStorageService.set('activeChannelToken', activeChannel.token);
+                }
+            }),
+        );
+    }
+}

+ 1 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -73,6 +73,7 @@ export * from './providers/auth/auth.service';
 export * from './providers/bulk-action-registry/bulk-action-registry.service';
 export * from './providers/bulk-action-registry/bulk-action-types';
 export * from './providers/bulk-action-registry/register-bulk-action';
+export * from './providers/channel/channel.service';
 export * from './providers/component-registry/component-registry.service';
 export * from './providers/custom-detail-component/custom-detail-component-types';
 export * from './providers/custom-detail-component/custom-detail-component.service';

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

@@ -19,6 +19,7 @@
         <button
             class="btn btn-primary"
             *ngIf="
+            order.type !== 'Aggregate' &&
                 (order.state === 'ArrangingPayment' || order.state === 'ArrangingAdditionalPayment') &&
                 (hasUnsettledModifications(order) || 0 < outstandingPaymentAmount(order))
             "
@@ -30,6 +31,7 @@
         <button
             class="btn btn-primary"
             *ngIf="
+                order.type !== 'Aggregate' &&
                 order.active === false &&
                 order.state !== 'ArrangingAdditionalPayment' &&
                 order.state !== 'ArrangingPayment' &&
@@ -39,7 +41,7 @@
         >
             {{ 'order.arrange-additional-payment' | translate }}
         </button>
-        <button class="btn btn-primary" (click)="fulfillOrder()" [disabled]="!canAddFulfillment(order)">
+        <button *ngIf="order.type !== 'Aggregate'" class="btn btn-primary" (click)="fulfillOrder()" [disabled]="!canAddFulfillment(order)">
             {{ 'order.fulfill-order' | translate }}
         </button>
         <vdr-dropdown>
@@ -47,7 +49,7 @@
                 <clr-icon shape="ellipsis-vertical"></clr-icon>
             </button>
             <vdr-dropdown-menu vdrPosition="bottom-right">
-                <ng-container *ngIf="order.nextStates.includes('Modifying')">
+                <ng-container *ngIf="order.type !== 'Aggregate' && order.nextStates.includes('Modifying')">
                     <button type="button" class="btn" vdrDropdownItem (click)="transitionToModifying()">
                         <clr-icon shape="pencil"></clr-icon>
                         {{ 'order.modify-order' | translate }}
@@ -58,7 +60,7 @@
                     type="button"
                     class="btn"
                     vdrDropdownItem
-                    *ngIf="order.nextStates.includes('Cancelled')"
+                    *ngIf="order.type !== 'Aggregate' && order.nextStates.includes('Cancelled')"
                     (click)="cancelOrRefund(order)"
                 >
                     <clr-icon shape="error-standard" class="is-error"></clr-icon>
@@ -99,6 +101,10 @@
 <div *ngIf="entity$ | async as order">
     <div class="clr-row">
         <div class="clr-col-lg-8">
+            <vdr-seller-orders-card
+                *ngIf="order.sellerOrders.length"
+                [orderId]="order.id"
+            ></vdr-seller-orders-card>
             <vdr-order-table
                 [order]="order"
                 [orderLineCustomFields]="orderLineCustomFields"

+ 4 - 0
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html

@@ -93,6 +93,7 @@
 >
     <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'order.customer' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'order.order-type' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'order.state' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'order.total' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'common.updated-at' | translate }}</vdr-dt-column>
@@ -104,6 +105,9 @@
         <td class="left align-middle">
             <vdr-customer-label [customer]="order.customer"></vdr-customer-label>
         </td>
+        <td class="left align-middle">
+            <vdr-chip>{{ order.type }}</vdr-chip>
+        </td>
         <td class="left align-middle">
             <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
         </td>

+ 20 - 4
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts

@@ -4,18 +4,21 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
+    ChannelService,
     DataService,
     GetOrderListQuery,
     ItemOf,
     LocalStorageService,
     LogicalOperator,
+    OrderFilterParameter,
     OrderListOptions,
+    OrderType,
     ServerConfigService,
     SortOrder,
 } from '@vendure/admin-ui/core';
 import { Order } from '@vendure/common/lib/generated-types';
 import { merge, Observable } from 'rxjs';
-import { debounceTime, distinctUntilChanged, filter, map, takeUntil } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, filter, map, takeUntil, tap } from 'rxjs/operators';
 
 interface OrderFilterConfig {
     active?: boolean;
@@ -88,11 +91,13 @@ export class OrderListComponent
     ];
     activePreset$: Observable<string>;
     canCreateDraftOrder = false;
+    private activeChannelIsDefaultChannel = false;
 
     constructor(
         private serverConfigService: ServerConfigService,
         private dataService: DataService,
         private localStorageService: LocalStorageService,
+        private channelService: ChannelService,
         router: Router,
         route: ActivatedRoute,
     ) {
@@ -133,8 +138,11 @@ export class OrderListComponent
             filter(value => 2 < value.length || value.length === 0),
             debounceTime(250),
         );
-        merge(searchTerms$, this.route.queryParamMap)
-            .pipe(takeUntil(this.destroy$))
+        const isDefaultChannel$ = this.channelService.defaultChannelIsActive$.pipe(
+            tap(isDefault => (this.activeChannelIsDefaultChannel = isDefault)),
+        );
+        merge(searchTerms$, isDefaultChannel$, this.route.queryParamMap)
+            .pipe(debounceTime(50), takeUntil(this.destroy$))
             .subscribe(val => {
                 this.refresh();
             });
@@ -185,7 +193,7 @@ export class OrderListComponent
     ): { options: OrderListOptions } {
         const filterConfig = this.filterPresets.find(p => p.name === activeFilterPreset);
         // tslint:disable-next-line:no-shadowed-variable
-        let filter: any = {};
+        let filter: OrderFilterParameter = {};
         let filterOperator: LogicalOperator = LogicalOperator.AND;
         if (filterConfig) {
             if (filterConfig.config.active != null) {
@@ -225,6 +233,14 @@ export class OrderListComponent
                 };
             }
         }
+        if (this.activeChannelIsDefaultChannel) {
+            filter = {
+                ...(filter ?? {}),
+                type: {
+                    notEq: OrderType.Seller,
+                },
+            };
+        }
         if (searchTerm) {
             filter = {
                 customerLastName: {

+ 22 - 0
packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.html

@@ -0,0 +1,22 @@
+<div class="card">
+    <div class="card-header payment-header">
+        <div>
+            {{ 'order.seller-orders' | translate }}
+        </div>
+    </div>
+    <div class="card-block" *ngFor="let order of sellerOrders$ | async">
+
+        <vdr-labeled-data [label]="'common.code' | translate">
+            <a  [routerLink]="['seller-orders', order.id]">{{ order.code }}</a>
+        </vdr-labeled-data>
+        <vdr-labeled-data *ngIf="getSeller(order) as seller" [label]="'common.seller' | translate">
+            {{ seller.name }}
+        </vdr-labeled-data>
+        <vdr-labeled-data *ngIf="getSeller(order) as seller" [label]="'order.total' | translate">
+            {{ order.totalWithTax | localeCurrency:order.currencyCode }}
+        </vdr-labeled-data>
+        <vdr-labeled-data [label]="'order.state' | translate">
+            <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
+        </vdr-labeled-data>
+    </div>
+</div>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.scss


+ 49 - 0
packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.component.ts

@@ -0,0 +1,49 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import {
+    ChannelService,
+    DataService,
+    GetSellerOrdersQuery,
+    GetSellerOrdersQueryVariables,
+} from '@vendure/admin-ui/core';
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
+import { Observable } from 'rxjs';
+
+import { GET_SELLER_ORDERS } from './seller-orders-card.graphql';
+
+type SellerOrder = NonNullable<NonNullable<GetSellerOrdersQuery['order']>['sellerOrders']>[number];
+
+@Component({
+    selector: 'vdr-seller-orders-card',
+    templateUrl: './seller-orders-card.component.html',
+    styleUrls: ['./seller-orders-card.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SellerOrdersCardComponent implements OnInit {
+    @Input() orderId: string;
+
+    sellerOrders$: Observable<SellerOrder[]>;
+
+    constructor(
+        private router: Router,
+        private dataService: DataService,
+        private channelService: ChannelService,
+    ) {}
+
+    ngOnInit() {
+        this.sellerOrders$ = this.dataService
+            .query<GetSellerOrdersQuery, GetSellerOrdersQueryVariables>(GET_SELLER_ORDERS, {
+                orderId: this.orderId,
+            })
+            .mapSingle(({ order }) => order?.sellerOrders ?? []);
+    }
+
+    getSeller(order: SellerOrder) {
+        const sellerChannel = order.channels.find(channel => channel.code !== DEFAULT_CHANNEL_CODE);
+        return sellerChannel?.seller;
+    }
+
+    navigateToSellerOrder(order: SellerOrder) {
+        this.router.navigate(['/orders', order.id]);
+    }
+}

+ 25 - 0
packages/admin-ui/src/lib/order/src/components/seller-orders-card/seller-orders-card.graphql.ts

@@ -0,0 +1,25 @@
+import { gql } from 'apollo-angular';
+
+export const GET_SELLER_ORDERS = gql`
+    query GetSellerOrders($orderId: ID!) {
+        order(id: $orderId) {
+            id
+            sellerOrders {
+                id
+                code
+                state
+                orderPlacedAt
+                currencyCode
+                totalWithTax
+                channels {
+                    id
+                    code
+                    seller {
+                        id
+                        name
+                    }
+                }
+            }
+        }
+    }
+`;

+ 2 - 0
packages/admin-ui/src/lib/order/src/order.module.ts

@@ -35,6 +35,7 @@ import { RefundStateLabelComponent } from './components/refund-state-label/refun
 import { SelectAddressDialogComponent } from './components/select-address-dialog/select-address-dialog.component';
 import { SelectCustomerDialogComponent } from './components/select-customer-dialog/select-customer-dialog.component';
 import { SelectShippingMethodDialogComponent } from './components/select-shipping-method-dialog/select-shipping-method-dialog.component';
+import { SellerOrdersCardComponent } from './components/seller-orders-card/seller-orders-card.component';
 import { SettleRefundDialogComponent } from './components/settle-refund-dialog/settle-refund-dialog.component';
 import { SimpleItemListComponent } from './components/simple-item-list/simple-item-list.component';
 import { orderRoutes } from './order.routes';
@@ -77,6 +78,7 @@ import { orderRoutes } from './order.routes';
         CouponCodeSelectorComponent,
         SelectShippingMethodDialogComponent,
         OrderHistoryEntryHostComponent,
+        SellerOrdersCardComponent,
     ],
 })
 export class OrderModule {}

+ 47 - 7
packages/admin-ui/src/lib/order/src/order.routes.ts

@@ -47,6 +47,18 @@ export const orderRoutes: Route[] = [
             breadcrumb: orderBreadcrumb,
         },
     },
+    {
+        path: ':aggregateOrderId/seller-orders/:id',
+        component: OrderDetailComponent,
+        resolve: {
+            entity: OrderResolver,
+        },
+        canActivate: [OrderGuard],
+        canDeactivate: [CanDeactivateDetailGuard],
+        data: {
+            breadcrumb: orderBreadcrumb,
+        },
+    },
     {
         path: ':id/modify',
         component: OrderEditorComponent,
@@ -61,13 +73,41 @@ export const orderRoutes: Route[] = [
 ];
 
 export function orderBreadcrumb(data: any, params: any) {
-    return detailBreadcrumb<OrderDetailFragment>({
-        entity: data.entity,
-        id: params.id,
-        breadcrumbKey: 'breadcrumb.orders',
-        getName: order => order.code,
-        route: '',
-    });
+    return data.entity.pipe(
+        map((entity: OrderDetailFragment) => {
+            if (entity.aggregateOrder) {
+                return [
+                    {
+                        label: 'breadcrumb.orders',
+                        link: ['../'],
+                    },
+                    {
+                        label: entity.aggregateOrder.code,
+                        link: ['../', entity.aggregateOrder.id],
+                    },
+                    {
+                        label: _('breadcrumb.seller-orders'),
+                        link: ['../', entity.aggregateOrder.id],
+                    },
+                    {
+                        label: entity.code,
+                        link: [entity.id],
+                    },
+                ];
+            } else {
+                return [
+                    {
+                        label: 'breadcrumb.orders',
+                        link: ['../'],
+                    },
+                    {
+                        label: entity.code,
+                        link: [entity.id],
+                    },
+                ];
+            }
+        }),
+    );
 }
 
 export function modifyingOrderBreadcrumb(data: any, params: any) {

+ 2 - 0
packages/admin-ui/src/lib/order/src/providers/routing/order-resolver.ts

@@ -10,6 +10,7 @@ import { DataService, OrderDetailFragment } from '@vendure/admin-ui/core';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { EMPTY, Observable } from 'rxjs';
 import { filter, map, shareReplay, switchMap, take, takeUntil } from 'rxjs/operators';
+
 import { DraftOrderDetailComponent } from '../../components/draft-order-detail/draft-order-detail.component';
 
 /**
@@ -32,6 +33,7 @@ export class OrderResolver implements Resolve<Observable<OrderDetailFragment>> {
         const navigateAway$ = this.router.events.pipe(filter(event => event instanceof ActivationStart));
 
         const stream = this.dataService.order
+            // tslint:disable-next-line:no-non-null-assertion
             .getOrder(id!)
             .mapStream(data => data.order)
             .pipe(

+ 2 - 0
packages/admin-ui/src/lib/order/src/public_api.ts

@@ -35,6 +35,8 @@ export * from './components/select-address-dialog/select-address-dialog.componen
 export * from './components/select-address-dialog/select-address-dialog.graphql';
 export * from './components/select-customer-dialog/select-customer-dialog.component';
 export * from './components/select-shipping-method-dialog/select-shipping-method-dialog.component';
+export * from './components/seller-orders-card/seller-orders-card.component';
+export * from './components/seller-orders-card/seller-orders-card.graphql';
 export * from './components/settle-refund-dialog/settle-refund-dialog.component';
 export * from './components/simple-item-list/simple-item-list.component';
 export * from './order.module';

+ 12 - 0
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.html

@@ -122,6 +122,18 @@
         </clr-alert-item>
     </clr-alert>
 
+    <vdr-form-field [label]="'common.seller' | translate" for="sellerId">
+        <select
+            clrSelect
+            name="sellerId"
+            formControlName="sellerId"
+            [vdrDisabled]="!(updatePermission | hasPermission)"
+        >
+            <option selected value style="display: none"></option>
+            <option *ngFor="let seller of sellers$ | async" [value]="seller.id">{{ seller.name }}</option>
+        </select>
+    </vdr-form-field>
+
     <section formGroupName="customFields" *ngIf="customFields.length">
         <label>{{ 'common.custom-fields' | translate }}</label>
         <vdr-tabbed-custom-fields

+ 10 - 2
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -9,6 +9,7 @@ import {
     CurrencyCode,
     CustomFieldConfig,
     DataService,
+    GetSellersQuery,
     GetZonesQuery,
     LanguageCode,
     NotificationService,
@@ -32,6 +33,7 @@ export class ChannelDetailComponent
 {
     customFields: CustomFieldConfig[];
     zones$: Observable<GetZonesQuery['zones']>;
+    sellers$: Observable<GetSellersQuery['sellers']['items']>;
     detailForm: FormGroup;
     currencyCodes = Object.values(CurrencyCode);
     availableLanguageCodes$: Observable<LanguageCode[]>;
@@ -56,6 +58,7 @@ export class ChannelDetailComponent
             defaultShippingZoneId: ['', Validators.required],
             defaultLanguageCode: [],
             defaultTaxZoneId: ['', Validators.required],
+            sellerId: ['', Validators.required],
             customFields: this.formBuilder.group(
                 this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
             ),
@@ -65,6 +68,8 @@ export class ChannelDetailComponent
     ngOnInit() {
         this.init();
         this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
+        // TODO: make this lazy-loaded autocomplete
+        this.sellers$ = this.dataService.settings.getSellers().mapSingle(data => data.sellers.items);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();
     }
 
@@ -90,6 +95,7 @@ export class ChannelDetailComponent
             defaultShippingZoneId: formValue.defaultShippingZoneId,
             defaultTaxZoneId: formValue.defaultTaxZoneId,
             customFields: formValue.customFields,
+            sellerId: formValue.sellerId,
         };
         this.dataService.settings
             .createChannel(input)
@@ -143,6 +149,7 @@ export class ChannelDetailComponent
                         defaultLanguageCode: formValue.defaultLanguageCode,
                         defaultTaxZoneId: formValue.defaultTaxZoneId,
                         customFields: formValue.customFields,
+                        sellerId: formValue.sellerId,
                     } as UpdateChannelInput;
                     return this.dataService.settings.updateChannel(input);
                 }),
@@ -171,9 +178,10 @@ export class ChannelDetailComponent
             token: entity.token || this.generateToken(),
             pricesIncludeTax: entity.pricesIncludeTax,
             currencyCode: entity.currencyCode,
-            defaultShippingZoneId: entity.defaultShippingZone ? entity.defaultShippingZone.id : '',
+            defaultShippingZoneId: entity.defaultShippingZone?.id ?? '',
             defaultLanguageCode: entity.defaultLanguageCode,
-            defaultTaxZoneId: entity.defaultTaxZone ? entity.defaultTaxZone.id : '',
+            defaultTaxZoneId: entity.defaultTaxZone?.id ?? '',
+            sellerId: entity.seller?.id ?? '',
         });
         if (this.customFields.length) {
             this.setCustomFieldFormValues(this.customFields, this.detailForm.get(['customFields']), entity);

+ 52 - 0
packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.html

@@ -0,0 +1,52 @@
+<vdr-action-bar>
+    <vdr-ab-left>
+        <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
+    </vdr-ab-left>
+
+    <vdr-ab-right>
+        <vdr-action-bar-items locationId="seller-detail"></vdr-action-bar-items>
+        <button
+            class="btn btn-primary"
+            *ngIf="isNew$ | async; else updateButton"
+            (click)="create()"
+            [disabled]="!saveButtonEnabled()"
+        >
+            {{ 'common.create' | translate }}
+        </button>
+        <ng-template #updateButton>
+            <button
+                class="btn btn-primary"
+                (click)="save()"
+                *vdrIfPermissions="['SuperAdmin', 'UpdateSeller']"
+                [disabled]="!saveButtonEnabled()"
+            >
+                {{ 'common.update' | translate }}
+            </button>
+        </ng-template>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<form class="form" [formGroup]="detailForm">
+    <vdr-form-field [label]="'common.name' | translate" for="name">
+        <input
+            id="name"
+            type="text"
+            [readonly]="!(updatePermission | hasPermission)"
+            formControlName="name"
+        />
+    </vdr-form-field>
+    <section formGroupName="customFields" *ngIf="customFields.length">
+        <label>{{ 'common.custom-fields' | translate }}</label>
+        <vdr-tabbed-custom-fields
+            entityName="Seller"
+            [customFields]="customFields"
+            [customFieldsFormGroup]="detailForm.get('customFields')"
+            [readonly]="!(updatePermission | hasPermission)"
+        ></vdr-tabbed-custom-fields>
+    </section>
+    <vdr-custom-detail-component-host
+        locationId="seller-detail"
+        [entity$]="entity$"
+        [detailForm]="detailForm"
+    ></vdr-custom-detail-component-host>
+</form>

+ 4 - 0
packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.scss

@@ -0,0 +1,4 @@
+clr-alert {
+    max-width: 30rem;
+    margin-bottom: 12px;
+}

+ 133 - 0
packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.ts

@@ -0,0 +1,133 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import {
+    BaseDetailComponent,
+    CreateSellerInput,
+    CurrencyCode,
+    CustomFieldConfig,
+    DataService,
+    GetZonesQuery,
+    LanguageCode,
+    NotificationService,
+    Permission,
+    SellerFragment,
+    ServerConfigService,
+    UpdateSellerInput,
+} from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { mergeMap, take } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-seller-detail',
+    templateUrl: './seller-detail.component.html',
+    styleUrls: ['./seller-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SellerDetailComponent extends BaseDetailComponent<SellerFragment> implements OnInit, OnDestroy {
+    customFields: CustomFieldConfig[];
+    zones$: Observable<GetZonesQuery['zones']>;
+    detailForm: FormGroup;
+    currencyCodes = Object.values(CurrencyCode);
+    availableLanguageCodes$: Observable<LanguageCode[]>;
+    readonly updatePermission = [Permission.SuperAdmin, Permission.UpdateSeller, Permission.CreateSeller];
+
+    constructor(
+        router: Router,
+        route: ActivatedRoute,
+        protected serverConfigService: ServerConfigService,
+        private changeDetector: ChangeDetectorRef,
+        protected dataService: DataService,
+        private formBuilder: FormBuilder,
+        private notificationService: NotificationService,
+    ) {
+        super(route, router, serverConfigService, dataService);
+        this.customFields = this.getCustomFieldConfig('Seller');
+        this.detailForm = this.formBuilder.group({
+            name: ['', Validators.required],
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
+        });
+    }
+
+    ngOnInit() {
+        this.init();
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+    }
+
+    saveButtonEnabled(): boolean {
+        return this.detailForm.dirty && this.detailForm.valid;
+    }
+
+    create() {
+        if (!this.detailForm.dirty) {
+            return;
+        }
+        const formValue = this.detailForm.value;
+        const input: CreateSellerInput = {
+            name: formValue.name,
+            customFields: formValue.customFields,
+        };
+        this.dataService.settings.createSeller(input).subscribe(data => {
+            switch (data.createSeller.__typename) {
+                case 'Seller':
+                    this.notificationService.success(_('common.notify-create-success'), {
+                        entity: 'Seller',
+                    });
+                    this.detailForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                    this.router.navigate(['../', data.createSeller.id], { relativeTo: this.route });
+                    break;
+            }
+        });
+    }
+
+    save() {
+        if (!this.detailForm.dirty) {
+            return;
+        }
+        const formValue = this.detailForm.value;
+        this.entity$
+            .pipe(
+                take(1),
+                mergeMap(seller => {
+                    const input = {
+                        id: seller.id,
+                        name: formValue.name,
+                        customFields: formValue.customFields,
+                    } as UpdateSellerInput;
+                    return this.dataService.settings.updateSeller(input);
+                }),
+            )
+            .subscribe(({ updateSeller }) => {
+                switch (updateSeller.__typename) {
+                    case 'Seller':
+                        this.notificationService.success(_('common.notify-update-success'), {
+                            entity: 'Seller',
+                        });
+                        this.detailForm.markAsPristine();
+                        this.changeDetector.markForCheck();
+                        break;
+                    // case 'LanguageNotAvailableError':
+                    //     this.notificationService.error(updateSeller.message);
+                }
+            });
+    }
+
+    /**
+     * Update the form values when the entity changes.
+     */
+    protected setFormValues(entity: SellerFragment, languageCode: LanguageCode): void {
+        this.detailForm.patchValue({
+            name: entity.name,
+        });
+        if (this.customFields.length) {
+            this.setCustomFieldFormValues(this.customFields, this.detailForm.get(['customFields']), entity);
+        }
+    }
+}

+ 47 - 0
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html

@@ -0,0 +1,47 @@
+<vdr-action-bar>
+    <vdr-ab-right>
+        <vdr-action-bar-items locationId="seller-list"></vdr-action-bar-items>
+        <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="['SuperAdmin', 'CreateSeller']">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'settings.create-new-seller' | translate }}
+        </a>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<vdr-data-table [items]="sellers$ | async">
+    <vdr-dt-column></vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <ng-template let-seller="item">
+        <td class="left align-middle">
+            {{ seller.name }}
+        </td>
+        <td class="right align-middle">
+            <vdr-table-row-action
+                iconShape="edit"
+                [label]="'common.edit' | translate"
+                [linkTo]="['./', seller.id]"
+            ></vdr-table-row-action>
+        </td>
+        <td class="right align-middle">
+            <vdr-dropdown>
+                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
+                    {{ 'common.actions' | translate }}
+                    <clr-icon shape="caret down"></clr-icon>
+                </button>
+                <vdr-dropdown-menu vdrPosition="bottom-right">
+                    <button
+                        type="button"
+                        class="delete-button"
+                        (click)="deleteSeller(seller.id)"
+                        [disabled]="!(['SuperAdmin', 'DeleteSeller'] | hasPermission)"
+                        vdrDropdownItem
+                    >
+                        <clr-icon shape="trash" class="is-danger"></clr-icon>
+                        {{ 'common.delete' | translate }}
+                    </button>
+                </vdr-dropdown-menu>
+            </vdr-dropdown>
+        </td>
+    </ng-template>
+</vdr-data-table>

+ 0 - 0
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.scss


+ 52 - 0
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts

@@ -0,0 +1,52 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { DataService, ModalService, NotificationService, SellerFragment } from '@vendure/admin-ui/core';
+import { EMPTY, Observable, Subject } from 'rxjs';
+import { mergeMap, startWith, switchMap } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-seller-list',
+    templateUrl: './seller-list.component.html',
+    styleUrls: ['./seller-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SellerListComponent {
+    sellers$: Observable<SellerFragment[]>;
+    private refresh$ = new Subject();
+
+    constructor(
+        private dataService: DataService,
+        private modalService: ModalService,
+        private notificationService: NotificationService,
+    ) {
+        this.sellers$ = this.refresh$.pipe(
+            startWith(1),
+            switchMap(() => this.dataService.settings.getSellers().mapStream(data => data.sellers.items)),
+        );
+    }
+
+    deleteSeller(id: string) {
+        this.modalService
+            .dialog({
+                title: _('catalog.confirm-delete-seller'),
+                buttons: [
+                    { type: 'secondary', label: _('common.cancel') },
+                    { type: 'danger', label: _('common.delete'), returnValue: true },
+                ],
+            })
+            .pipe(switchMap(response => (response ? this.dataService.settings.deleteSeller(id) : EMPTY)))
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-delete-success'), {
+                        entity: 'Seller',
+                    });
+                    this.refresh$.next(1);
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-delete-error'), {
+                        entity: 'Seller',
+                    });
+                },
+            );
+    }
+}

+ 1 - 0
packages/admin-ui/src/lib/settings/src/providers/routing/channel-resolver.ts

@@ -30,6 +30,7 @@ export class ChannelResolver extends BaseEntityResolver<ChannelFragment> {
                 defaultLanguageCode: getDefaultUiLanguage(),
                 defaultShippingZone: {} as any,
                 defaultTaxZone: {} as any,
+                seller: {} as any,
             },
             id => dataService.settings.getChannel(id).mapStream(data => data.channel),
         );

+ 25 - 0
packages/admin-ui/src/lib/settings/src/providers/routing/seller-resolver.ts

@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+import { BaseEntityResolver, DataService, SellerFragment } from '@vendure/admin-ui/core';
+
+/**
+ * Resolves the id from the path into a Customer entity.
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class SellerResolver extends BaseEntityResolver<SellerFragment> {
+    constructor(router: Router, dataService: DataService) {
+        super(
+            router,
+            {
+                __typename: 'Seller',
+                id: '',
+                createdAt: '',
+                updatedAt: '',
+                name: '',
+            },
+            id => dataService.settings.getSeller(id).mapStream(data => data.seller),
+        );
+    }
+}

+ 3 - 0
packages/admin-ui/src/lib/settings/src/public_api.ts

@@ -13,6 +13,8 @@ export * from './components/permission-grid/permission-grid.component';
 export * from './components/profile/profile.component';
 export * from './components/role-detail/role-detail.component';
 export * from './components/role-list/role-list.component';
+export * from './components/seller-detail/seller-detail.component';
+export * from './components/seller-list/seller-list.component';
 export * from './components/shipping-eligibility-test-result/shipping-eligibility-test-result.component';
 export * from './components/shipping-method-detail/shipping-method-detail.component';
 export * from './components/shipping-method-list/shipping-method-list.component';
@@ -35,6 +37,7 @@ export * from './providers/routing/global-settings-resolver';
 export * from './providers/routing/payment-method-resolver';
 export * from './providers/routing/profile-resolver';
 export * from './providers/routing/role-resolver';
+export * from './providers/routing/seller-resolver';
 export * from './providers/routing/shipping-method-resolver';
 export * from './providers/routing/tax-category-resolver';
 export * from './providers/routing/tax-rate-resolver';

+ 4 - 0
packages/admin-ui/src/lib/settings/src/settings.module.ts

@@ -16,6 +16,8 @@ import { PermissionGridComponent } from './components/permission-grid/permission
 import { ProfileComponent } from './components/profile/profile.component';
 import { RoleDetailComponent } from './components/role-detail/role-detail.component';
 import { RoleListComponent } from './components/role-list/role-list.component';
+import { SellerDetailComponent } from './components/seller-detail/seller-detail.component';
+import { SellerListComponent } from './components/seller-list/seller-list.component';
 import { ShippingEligibilityTestResultComponent } from './components/shipping-eligibility-test-result/shipping-eligibility-test-result.component';
 import { ShippingMethodDetailComponent } from './components/shipping-method-detail/shipping-method-detail.component';
 import { ShippingMethodListComponent } from './components/shipping-method-list/shipping-method-list.component';
@@ -56,6 +58,8 @@ import { settingsRoutes } from './settings.routes';
         GlobalSettingsComponent,
         TestOrderBuilderComponent,
         TestAddressFormComponent,
+        SellerDetailComponent,
+        SellerListComponent,
         ShippingMethodTestResultComponent,
         ShippingEligibilityTestResultComponent,
         ZoneListComponent,

+ 28 - 0
packages/admin-ui/src/lib/settings/src/settings.routes.ts

@@ -9,6 +9,7 @@ import {
     detailBreadcrumb,
     PaymentMethodFragment,
     Role,
+    Seller,
     ShippingMethodFragment,
     TaxCategoryFragment,
     TaxRateFragment,
@@ -26,6 +27,8 @@ import { PaymentMethodListComponent } from './components/payment-method-list/pay
 import { ProfileComponent } from './components/profile/profile.component';
 import { RoleDetailComponent } from './components/role-detail/role-detail.component';
 import { RoleListComponent } from './components/role-list/role-list.component';
+import { SellerDetailComponent } from './components/seller-detail/seller-detail.component';
+import { SellerListComponent } from './components/seller-list/seller-list.component';
 import { ShippingMethodDetailComponent } from './components/shipping-method-detail/shipping-method-detail.component';
 import { ShippingMethodListComponent } from './components/shipping-method-list/shipping-method-list.component';
 import { TaxCategoryDetailComponent } from './components/tax-category-detail/tax-category-detail.component';
@@ -40,6 +43,7 @@ import { GlobalSettingsResolver } from './providers/routing/global-settings-reso
 import { PaymentMethodResolver } from './providers/routing/payment-method-resolver';
 import { ProfileResolver } from './providers/routing/profile-resolver';
 import { RoleResolver } from './providers/routing/role-resolver';
+import { SellerResolver } from './providers/routing/seller-resolver';
 import { ShippingMethodResolver } from './providers/routing/shipping-method-resolver';
 import { TaxCategoryResolver } from './providers/routing/tax-category-resolver';
 import { TaxRateResolver } from './providers/routing/tax-rate-resolver';
@@ -82,6 +86,20 @@ export const settingsRoutes: Route[] = [
         canDeactivate: [CanDeactivateDetailGuard],
         data: { breadcrumb: channelBreadcrumb },
     },
+    {
+        path: 'sellers',
+        component: SellerListComponent,
+        data: {
+            breadcrumb: _('breadcrumb.sellers'),
+        },
+    },
+    {
+        path: 'sellers/:id',
+        component: SellerDetailComponent,
+        resolve: createResolveData(SellerResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
+        data: { breadcrumb: sellerBreadcrumb },
+    },
     {
         path: 'roles',
         component: RoleListComponent,
@@ -214,6 +232,16 @@ export function channelBreadcrumb(data: any, params: any) {
     });
 }
 
+export function sellerBreadcrumb(data: any, params: any) {
+    return detailBreadcrumb<Seller>({
+        entity: data.entity,
+        id: params.id,
+        breadcrumbKey: 'breadcrumb.sellers',
+        getName: seller => seller.name,
+        route: 'sellers',
+    });
+}
+
 export function roleBreadcrumb(data: any, params: any) {
     return detailBreadcrumb<Role>({
         entity: data.entity,

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -45,6 +45,8 @@
     "profile": "Profil",
     "promotions": "Propagace",
     "roles": "Role",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Dopravní metody",
     "system-status": "Status systému",
     "tax-categories": "Daňové kategorie",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Smazat variantu produktu?",
     "confirm-delete-promotion": "Smazat propagaci?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Smazat dopravní metodu?",
     "confirm-delete-zone": "Smazat zónu?",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Odhlásit",
     "login": "Přihlásit",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "",
     "manage-tags-description": "",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Vybrat dnešní datum",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "",
@@ -456,6 +462,7 @@
     "promotions": "Propagace",
     "roles": "Role",
     "sales": "Prodeje",
+    "sellers": "",
     "settings": "Nastavení",
     "shipping-methods": "Dopravní metody",
     "system": "Systém",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Pro adminy i zákazníka",
     "order-history": "Historie objednávky",
     "order-state-diagram": "Přehled stavu objednávky",
+    "order-type": "",
     "payment": "Platba",
     "payment-amount": "Částka platby",
     "payment-metadata": "Data platby",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Vyberte stav",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Vytvořit zemi",
     "create-new-payment-method": "",
     "create-new-role": "Vytvořit roli",
+    "create-new-seller": "",
     "create-new-shipping-method": "Vytvořit dodací metodu",
     "create-new-tax-category": "Vytvořit daňovou kategorii",
     "create-new-tax-rate": "Vytvořit daňovou sazbu",

+ 11 - 3
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -45,6 +45,8 @@
     "profile": "Profil",
     "promotions": "Promotionen",
     "roles": "Rollen",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Versandarten",
     "system-status": "Systemstatus",
     "tax-categories": "Steuerkategorien",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Produktvariante löschen?",
     "confirm-delete-promotion": "Werbeaktion löschen?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Versandart löschen?",
     "confirm-delete-zone": "Zone löschen?",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Abmelden",
     "login": "Anmelden",
+    "login-image-title": "Hallo! Willkommen zurück. Schön, dich zu sehen.",
+    "login-title": "Anmelden bei Vendure",
     "manage-tags": "Tags verwalten",
     "manage-tags-description": "Tagbeschreibungen verwalten",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Heute auswählen",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Tags",
@@ -289,9 +295,7 @@
     "username": "Benutzername",
     "view-next-month": "Nächsten Monat anzeigen",
     "view-previous-month": "Vorherigen Monat anzeigen",
-    "with-selected": "Auswahl...",
-    "login-title": "Anmelden bei Vendure",
-    "login-image-title": "Hallo! Willkommen zurück. Schön, dich zu sehen."
+    "with-selected": "Auswahl..."
   },
   "customer": {
     "add-customer-to-group": "Kunde zu Gruppe hinzufügen",
@@ -458,6 +462,7 @@
     "promotions": "Werbeaktionen",
     "roles": "Rollen",
     "sales": "Verkäufe",
+    "sellers": "",
     "settings": "Einstellungen",
     "shipping-methods": "Versandarten",
     "system": "System",
@@ -552,6 +557,7 @@
     "note-visible-to-customer": "Sichtbar für Administratoren und Kunden",
     "order-history": "Bestellhistorie",
     "order-state-diagram": "",
+    "order-type": "",
     "payment": "Zahlung",
     "payment-amount": "Zahlungsbetrag",
     "payment-metadata": "Metadaten zur Bezahlung",
@@ -589,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Status auswählen",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -638,6 +645,7 @@
     "create-new-country": "Neues Land erstellen",
     "create-new-payment-method": "Neue Zahlungsmethode erstellen",
     "create-new-role": "Neue Rolle erstellen",
+    "create-new-seller": "",
     "create-new-shipping-method": "Neue Versandart erstellen",
     "create-new-tax-category": "Neue Steuerkategorie erstellen",
     "create-new-tax-rate": "Neuen Steuersatz erstellen",

+ 11 - 3
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -45,6 +45,8 @@
     "profile": "Profile",
     "promotions": "Promotions",
     "roles": "Roles",
+    "seller-orders": "Seller orders",
+    "sellers": "Sellers",
     "shipping-methods": "Shipping methods",
     "system-status": "System status",
     "tax-categories": "Tax categories",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "Delete product option group \"{name}\"?",
     "confirm-delete-product-variant": "Delete product variant \"{name}\"?",
     "confirm-delete-promotion": "Delete promotion?",
+    "confirm-delete-seller": "Delete seller?",
     "confirm-delete-shipping-method": "Delete shipping method?",
     "confirm-delete-zone": "Delete zone?",
     "confirm-deletion-of-unused-variants-body": "The following product variants have been made obsolete due to the addition of new options. They will be deleted during the creation of the new product variants.",
@@ -238,6 +241,8 @@
     "locale": "Locale",
     "log-out": "Log out",
     "login": "Log in",
+    "login-image-title": "Hi! Welcome back. Good to see you.",
+    "login-title": "Log in to Vendure",
     "manage-tags": "Manage tags",
     "manage-tags-description": "Update or delete tags globally.",
     "medium-date": "Medium date",
@@ -277,6 +282,7 @@
     "select-relation-id": "Select relation ID",
     "select-today": "Select today",
     "select-variants": "Select variants",
+    "seller": "Seller",
     "set-language": "Set language",
     "short-date": "Short date",
     "tags": "Tags",
@@ -289,9 +295,7 @@
     "username": "Username",
     "view-next-month": "View next month",
     "view-previous-month": "View previous month",
-    "with-selected": "With {count} selected...",
-    "login-title": "Log in to Vendure",
-    "login-image-title": "Hi! Welcome back. Good to see you."
+    "with-selected": "With {count} selected..."
   },
   "customer": {
     "add-customer-to-group": "Add customer to group",
@@ -458,6 +462,7 @@
     "promotions": "Promotions",
     "roles": "Roles",
     "sales": "Sales",
+    "sellers": "Sellers",
     "settings": "Settings",
     "shipping-methods": "Shipping methods",
     "system": "System",
@@ -552,6 +557,7 @@
     "note-visible-to-customer": "Visible to admins and customer",
     "order-history": "Order history",
     "order-state-diagram": "Order state diagram",
+    "order-type": "Order type",
     "payment": "Payment",
     "payment-amount": "Payment amount",
     "payment-metadata": "Payment metadata",
@@ -589,6 +595,7 @@
     "select-address": "Select address",
     "select-shipping-method": "Select shipping method",
     "select-state": "Select state",
+    "seller-orders": "Seller orders",
     "set-billing-address": "Set billing address",
     "set-coupon-codes": "Set coupon codes",
     "set-customer-for-order": "Set customer",
@@ -638,6 +645,7 @@
     "create-new-country": "Create new country",
     "create-new-payment-method": "Create new payment method",
     "create-new-role": "Create new role",
+    "create-new-seller": "Create new seller",
     "create-new-shipping-method": "Create new shipping method",
     "create-new-tax-category": "Create tax category",
     "create-new-tax-rate": "Create new tax rate",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -45,6 +45,8 @@
     "profile": "Perfil",
     "promotions": "Promociones",
     "roles": "Roles",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Métodos de envío",
     "system-status": "Estado del sistema",
     "tax-categories": "Categorías de impuestos",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "¿Eliminar variante?",
     "confirm-delete-promotion": "¿Eliminar promoción?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "¿Eliminar método de envío?",
     "confirm-delete-zone": "¿Eliminar zona?",
     "confirm-deletion-of-unused-variants-body": "Las siguientes variantes de producto han quedado obsoletas debido a la incorporación de nuevas opciones. Se eliminarán durante la creación de las nuevas variantes de producto.",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Salir",
     "login": "Entrar",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Gestionar facetas",
     "manage-tags-description": "Actualiza o elimina facetas globalmente.",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Hoy",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Etiquetas",
@@ -456,6 +462,7 @@
     "promotions": "Promociones",
     "roles": "Roles",
     "sales": "Ventas",
+    "sellers": "",
     "settings": "Ajustes",
     "shipping-methods": "Envíos",
     "system": "Sistema",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Visible para administradores y clientes",
     "order-history": "Historial de pedidos",
     "order-state-diagram": "Diagrama de estado de los pedidos",
+    "order-type": "",
     "payment": "Pago",
     "payment-amount": "Importe del pago",
     "payment-metadata": "Metadatos de pago",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Seleccionar estado",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Crear nuevo país",
     "create-new-payment-method": "Crear nuevo método de pago",
     "create-new-role": "Crear nuevo rol",
+    "create-new-seller": "",
     "create-new-shipping-method": "Crear nuevo método de envío",
     "create-new-tax-category": "Crear categoría de impuestos",
     "create-new-tax-rate": "Crear nueva tasa de impuestos",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -45,6 +45,8 @@
     "profile": "Profil",
     "promotions": "Promotions",
     "roles": "Roles",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Modes d'expédition",
     "system-status": "Statut du système",
     "tax-categories": "Catégories des taxes",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Supprimer variation du produit ?",
     "confirm-delete-promotion": "Supprimer promotion ?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Supprimer mode d'expédition ?",
     "confirm-delete-zone": "Supprimer zone ?",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Déconnexion",
     "login": "Connexion",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Gérer les mot-clés",
     "manage-tags-description": "Mettre à jour ou supprimer les mots-clés de façon globale",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Choisir aujourd'hui",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Mots-clés",
@@ -456,6 +462,7 @@
     "promotions": "Promotions",
     "roles": "Roles",
     "sales": "Ventes",
+    "sellers": "",
     "settings": "Paramètres",
     "shipping-methods": "Mode d'expédition",
     "system": "Système",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Visible par les admins et le client",
     "order-history": "Historique de la commande",
     "order-state-diagram": "Diagramme des états de la commande",
+    "order-type": "",
     "payment": "paiement",
     "payment-amount": "Montant à payer",
     "payment-metadata": "Métadonnées de paiement",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Sélectionner un état",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Créer nouveau pays",
     "create-new-payment-method": "Créer une nouvelle méthode de paiement",
     "create-new-role": "Créer nouveau role",
+    "create-new-seller": "",
     "create-new-shipping-method": "Créer nouveau mode d'expédition",
     "create-new-tax-category": "Créer catégorie de taxe",
     "create-new-tax-rate": "Créer nouveau taux de taxe",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -45,6 +45,8 @@
     "profile": "Profilo",
     "promotions": "Promozioni",
     "roles": "Ruoli",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Metodi di spedizione",
     "system-status": "Stato del sistema",
     "tax-categories": "Impostazioni Tasse",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Eliminare la variante?",
     "confirm-delete-promotion": "Eliminare la promozione?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Eliminare il metodo di spedizione?",
     "confirm-delete-zone": "Eliminare la zona?",
     "confirm-deletion-of-unused-variants-body": "Le seguenti varianti sono diventate obsolete a seguito dell'aggiunta di nuove opzioni. Queste verranno cancellate durante la creazione delle nuove varianti.",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Log out",
     "login": "Log in",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Gestisci tag",
     "manage-tags-description": "Aggiungi o elimina tag globalmente",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Seleziona oggi",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Tag",
@@ -456,6 +462,7 @@
     "promotions": "Promozioni",
     "roles": "Ruoli",
     "sales": "Vendite",
+    "sellers": "",
     "settings": "Impostazioni",
     "shipping-methods": "Metodi di Spedizione",
     "system": "Sistema",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Visibile agli amministratori e al cliente",
     "order-history": "Storico ordine",
     "order-state-diagram": "Diagramma di stato dell'ordine",
+    "order-type": "",
     "payment": "Pagamento",
     "payment-amount": "Importo pagamento",
     "payment-metadata": "Metadati pagamento",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Seleziona stato",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Crea nuova nazione",
     "create-new-payment-method": "Crea nuovo metodo di pagamento",
     "create-new-role": "Crea nuovo ruolo",
+    "create-new-seller": "",
     "create-new-shipping-method": "Crea nuovo metodo di spedizione",
     "create-new-tax-category": "Crea categoria tassa",
     "create-new-tax-rate": "Crea nuovo tasso fiscale",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -45,6 +45,8 @@
     "profile": "",
     "promotions": "Promocje",
     "roles": "Role",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Metody wysyłki",
     "system-status": "",
     "tax-categories": "Kategorie podatkowe",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Usunąć wariant produktu?",
     "confirm-delete-promotion": "Usunąć promocje?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Usunąć metode wysyłki?",
     "confirm-delete-zone": "",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Wyloguj",
     "login": "Zaloguj",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "",
     "manage-tags-description": "",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Wybierz dzisiaj",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "",
@@ -456,6 +462,7 @@
     "promotions": "Promocje",
     "roles": "Role",
     "sales": "Sprzedaż",
+    "sellers": "",
     "settings": "Ustawienia",
     "shipping-methods": "Metody wysyłki",
     "system": "",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Widoczne dla administratora i klienta",
     "order-history": "Historia zamówienia",
     "order-state-diagram": "",
+    "order-type": "",
     "payment": "Płatność",
     "payment-amount": "Wartość płatności",
     "payment-metadata": "Metadane płatności",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Utwórz nowy kraj",
     "create-new-payment-method": "",
     "create-new-role": "Utwórz nową role",
+    "create-new-seller": "",
     "create-new-shipping-method": "Utwórz nową metode wysyłki",
     "create-new-tax-category": "Utwórz nową kategorię podatkową",
     "create-new-tax-rate": "Utwórz nową stawkę podatkową",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -45,6 +45,8 @@
     "profile": "Perfil",
     "promotions": "Promoções",
     "roles": "Regras",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Métodos de envio",
     "system-status": "Status do sistema",
     "tax-categories": "Categorias de taxas e impostos",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Excluir variação de produto?",
     "confirm-delete-promotion": "Excluir promoção?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Excluir método de envio?",
     "confirm-delete-zone": "Excluir zona?",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Sair",
     "login": "Entrar",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "",
     "manage-tags-description": "",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Selecione hoje",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "",
@@ -456,6 +462,7 @@
     "promotions": "Promoções",
     "roles": "Regras de permissão",
     "sales": "Vendas",
+    "sellers": "",
     "settings": "Configurações",
     "shipping-methods": "Métodos de envio",
     "system": "Sistema",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Visível para administradores e clientes",
     "order-history": "Histórico de pedidos",
     "order-state-diagram": "Diagrama do estado do pedido",
+    "order-type": "",
     "payment": "Pagamento",
     "payment-amount": "Valor do pagamento",
     "payment-metadata": "Dados do pagamento",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Selecionar estado",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Criar novo país",
     "create-new-payment-method": "",
     "create-new-role": "Criar nova regra",
+    "create-new-seller": "",
     "create-new-shipping-method": "Criar novo método de envio",
     "create-new-tax-category": "Criar categoria de imposto",
     "create-new-tax-rate": "Criar nova taxa de imposto",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -45,6 +45,8 @@
     "profile": "Perfil",
     "promotions": "Promoções",
     "roles": "Regras",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Métodos de envio",
     "system-status": "Estado do sistema",
     "tax-categories": "Categorias de taxas e impostos",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Eliminar variante do produto?",
     "confirm-delete-promotion": "Eliminar promoção?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Eliminar método de envio?",
     "confirm-delete-zone": "Eliminar região?",
     "confirm-deletion-of-unused-variants-body": "As variantes listadas abaixo estão obsoletas e serão eliminadas devido à adição de novas opções.",
@@ -238,6 +241,8 @@
     "locale": "Localidade",
     "log-out": "Sair",
     "login": "Entrar",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Gerir tags",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
     "medium-date": "Data média",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Seleccione a data de hoje",
     "select-variants": "",
+    "seller": "",
     "set-language": "Definir idioma",
     "short-date": "Data abreviada",
     "tags": "Tags",
@@ -456,6 +462,7 @@
     "promotions": "Promoções",
     "roles": "Gerir permissões",
     "sales": "Vendas",
+    "sellers": "",
     "settings": "Configurações",
     "shipping-methods": "Métodos de envio",
     "system": "Sistema",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Visível para administradores e clientes",
     "order-history": "Histórico de encomendas",
     "order-state-diagram": "Diagrama do estado da encomenda",
+    "order-type": "",
     "payment": "Pagamento",
     "payment-amount": "Valor do pagamento",
     "payment-metadata": "Dados do pagamento",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Seleccionar estado",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Criar novo país",
     "create-new-payment-method": "Criar novo método de pagamento",
     "create-new-role": "Criar nova regra",
+    "create-new-seller": "",
     "create-new-shipping-method": "Criar novo método de envio",
     "create-new-tax-category": "Criar categoria de imposto",
     "create-new-tax-rate": "Criar nova taxa de imposto",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -45,6 +45,8 @@
     "profile": "Профиль",
     "promotions": "Акции",
     "roles": "Роли",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Способы доставки",
     "system-status": "Состояние системы",
     "tax-categories": "Категории налогов",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Удалить вариант товара?",
     "confirm-delete-promotion": "Удалить промо-акцию?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Удалить способ доставки?",
     "confirm-delete-zone": "Удалить зону?",
     "confirm-deletion-of-unused-variants-body": "Следующие варианты товаров устарели из за добавления новых опций. Они будут удалены во время создания новых вариантов товара.",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Выйти",
     "login": "Войти",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Управление тегами",
     "manage-tags-description": "Обновление или удаление тегов глобально.",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Выберите сегодня",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Теги",
@@ -456,6 +462,7 @@
     "promotions": "Промо-акции",
     "roles": "Роли",
     "sales": "Продажи",
+    "sellers": "",
     "settings": "Настройки",
     "shipping-methods": "Способы доставки",
     "system": "Система",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Доступно администраторам и клиентам",
     "order-history": "История заказов",
     "order-state-diagram": "Диаграмма состояния заказа",
+    "order-type": "",
     "payment": "Оплата",
     "payment-amount": "Сумма к оплате",
     "payment-metadata": "Метаданные платежа",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Выберите состояние",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Создать новую страну",
     "create-new-payment-method": "Создать новый способ оплаты",
     "create-new-role": "Создать новую роль",
+    "create-new-seller": "",
     "create-new-shipping-method": "Создать новый способ доставки",
     "create-new-tax-category": "Создать новую налоговую категорию",
     "create-new-tax-rate": "Создать новую налоговую ставку",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -45,6 +45,8 @@
     "profile": "Профіль",
     "promotions": "Акції",
     "roles": "Ролі",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "Способи доставки",
     "system-status": "Стан системи",
     "tax-categories": "Категорії податків",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "Видалити варіант товару?",
     "confirm-delete-promotion": "Видалити промо-акцію?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "Видалити спосіб доставки?",
     "confirm-delete-zone": "Видалити зону?",
     "confirm-deletion-of-unused-variants-body": "Наступні варіанти товару застаріли через додавання нових опцій. Вони будуть видалені під час створення нових варіантів товару.",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "Вийти",
     "login": "Увійти",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "Керування тегами",
     "manage-tags-description": "Оновлення або видалення тегів глобально.",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "Виберіть сьогодні",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "Теги",
@@ -456,6 +462,7 @@
     "promotions": "Промо-акції",
     "roles": "Ролі",
     "sales": "Продажі",
+    "sellers": "",
     "settings": "Налаштування",
     "shipping-methods": "Способи доставки",
     "system": "Система",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "Доступно адміністраторам і клієнтам",
     "order-history": "Історія замовлень",
     "order-state-diagram": "Діаграма стану замовлення",
+    "order-type": "",
     "payment": "Оплата",
     "payment-amount": "Сума до оплати",
     "payment-metadata": "Метадані платежу",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "Виберіть стан",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "Створити нову країну",
     "create-new-payment-method": "Створити новий спосіб оплати",
     "create-new-role": "Створити нову роль",
+    "create-new-seller": "",
     "create-new-shipping-method": "Створити новий спосіб доставки",
     "create-new-tax-category": "Створити нову податкову категорію",
     "create-new-tax-rate": "Створити нову податкову ставку",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -45,6 +45,8 @@
     "profile": "个人资料",
     "promotions": "优惠券管理",
     "roles": "角色管理",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "配送方式管理",
     "system-status": "系统状态",
     "tax-categories": "税表分类",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "确认删除商品规格?",
     "confirm-delete-promotion": "确认删除优惠券?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "确认删除邮寄方式?",
     "confirm-delete-zone": "确认删除分区么?",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "退出",
     "login": "登陆",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "管理标签",
     "manage-tags-description": "更新或删除标签",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "选择今天",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "标签",
@@ -456,6 +462,7 @@
     "promotions": "优惠券管理",
     "roles": "角色管理",
     "sales": "销售管理",
+    "sellers": "",
     "settings": "系统设置",
     "shipping-methods": "配送方式",
     "system": "系统状态",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "管理员及客户可见",
     "order-history": "历史订单",
     "order-state-diagram": "",
+    "order-type": "",
     "payment": "付款信息",
     "payment-amount": "付款金额",
     "payment-metadata": "付款元数据",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "添加国家",
     "create-new-payment-method": "",
     "create-new-role": "添加角色",
+    "create-new-seller": "",
     "create-new-shipping-method": "添加配送方式",
     "create-new-tax-category": "创建税表分类",
     "create-new-tax-rate": "添加税率",

+ 10 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -45,6 +45,8 @@
     "profile": "",
     "promotions": "優惠",
     "roles": "角色管理",
+    "seller-orders": "",
+    "sellers": "",
     "shipping-methods": "配送方式管理",
     "system-status": "",
     "tax-categories": "稅項類別",
@@ -86,6 +88,7 @@
     "confirm-delete-product-option-group": "",
     "confirm-delete-product-variant": "確認移除商品規格?",
     "confirm-delete-promotion": "確認移除優惠券?",
+    "confirm-delete-seller": "",
     "confirm-delete-shipping-method": "確認移除此郵寄方式?",
     "confirm-delete-zone": "",
     "confirm-deletion-of-unused-variants-body": "",
@@ -238,6 +241,8 @@
     "locale": "",
     "log-out": "退出",
     "login": "登陆",
+    "login-image-title": "",
+    "login-title": "",
     "manage-tags": "",
     "manage-tags-description": "",
     "medium-date": "",
@@ -277,6 +282,7 @@
     "select-relation-id": "",
     "select-today": "選擇今天",
     "select-variants": "",
+    "seller": "",
     "set-language": "",
     "short-date": "",
     "tags": "",
@@ -456,6 +462,7 @@
     "promotions": "優惠券管理",
     "roles": "角色管理",
     "sales": "銷售管理",
+    "sellers": "",
     "settings": "系统設定",
     "shipping-methods": "配送方式",
     "system": "",
@@ -550,6 +557,7 @@
     "note-visible-to-customer": "管理員及客户可瀏覽",
     "order-history": "訂單",
     "order-state-diagram": "",
+    "order-type": "",
     "payment": "付款信息",
     "payment-amount": "付款金額",
     "payment-metadata": "付款元數據",
@@ -587,6 +595,7 @@
     "select-address": "",
     "select-shipping-method": "",
     "select-state": "",
+    "seller-orders": "",
     "set-billing-address": "",
     "set-coupon-codes": "",
     "set-customer-for-order": "",
@@ -636,6 +645,7 @@
     "create-new-country": "新增國家",
     "create-new-payment-method": "",
     "create-new-role": "新增角色",
+    "create-new-seller": "",
     "create-new-shipping-method": "新增配送方式",
     "create-new-tax-category": "建立税表分類",
     "create-new-tax-rate": "新增税率",

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 537 - 504
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 645 - 627
packages/common/src/generated-shop-types.ts


+ 210 - 0
packages/common/src/generated-types.ts

@@ -362,6 +362,7 @@ export type Channel = Node & {
   defaultTaxZone?: Maybe<Zone>;
   id: Scalars['ID'];
   pricesIncludeTax: Scalars['Boolean'];
+  seller?: Maybe<Seller>;
   token: Scalars['String'];
   updatedAt: Scalars['DateTime'];
 };
@@ -660,6 +661,7 @@ export type CreateChannelInput = {
   defaultShippingZoneId: Scalars['ID'];
   defaultTaxZoneId: Scalars['ID'];
   pricesIncludeTax: Scalars['Boolean'];
+  sellerId?: InputMaybe<Scalars['ID']>;
   token: Scalars['String'];
 };
 
@@ -818,6 +820,15 @@ export type CreateRoleInput = {
   permissions: Array<Permission>;
 };
 
+export type CreateSellerCustomFieldsInput = {
+  connectedAccountId?: InputMaybe<Scalars['String']>;
+};
+
+export type CreateSellerInput = {
+  customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+  name: Scalars['String'];
+};
+
 export type CreateShippingMethodInput = {
   calculator: ConfigurableOperationInput;
   checker: ConfigurableOperationInput;
@@ -847,6 +858,10 @@ export type CreateTaxRateInput = {
   zoneId: Scalars['ID'];
 };
 
+export type CreateVendorInput = {
+  name: Scalars['String'];
+};
+
 export type CreateZoneInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   memberIds?: InputMaybe<Array<Scalars['ID']>>;
@@ -1227,6 +1242,7 @@ export type CustomFields = {
   ProductOptionGroup: Array<CustomFieldConfig>;
   ProductVariant: Array<CustomFieldConfig>;
   Promotion: Array<CustomFieldConfig>;
+  Seller: Array<CustomFieldConfig>;
   ShippingMethod: Array<CustomFieldConfig>;
   TaxCategory: Array<CustomFieldConfig>;
   TaxRate: Array<CustomFieldConfig>;
@@ -2478,6 +2494,8 @@ export type Mutation = {
   createPromotion: CreatePromotionResult;
   /** Create a new Role */
   createRole: Role;
+  /** Create a new Seller */
+  createSeller: Seller;
   /** Create a new ShippingMethod */
   createShippingMethod: ShippingMethod;
   /** Create a new Tag */
@@ -2486,6 +2504,8 @@ export type Mutation = {
   createTaxCategory: TaxCategory;
   /** Create a new TaxRate */
   createTaxRate: TaxRate;
+  /** Create a new Vendor */
+  createVendor: Vendor;
   /** Create a new Zone */
   createZone: Zone;
   /** Delete an Administrator */
@@ -2533,6 +2553,8 @@ export type Mutation = {
   deletePromotion: DeletionResponse;
   /** Delete an existing Role */
   deleteRole: DeletionResponse;
+  /** Delete a Seller */
+  deleteSeller: DeletionResponse;
   /** Delete a ShippingMethod */
   deleteShippingMethod: DeletionResponse;
   /** Delete an existing Tag */
@@ -2541,6 +2563,8 @@ export type Mutation = {
   deleteTaxCategory: DeletionResponse;
   /** Delete a TaxRate */
   deleteTaxRate: DeletionResponse;
+  /** Delete a Vendor */
+  deleteVendor: DeletionResponse;
   /** Delete a Zone */
   deleteZone: DeletionResponse;
   flushBufferedJobs: Success;
@@ -2635,6 +2659,8 @@ export type Mutation = {
   updatePromotion: UpdatePromotionResult;
   /** Update an existing Role */
   updateRole: Role;
+  /** Update an existing Seller */
+  updateSeller: Seller;
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod;
   /** Update an existing Tag */
@@ -2643,6 +2669,8 @@ export type Mutation = {
   updateTaxCategory: TaxCategory;
   /** Update an existing TaxRate */
   updateTaxRate: TaxRate;
+  /** Update an existing Vendor */
+  updateVendor: Vendor;
   /** Update an existing Zone */
   updateZone: Zone;
 };
@@ -2848,6 +2876,11 @@ export type MutationCreateRoleArgs = {
 };
 
 
+export type MutationCreateSellerArgs = {
+  input: CreateSellerInput;
+};
+
+
 export type MutationCreateShippingMethodArgs = {
   input: CreateShippingMethodInput;
 };
@@ -2868,6 +2901,11 @@ export type MutationCreateTaxRateArgs = {
 };
 
 
+export type MutationCreateVendorArgs = {
+  input: CreateVendorInput;
+};
+
+
 export type MutationCreateZoneArgs = {
   input: CreateZoneInput;
 };
@@ -2997,6 +3035,11 @@ export type MutationDeleteRoleArgs = {
 };
 
 
+export type MutationDeleteSellerArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationDeleteShippingMethodArgs = {
   id: Scalars['ID'];
 };
@@ -3017,6 +3060,11 @@ export type MutationDeleteTaxRateArgs = {
 };
 
 
+export type MutationDeleteVendorArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationDeleteZoneArgs = {
   id: Scalars['ID'];
 };
@@ -3289,6 +3337,11 @@ export type MutationUpdateRoleArgs = {
 };
 
 
+export type MutationUpdateSellerArgs = {
+  input: UpdateSellerInput;
+};
+
+
 export type MutationUpdateShippingMethodArgs = {
   input: UpdateShippingMethodInput;
 };
@@ -3309,6 +3362,11 @@ export type MutationUpdateTaxRateArgs = {
 };
 
 
+export type MutationUpdateVendorArgs = {
+  input: UpdateVendorInput;
+};
+
+
 export type MutationUpdateZoneArgs = {
   input: UpdateZoneInput;
 };
@@ -3387,7 +3445,10 @@ export type Order = Node & {
   __typename?: 'Order';
   /** An order is active as long as the payment process has not been completed */
   active: Scalars['Boolean'];
+  aggregateOrder?: Maybe<Order>;
+  aggregateOrderId?: Maybe<Scalars['ID']>;
   billingAddress?: Maybe<OrderAddress>;
+  channels: Array<Channel>;
   /** A unique code for the Order */
   code: Scalars['String'];
   /** An array of all coupon codes applied to the Order */
@@ -3411,6 +3472,7 @@ export type Order = Node & {
   payments?: Maybe<Array<Payment>>;
   /** Promotions applied to the order. Only gets populated after the payment process has completed. */
   promotions: Array<Promotion>;
+  sellerOrders?: Maybe<Array<Order>>;
   shipping: Scalars['Int'];
   shippingAddress?: Maybe<OrderAddress>;
   shippingLines: Array<ShippingLine>;
@@ -3439,6 +3501,7 @@ export type Order = Node & {
   totalQuantity: Scalars['Int'];
   /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
   totalWithTax: Scalars['Int'];
+  type: OrderType;
   updatedAt: Scalars['DateTime'];
 };
 
@@ -3464,6 +3527,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
   active?: InputMaybe<BooleanOperators>;
+  aggregateOrderId?: InputMaybe<IdOperators>;
   code?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
   currencyCode?: InputMaybe<StringOperators>;
@@ -3479,6 +3543,7 @@ export type OrderFilterParameter = {
   totalQuantity?: InputMaybe<NumberOperators>;
   totalWithTax?: InputMaybe<NumberOperators>;
   transactionId?: InputMaybe<StringOperators>;
+  type?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
 };
 
@@ -3648,6 +3713,7 @@ export type OrderProcessState = {
 };
 
 export type OrderSortParameter = {
+  aggregateOrderId?: InputMaybe<SortOrder>;
   code?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   customerLastName?: InputMaybe<SortOrder>;
@@ -3691,6 +3757,12 @@ export type OrderTaxSummary = {
   taxTotal: Scalars['Int'];
 };
 
+export enum OrderType {
+  Aggregate = 'Aggregate',
+  Regular = 'Regular',
+  Seller = 'Seller'
+}
+
 export type PaginatedList = {
   items: Array<Node>;
   totalItems: Scalars['Int'];
@@ -3862,6 +3934,8 @@ export enum Permission {
   CreateProduct = 'CreateProduct',
   /** Grants permission to create Promotion */
   CreatePromotion = 'CreatePromotion',
+  /** Grants permission to create Seller */
+  CreateSeller = 'CreateSeller',
   /** Grants permission to create PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   CreateSettings = 'CreateSettings',
   /** Grants permission to create ShippingMethod */
@@ -3902,6 +3976,8 @@ export enum Permission {
   DeleteProduct = 'DeleteProduct',
   /** Grants permission to delete Promotion */
   DeletePromotion = 'DeletePromotion',
+  /** Grants permission to delete Seller */
+  DeleteSeller = 'DeleteSeller',
   /** Grants permission to delete PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   DeleteSettings = 'DeleteSettings',
   /** Grants permission to delete ShippingMethod */
@@ -3946,6 +4022,8 @@ export enum Permission {
   ReadProduct = 'ReadProduct',
   /** Grants permission to read Promotion */
   ReadPromotion = 'ReadPromotion',
+  /** Grants permission to read Seller */
+  ReadSeller = 'ReadSeller',
   /** Grants permission to read PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   ReadSettings = 'ReadSettings',
   /** Grants permission to read ShippingMethod */
@@ -3990,6 +4068,8 @@ export enum Permission {
   UpdateProduct = 'UpdateProduct',
   /** Grants permission to update Promotion */
   UpdatePromotion = 'UpdatePromotion',
+  /** Grants permission to update Seller */
+  UpdateSeller = 'UpdateSeller',
   /** Grants permission to update PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   UpdateSettings = 'UpdateSettings',
   /** Grants permission to update ShippingMethod */
@@ -4414,6 +4494,8 @@ export type Query = {
   role?: Maybe<Role>;
   roles: RoleList;
   search: SearchResponse;
+  seller?: Maybe<Seller>;
+  sellers: SellerList;
   shippingCalculators: Array<ConfigurableOperationDefinition>;
   shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
   shippingMethod?: Maybe<ShippingMethod>;
@@ -4426,6 +4508,8 @@ export type Query = {
   taxRates: TaxRateList;
   testEligibleShippingMethods: Array<ShippingMethodQuote>;
   testShippingMethod: TestShippingMethodResult;
+  vendor?: Maybe<Vendor>;
+  vendors: VendorList;
   zone?: Maybe<Zone>;
   zones: Array<Zone>;
 };
@@ -4620,6 +4704,16 @@ export type QuerySearchArgs = {
 };
 
 
+export type QuerySellerArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QuerySellersArgs = {
+  options?: InputMaybe<SellerListOptions>;
+};
+
+
 export type QueryShippingMethodArgs = {
   id: Scalars['ID'];
 };
@@ -4665,6 +4759,16 @@ export type QueryTestShippingMethodArgs = {
 };
 
 
+export type QueryVendorArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QueryVendorsArgs = {
+  options?: InputMaybe<VendorListOptions>;
+};
+
+
 export type QueryZoneArgs = {
   id: Scalars['ID'];
 };
@@ -4917,6 +5021,55 @@ export type SearchResultSortParameter = {
   price?: InputMaybe<SortOrder>;
 };
 
+export type Seller = Node & {
+  __typename?: 'Seller';
+  createdAt: Scalars['DateTime'];
+  customFields?: Maybe<SellerCustomFields>;
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type SellerCustomFields = {
+  __typename?: 'SellerCustomFields';
+  connectedAccountId?: Maybe<Scalars['String']>;
+};
+
+export type SellerFilterParameter = {
+  connectedAccountId?: InputMaybe<StringOperators>;
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type SellerList = PaginatedList & {
+  __typename?: 'SellerList';
+  items: Array<Seller>;
+  totalItems: Scalars['Int'];
+};
+
+export type SellerListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<SellerFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<SellerSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type SellerSortParameter = {
+  connectedAccountId?: InputMaybe<SortOrder>;
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type ServerConfig = {
   __typename?: 'ServerConfig';
   customFieldConfig: CustomFields;
@@ -5371,6 +5524,7 @@ export type UpdateChannelInput = {
   defaultTaxZoneId?: InputMaybe<Scalars['ID']>;
   id: Scalars['ID'];
   pricesIncludeTax?: InputMaybe<Scalars['Boolean']>;
+  sellerId?: InputMaybe<Scalars['ID']>;
   token?: InputMaybe<Scalars['String']>;
 };
 
@@ -5552,6 +5706,16 @@ export type UpdateRoleInput = {
   permissions?: InputMaybe<Array<Permission>>;
 };
 
+export type UpdateSellerCustomFieldsInput = {
+  connectedAccountId?: InputMaybe<Scalars['String']>;
+};
+
+export type UpdateSellerInput = {
+  customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateShippingMethodInput = {
   calculator?: InputMaybe<ConfigurableOperationInput>;
   checker?: InputMaybe<ConfigurableOperationInput>;
@@ -5585,6 +5749,11 @@ export type UpdateTaxRateInput = {
   zoneId?: InputMaybe<Scalars['ID']>;
 };
 
+export type UpdateVendorInput = {
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateZoneInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   id: Scalars['ID'];
@@ -5604,6 +5773,47 @@ export type User = Node & {
   verified: Scalars['Boolean'];
 };
 
+export type Vendor = Node & {
+  __typename?: 'Vendor';
+  createdAt: Scalars['DateTime'];
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type VendorFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type VendorList = PaginatedList & {
+  __typename?: 'VendorList';
+  items: Array<Vendor>;
+  totalItems: Scalars['Int'];
+};
+
+export type VendorListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<VendorFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<VendorSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type VendorSortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type Zone = Node & {
   __typename?: 'Zone';
   createdAt: Scalars['DateTime'];

+ 4 - 0
packages/core/e2e/__snapshots__/administrator.e2e-spec.ts.snap

@@ -91,6 +91,10 @@ Object {
           "ReadTaxRate",
           "UpdateTaxRate",
           "DeleteTaxRate",
+          "CreateSeller",
+          "ReadSeller",
+          "UpdateSeller",
+          "DeleteSeller",
           "CreateSystem",
           "ReadSystem",
           "UpdateSystem",

+ 1 - 0
packages/core/e2e/custom-field-relations.e2e-spec.ts

@@ -61,6 +61,7 @@ const entitiesWithCustomFields = enumerate<keyof CustomFields>()(
     'ProductOptionGroup',
     'ProductVariant',
     'Promotion',
+    'Seller',
     'ShippingMethod',
     'TaxCategory',
     'TaxRate',

+ 205 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -348,6 +348,7 @@ export type Channel = Node & {
   defaultTaxZone?: Maybe<Zone>;
   id: Scalars['ID'];
   pricesIncludeTax: Scalars['Boolean'];
+  seller?: Maybe<Seller>;
   token: Scalars['String'];
   updatedAt: Scalars['DateTime'];
 };
@@ -629,6 +630,7 @@ export type CreateChannelInput = {
   defaultShippingZoneId: Scalars['ID'];
   defaultTaxZoneId: Scalars['ID'];
   pricesIncludeTax: Scalars['Boolean'];
+  sellerId?: InputMaybe<Scalars['ID']>;
   token: Scalars['String'];
 };
 
@@ -786,6 +788,15 @@ export type CreateRoleInput = {
   permissions: Array<Permission>;
 };
 
+export type CreateSellerCustomFieldsInput = {
+  connectedAccountId?: InputMaybe<Scalars['String']>;
+};
+
+export type CreateSellerInput = {
+  customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+  name: Scalars['String'];
+};
+
 export type CreateShippingMethodInput = {
   calculator: ConfigurableOperationInput;
   checker: ConfigurableOperationInput;
@@ -815,6 +826,10 @@ export type CreateTaxRateInput = {
   zoneId: Scalars['ID'];
 };
 
+export type CreateVendorInput = {
+  name: Scalars['String'];
+};
+
 export type CreateZoneInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   memberIds?: InputMaybe<Array<Scalars['ID']>>;
@@ -1192,6 +1207,7 @@ export type CustomFields = {
   ProductOptionGroup: Array<CustomFieldConfig>;
   ProductVariant: Array<CustomFieldConfig>;
   Promotion: Array<CustomFieldConfig>;
+  Seller: Array<CustomFieldConfig>;
   ShippingMethod: Array<CustomFieldConfig>;
   TaxCategory: Array<CustomFieldConfig>;
   TaxRate: Array<CustomFieldConfig>;
@@ -2399,6 +2415,8 @@ export type Mutation = {
   createPromotion: CreatePromotionResult;
   /** Create a new Role */
   createRole: Role;
+  /** Create a new Seller */
+  createSeller: Seller;
   /** Create a new ShippingMethod */
   createShippingMethod: ShippingMethod;
   /** Create a new Tag */
@@ -2407,6 +2425,8 @@ export type Mutation = {
   createTaxCategory: TaxCategory;
   /** Create a new TaxRate */
   createTaxRate: TaxRate;
+  /** Create a new Vendor */
+  createVendor: Vendor;
   /** Create a new Zone */
   createZone: Zone;
   /** Delete an Administrator */
@@ -2454,6 +2474,8 @@ export type Mutation = {
   deletePromotion: DeletionResponse;
   /** Delete an existing Role */
   deleteRole: DeletionResponse;
+  /** Delete a Seller */
+  deleteSeller: DeletionResponse;
   /** Delete a ShippingMethod */
   deleteShippingMethod: DeletionResponse;
   /** Delete an existing Tag */
@@ -2462,6 +2484,8 @@ export type Mutation = {
   deleteTaxCategory: DeletionResponse;
   /** Delete a TaxRate */
   deleteTaxRate: DeletionResponse;
+  /** Delete a Vendor */
+  deleteVendor: DeletionResponse;
   /** Delete a Zone */
   deleteZone: DeletionResponse;
   flushBufferedJobs: Success;
@@ -2556,6 +2580,8 @@ export type Mutation = {
   updatePromotion: UpdatePromotionResult;
   /** Update an existing Role */
   updateRole: Role;
+  /** Update an existing Seller */
+  updateSeller: Seller;
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod;
   /** Update an existing Tag */
@@ -2564,6 +2590,8 @@ export type Mutation = {
   updateTaxCategory: TaxCategory;
   /** Update an existing TaxRate */
   updateTaxRate: TaxRate;
+  /** Update an existing Vendor */
+  updateVendor: Vendor;
   /** Update an existing Zone */
   updateZone: Zone;
 };
@@ -2769,6 +2797,11 @@ export type MutationCreateRoleArgs = {
 };
 
 
+export type MutationCreateSellerArgs = {
+  input: CreateSellerInput;
+};
+
+
 export type MutationCreateShippingMethodArgs = {
   input: CreateShippingMethodInput;
 };
@@ -2789,6 +2822,11 @@ export type MutationCreateTaxRateArgs = {
 };
 
 
+export type MutationCreateVendorArgs = {
+  input: CreateVendorInput;
+};
+
+
 export type MutationCreateZoneArgs = {
   input: CreateZoneInput;
 };
@@ -2918,6 +2956,11 @@ export type MutationDeleteRoleArgs = {
 };
 
 
+export type MutationDeleteSellerArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationDeleteShippingMethodArgs = {
   id: Scalars['ID'];
 };
@@ -2938,6 +2981,11 @@ export type MutationDeleteTaxRateArgs = {
 };
 
 
+export type MutationDeleteVendorArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationDeleteZoneArgs = {
   id: Scalars['ID'];
 };
@@ -3210,6 +3258,11 @@ export type MutationUpdateRoleArgs = {
 };
 
 
+export type MutationUpdateSellerArgs = {
+  input: UpdateSellerInput;
+};
+
+
 export type MutationUpdateShippingMethodArgs = {
   input: UpdateShippingMethodInput;
 };
@@ -3230,6 +3283,11 @@ export type MutationUpdateTaxRateArgs = {
 };
 
 
+export type MutationUpdateVendorArgs = {
+  input: UpdateVendorInput;
+};
+
+
 export type MutationUpdateZoneArgs = {
   input: UpdateZoneInput;
 };
@@ -3302,7 +3360,10 @@ export type NumberRange = {
 export type Order = Node & {
   /** An order is active as long as the payment process has not been completed */
   active: Scalars['Boolean'];
+  aggregateOrder?: Maybe<Order>;
+  aggregateOrderId?: Maybe<Scalars['ID']>;
   billingAddress?: Maybe<OrderAddress>;
+  channels: Array<Channel>;
   /** A unique code for the Order */
   code: Scalars['String'];
   /** An array of all coupon codes applied to the Order */
@@ -3326,6 +3387,7 @@ export type Order = Node & {
   payments?: Maybe<Array<Payment>>;
   /** Promotions applied to the order. Only gets populated after the payment process has completed. */
   promotions: Array<Promotion>;
+  sellerOrders?: Maybe<Array<Order>>;
   shipping: Scalars['Int'];
   shippingAddress?: Maybe<OrderAddress>;
   shippingLines: Array<ShippingLine>;
@@ -3354,6 +3416,7 @@ export type Order = Node & {
   totalQuantity: Scalars['Int'];
   /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
   totalWithTax: Scalars['Int'];
+  type: OrderType;
   updatedAt: Scalars['DateTime'];
 };
 
@@ -3378,6 +3441,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
   active?: InputMaybe<BooleanOperators>;
+  aggregateOrderId?: InputMaybe<IdOperators>;
   code?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
   currencyCode?: InputMaybe<StringOperators>;
@@ -3393,6 +3457,7 @@ export type OrderFilterParameter = {
   totalQuantity?: InputMaybe<NumberOperators>;
   totalWithTax?: InputMaybe<NumberOperators>;
   transactionId?: InputMaybe<StringOperators>;
+  type?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
 };
 
@@ -3554,6 +3619,7 @@ export type OrderProcessState = {
 };
 
 export type OrderSortParameter = {
+  aggregateOrderId?: InputMaybe<SortOrder>;
   code?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   customerLastName?: InputMaybe<SortOrder>;
@@ -3595,6 +3661,12 @@ export type OrderTaxSummary = {
   taxTotal: Scalars['Int'];
 };
 
+export enum OrderType {
+  Aggregate = 'Aggregate',
+  Regular = 'Regular',
+  Seller = 'Seller'
+}
+
 export type PaginatedList = {
   items: Array<Node>;
   totalItems: Scalars['Int'];
@@ -3759,6 +3831,8 @@ export enum Permission {
   CreateProduct = 'CreateProduct',
   /** Grants permission to create Promotion */
   CreatePromotion = 'CreatePromotion',
+  /** Grants permission to create Seller */
+  CreateSeller = 'CreateSeller',
   /** Grants permission to create PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   CreateSettings = 'CreateSettings',
   /** Grants permission to create ShippingMethod */
@@ -3799,6 +3873,8 @@ export enum Permission {
   DeleteProduct = 'DeleteProduct',
   /** Grants permission to delete Promotion */
   DeletePromotion = 'DeletePromotion',
+  /** Grants permission to delete Seller */
+  DeleteSeller = 'DeleteSeller',
   /** Grants permission to delete PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   DeleteSettings = 'DeleteSettings',
   /** Grants permission to delete ShippingMethod */
@@ -3843,6 +3919,8 @@ export enum Permission {
   ReadProduct = 'ReadProduct',
   /** Grants permission to read Promotion */
   ReadPromotion = 'ReadPromotion',
+  /** Grants permission to read Seller */
+  ReadSeller = 'ReadSeller',
   /** Grants permission to read PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   ReadSettings = 'ReadSettings',
   /** Grants permission to read ShippingMethod */
@@ -3887,6 +3965,8 @@ export enum Permission {
   UpdateProduct = 'UpdateProduct',
   /** Grants permission to update Promotion */
   UpdatePromotion = 'UpdatePromotion',
+  /** Grants permission to update Seller */
+  UpdateSeller = 'UpdateSeller',
   /** Grants permission to update PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   UpdateSettings = 'UpdateSettings',
   /** Grants permission to update ShippingMethod */
@@ -4294,6 +4374,8 @@ export type Query = {
   role?: Maybe<Role>;
   roles: RoleList;
   search: SearchResponse;
+  seller?: Maybe<Seller>;
+  sellers: SellerList;
   shippingCalculators: Array<ConfigurableOperationDefinition>;
   shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
   shippingMethod?: Maybe<ShippingMethod>;
@@ -4306,6 +4388,8 @@ export type Query = {
   taxRates: TaxRateList;
   testEligibleShippingMethods: Array<ShippingMethodQuote>;
   testShippingMethod: TestShippingMethodResult;
+  vendor?: Maybe<Vendor>;
+  vendors: VendorList;
   zone?: Maybe<Zone>;
   zones: Array<Zone>;
 };
@@ -4500,6 +4584,16 @@ export type QuerySearchArgs = {
 };
 
 
+export type QuerySellerArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QuerySellersArgs = {
+  options?: InputMaybe<SellerListOptions>;
+};
+
+
 export type QueryShippingMethodArgs = {
   id: Scalars['ID'];
 };
@@ -4545,6 +4639,16 @@ export type QueryTestShippingMethodArgs = {
 };
 
 
+export type QueryVendorArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QueryVendorsArgs = {
+  options?: InputMaybe<VendorListOptions>;
+};
+
+
 export type QueryZoneArgs = {
   id: Scalars['ID'];
 };
@@ -4783,6 +4887,52 @@ export type SearchResultSortParameter = {
   price?: InputMaybe<SortOrder>;
 };
 
+export type Seller = Node & {
+  createdAt: Scalars['DateTime'];
+  customFields?: Maybe<SellerCustomFields>;
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type SellerCustomFields = {
+  connectedAccountId?: Maybe<Scalars['String']>;
+};
+
+export type SellerFilterParameter = {
+  connectedAccountId?: InputMaybe<StringOperators>;
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type SellerList = PaginatedList & {
+  items: Array<Seller>;
+  totalItems: Scalars['Int'];
+};
+
+export type SellerListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<SellerFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<SellerSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type SellerSortParameter = {
+  connectedAccountId?: InputMaybe<SortOrder>;
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type ServerConfig = {
   customFieldConfig: CustomFields;
   orderProcess: Array<OrderProcessState>;
@@ -5214,6 +5364,7 @@ export type UpdateChannelInput = {
   defaultTaxZoneId?: InputMaybe<Scalars['ID']>;
   id: Scalars['ID'];
   pricesIncludeTax?: InputMaybe<Scalars['Boolean']>;
+  sellerId?: InputMaybe<Scalars['ID']>;
   token?: InputMaybe<Scalars['String']>;
 };
 
@@ -5395,6 +5546,16 @@ export type UpdateRoleInput = {
   permissions?: InputMaybe<Array<Permission>>;
 };
 
+export type UpdateSellerCustomFieldsInput = {
+  connectedAccountId?: InputMaybe<Scalars['String']>;
+};
+
+export type UpdateSellerInput = {
+  customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateShippingMethodInput = {
   calculator?: InputMaybe<ConfigurableOperationInput>;
   checker?: InputMaybe<ConfigurableOperationInput>;
@@ -5428,6 +5589,11 @@ export type UpdateTaxRateInput = {
   zoneId?: InputMaybe<Scalars['ID']>;
 };
 
+export type UpdateVendorInput = {
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateZoneInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   id: Scalars['ID'];
@@ -5446,6 +5612,45 @@ export type User = Node & {
   verified: Scalars['Boolean'];
 };
 
+export type Vendor = Node & {
+  createdAt: Scalars['DateTime'];
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type VendorFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type VendorList = PaginatedList & {
+  items: Array<Vendor>;
+  totalItems: Scalars['Int'];
+};
+
+export type VendorListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<VendorFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<VendorSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type VendorSortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type Zone = Node & {
   createdAt: Scalars['DateTime'];
   customFields?: Maybe<Scalars['JSON']>;

+ 35 - 2
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -73,6 +73,7 @@ export type Asset = Node & {
   name: Scalars['String'];
   preview: Scalars['String'];
   source: Scalars['String'];
+  tags: Array<Tag>;
   type: AssetType;
   updatedAt: Scalars['DateTime'];
   width: Scalars['Int'];
@@ -135,6 +136,7 @@ export type Channel = Node & {
   defaultTaxZone?: Maybe<Zone>;
   id: Scalars['ID'];
   pricesIncludeTax: Scalars['Boolean'];
+  seller?: Maybe<Seller>;
   token: Scalars['String'];
   updatedAt: Scalars['DateTime'];
 };
@@ -1690,7 +1692,7 @@ export type MutationSetOrderShippingAddressArgs = {
 
 
 export type MutationSetOrderShippingMethodArgs = {
-  shippingMethodId: Scalars['ID'];
+  shippingMethodId: Array<Scalars['ID']>;
 };
 
 
@@ -1840,6 +1842,7 @@ export type Order = Node & {
   totalQuantity: Scalars['Int'];
   /** The final payable amount. Equal to subTotalWithTax plus shippingWithTax */
   totalWithTax: Scalars['Int'];
+  type: OrderType;
   updatedAt: Scalars['DateTime'];
 };
 
@@ -1877,6 +1880,7 @@ export type OrderFilterParameter = {
   total?: InputMaybe<NumberOperators>;
   totalQuantity?: InputMaybe<NumberOperators>;
   totalWithTax?: InputMaybe<NumberOperators>;
+  type?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
 };
 
@@ -2054,6 +2058,12 @@ export type OrderTaxSummary = {
   taxTotal: Scalars['Int'];
 };
 
+export enum OrderType {
+  Aggregate = 'Aggregate',
+  Regular = 'Regular',
+  Seller = 'Seller'
+}
+
 export type PaginatedList = {
   items: Array<Node>;
   totalItems: Scalars['Int'];
@@ -2213,6 +2223,8 @@ export enum Permission {
   CreateProduct = 'CreateProduct',
   /** Grants permission to create Promotion */
   CreatePromotion = 'CreatePromotion',
+  /** Grants permission to create Seller */
+  CreateSeller = 'CreateSeller',
   /** Grants permission to create PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   CreateSettings = 'CreateSettings',
   /** Grants permission to create ShippingMethod */
@@ -2253,6 +2265,8 @@ export enum Permission {
   DeleteProduct = 'DeleteProduct',
   /** Grants permission to delete Promotion */
   DeletePromotion = 'DeletePromotion',
+  /** Grants permission to delete Seller */
+  DeleteSeller = 'DeleteSeller',
   /** Grants permission to delete PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   DeleteSettings = 'DeleteSettings',
   /** Grants permission to delete ShippingMethod */
@@ -2297,6 +2311,8 @@ export enum Permission {
   ReadProduct = 'ReadProduct',
   /** Grants permission to read Promotion */
   ReadPromotion = 'ReadPromotion',
+  /** Grants permission to read Seller */
+  ReadSeller = 'ReadSeller',
   /** Grants permission to read PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   ReadSettings = 'ReadSettings',
   /** Grants permission to read ShippingMethod */
@@ -2341,6 +2357,8 @@ export enum Permission {
   UpdateProduct = 'UpdateProduct',
   /** Grants permission to update Promotion */
   UpdatePromotion = 'UpdatePromotion',
+  /** Grants permission to update Seller */
+  UpdateSeller = 'UpdateSeller',
   /** Grants permission to update PaymentMethods, ShippingMethods, TaxCategories, TaxRates, Zones, Countries, System & GlobalSettings */
   UpdateSettings = 'UpdateSettings',
   /** Grants permission to update ShippingMethod */
@@ -2794,6 +2812,14 @@ export type SearchResultSortParameter = {
   price?: InputMaybe<SortOrder>;
 };
 
+export type Seller = Node & {
+  createdAt: Scalars['DateTime'];
+  customFields?: Maybe<Scalars['JSON']>;
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
 export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError | Order;
 
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
@@ -3017,6 +3043,13 @@ export type User = Node & {
   verified: Scalars['Boolean'];
 };
 
+export type Vendor = Node & {
+  createdAt: Scalars['DateTime'];
+  id: Scalars['ID'];
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
 /**
  * Returned if the verification token (used to verify a Customer's email address) is valid, but has
  * expired according to the `verificationTokenDuration` setting in the AuthOptions.
@@ -3191,7 +3224,7 @@ export type GetShippingMethodsQueryVariables = Exact<{ [key: string]: never; }>;
 export type GetShippingMethodsQuery = { eligibleShippingMethods: Array<{ id: string, code: string, price: number, name: string, description: string }> };
 
 export type SetShippingMethodMutationVariables = Exact<{
-  id: Scalars['ID'];
+  id: Array<Scalars['ID']> | Scalars['ID'];
 }>;
 
 

+ 1 - 1
packages/core/e2e/graphql/shop-definitions.ts

@@ -403,7 +403,7 @@ export const GET_ELIGIBLE_SHIPPING_METHODS = gql`
 `;
 
 export const SET_SHIPPING_METHOD = gql`
-    mutation SetShippingMethod($id: ID!) {
+    mutation SetShippingMethod($id: [ID!]!) {
         setOrderShippingMethod(shippingMethodId: $id) {
             ...TestOrderFragment
             ... on ErrorResult {

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

@@ -31,6 +31,7 @@ import { ProductResolver } from './resolvers/admin/product.resolver';
 import { PromotionResolver } from './resolvers/admin/promotion.resolver';
 import { RoleResolver } from './resolvers/admin/role.resolver';
 import { SearchResolver } from './resolvers/admin/search.resolver';
+import { SellerResolver } from './resolvers/admin/seller.resolver';
 import { ShippingMethodResolver } from './resolvers/admin/shipping-method.resolver';
 import { TagResolver } from './resolvers/admin/tag.resolver';
 import { TaxCategoryResolver } from './resolvers/admin/tax-category.resolver';
@@ -38,6 +39,7 @@ import { TaxRateResolver } from './resolvers/admin/tax-rate.resolver';
 import { ZoneResolver } from './resolvers/admin/zone.resolver';
 import { AdministratorEntityResolver } from './resolvers/entity/administrator-entity.resolver';
 import { AssetEntityResolver } from './resolvers/entity/asset-entity.resolver';
+import { ChannelEntityResolver } from './resolvers/entity/channel-entity.resolver';
 import { CollectionEntityResolver } from './resolvers/entity/collection-entity.resolver';
 import { CountryEntityResolver } from './resolvers/entity/country-entity.resolver';
 import {
@@ -108,6 +110,7 @@ const adminResolvers = [
     TagResolver,
     TaxCategoryResolver,
     TaxRateResolver,
+    SellerResolver,
     ZoneResolver,
 ];
 
@@ -120,6 +123,7 @@ const shopResolvers = [
 ];
 
 export const entityResolvers = [
+    ChannelEntityResolver,
     CollectionEntityResolver,
     CountryEntityResolver,
     CustomerEntityResolver,

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

@@ -208,6 +208,6 @@ export class DraftOrderResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: MutationSetDraftOrderShippingMethodArgs,
     ): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
-        return this.orderService.setShippingMethod(ctx, args.orderId, args.shippingMethodId);
+        return this.orderService.setShippingMethod(ctx, args.orderId, [args.shippingMethodId]);
     }
 }

+ 59 - 0
packages/core/src/api/resolvers/admin/seller.resolver.ts

@@ -0,0 +1,59 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import {
+    DeletionResponse,
+    MutationCreateSellerArgs,
+    MutationDeleteSellerArgs,
+    MutationUpdateSellerArgs,
+    Permission,
+    QuerySellerArgs,
+    QuerySellersArgs,
+    SellerList,
+} from '@vendure/common/lib/generated-types';
+
+import { Seller } from '../../../entity/seller/seller.entity';
+import { SellerService } from '../../../service/index';
+import { RequestContext } from '../../common/request-context';
+import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
+import { Transaction } from '../../decorators/transaction.decorator';
+
+@Resolver('Seller')
+export class SellerResolver {
+    constructor(private sellerService: SellerService) {}
+
+    @Query()
+    @Allow(Permission.ReadSeller)
+    async sellers(@Ctx() ctx: RequestContext, @Args() args: QuerySellersArgs): Promise<SellerList> {
+        return this.sellerService.findAll(ctx, args.options);
+    }
+
+    @Query()
+    @Allow(Permission.ReadSeller)
+    async seller(@Ctx() ctx: RequestContext, @Args() args: QuerySellerArgs): Promise<Seller | undefined> {
+        return this.sellerService.findOne(ctx, args.id);
+    }
+
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.CreateSeller)
+    async createSeller(@Ctx() ctx: RequestContext, @Args() args: MutationCreateSellerArgs): Promise<Seller> {
+        return this.sellerService.create(ctx, args.input);
+    }
+
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.UpdateSeller)
+    async updateSeller(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateSellerArgs): Promise<Seller> {
+        return this.sellerService.update(ctx, args.input);
+    }
+
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.DeleteSeller)
+    async deleteSeller(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationDeleteSellerArgs,
+    ): Promise<DeletionResponse> {
+        return this.sellerService.delete(ctx, args.id);
+    }
+}

+ 19 - 0
packages/core/src/api/resolvers/entity/channel-entity.resolver.ts

@@ -0,0 +1,19 @@
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+
+import { Channel } from '../../../entity/channel/channel.entity';
+import { Seller } from '../../../entity/seller/seller.entity';
+import { SellerService } from '../../../service/index';
+import { RequestContext } from '../../common/request-context';
+import { Ctx } from '../../decorators/request-context.decorator';
+
+@Resolver('Channel')
+export class ChannelEntityResolver {
+    constructor(private sellerService: SellerService) {}
+
+    @ResolveField()
+    async seller(@Ctx() ctx: RequestContext, @Parent() channel: Channel): Promise<Seller | undefined> {
+        return channel.sellerId
+            ? channel.seller ?? (await this.sellerService.findOne(ctx, channel.sellerId))
+            : undefined;
+    }
+}

+ 30 - 1
packages/core/src/api/resolvers/entity/order-entity.resolver.ts

@@ -1,7 +1,7 @@
 import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
 import { HistoryEntryListOptions, OrderHistoryArgs, SortOrder } from '@vendure/common/lib/generated-types';
 
-import { assertFound } from '../../../common/utils';
+import { assertFound, idsAreEqual } from '../../../common/utils';
 import { Order } from '../../../entity/order/order.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { HistoryService } from '../../../service/services/history.service';
@@ -78,6 +78,14 @@ export class OrderEntityResolver {
 export class OrderAdminEntityResolver {
     constructor(private orderService: OrderService) {}
 
+    @ResolveField()
+    async channels(@Ctx() ctx: RequestContext, @Parent() order: Order) {
+        const channels = order.channels ?? (await this.orderService.getOrderChannels(ctx, order));
+        return channels.filter(channel =>
+            ctx.session?.user?.channelPermissions.find(cp => idsAreEqual(cp.id, channel.id)),
+        );
+    }
+
     @ResolveField()
     async modifications(@Ctx() ctx: RequestContext, @Parent() order: Order) {
         if (order.modifications) {
@@ -90,4 +98,25 @@ export class OrderAdminEntityResolver {
     async nextStates(@Parent() order: Order) {
         return this.orderService.getNextOrderStates(order);
     }
+
+    @ResolveField()
+    async sellerOrders(@Ctx() ctx: RequestContext, @Parent() order: Order) {
+        const sellerOrders = await this.orderService.getSellerOrders(ctx, order);
+        // Only return seller orders on those channels to which the active user has access.
+        const userChannelIds = ctx.session?.user?.channelPermissions.map(cp => cp.id) ?? [];
+        return sellerOrders.filter(sellerOrder =>
+            sellerOrder.channels.find(c => userChannelIds.includes(c.id)),
+        );
+    }
+
+    @ResolveField()
+    async aggregateOrder(@Ctx() ctx: RequestContext, @Parent() order: Order) {
+        const aggregateOrder = await this.orderService.getAggregateOrder(ctx, order);
+        const userChannelIds = ctx.session?.user?.channelPermissions.map(cp => cp.id) ?? [];
+        // Only return the aggregate order if the active user has permissions on that channel
+        return aggregateOrder &&
+            userChannelIds.find(id => aggregateOrder.channels.find(channel => idsAreEqual(channel.id, id)))
+            ? aggregateOrder
+            : undefined;
+    }
 }

+ 2 - 0
packages/core/src/api/schema/admin-api/channel.api.graphql

@@ -23,6 +23,7 @@ input CreateChannelInput {
     currencyCode: CurrencyCode!
     defaultTaxZoneId: ID!
     defaultShippingZoneId: ID!
+    sellerId: ID
 }
 
 input UpdateChannelInput {
@@ -34,6 +35,7 @@ input UpdateChannelInput {
     currencyCode: CurrencyCode
     defaultTaxZoneId: ID
     defaultShippingZoneId: ID
+    sellerId: ID
 }
 
 "Returned if attempting to set a Channel's defaultLanguageCode to a language which is not enabled in GlobalSettings"

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

@@ -1,6 +1,10 @@
 type Order {
     nextStates: [String!]!
     modifications: [OrderModification!]!
+    sellerOrders: [Order!]
+    aggregateOrder: Order
+    aggregateOrderId: ID
+    channels: [Channel!]!
 }
 
 type Fulfillment {

+ 29 - 0
packages/core/src/api/schema/admin-api/seller.api.graphql

@@ -0,0 +1,29 @@
+type Query {
+  sellers(options: SellerListOptions): SellerList!
+  seller(id: ID!): Seller
+}
+
+type Mutation {
+  "Create a new Seller"
+  createSeller(input: CreateSellerInput!): Seller!
+  "Update an existing Seller"
+  updateSeller(input: UpdateSellerInput!): Seller!
+  "Delete a Seller"
+  deleteSeller(id: ID!): DeletionResponse!
+}
+
+type SellerList implements PaginatedList {
+    items: [Seller!]!
+    totalItems: Int!
+}
+
+input SellerListOptions
+
+input CreateSellerInput {
+    name: String!
+}
+
+input UpdateSellerInput {
+    id: ID!
+    name: String
+}

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

@@ -9,4 +9,5 @@ type Channel implements Node {
     defaultLanguageCode: LanguageCode!
     currencyCode: CurrencyCode!
     pricesIncludeTax: Boolean!
+    seller: Seller
 }

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

@@ -1,7 +1,14 @@
+enum OrderType {
+    Regular
+    Seller
+    Aggregate
+}
+
 type Order implements Node {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
+    type: OrderType!
     """
     The date & time that the Order was placed, i.e. the Customer
     completed the checkout and the Order is no longer "active"

+ 6 - 0
packages/core/src/api/schema/common/seller.type.graphql

@@ -0,0 +1,6 @@
+type Seller implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    name: String!
+}

+ 1 - 1
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -69,7 +69,7 @@ type Mutation {
     "Allows any custom fields to be set for the active order"
     setOrderCustomFields(input: UpdateOrderInput!): ActiveOrderResult!
     "Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query"
-    setOrderShippingMethod(shippingMethodId: ID!): SetOrderShippingMethodResult!
+    setOrderShippingMethod(shippingMethodId: [ID!]!): SetOrderShippingMethodResult!
     "Add a Payment to the Order"
     addPaymentToOrder(input: PaymentInput!): AddPaymentToOrderResult!
     "Set the Customer for the Order. Required only if the Customer is not currently logged in"

+ 1 - 0
packages/core/src/common/constants.ts

@@ -66,6 +66,7 @@ export const DEFAULT_PERMISSIONS: PermissionDefinition[] = [
     new CrudPermissionDefinition('Tag'),
     new CrudPermissionDefinition('TaxCategory'),
     new CrudPermissionDefinition('TaxRate'),
+    new CrudPermissionDefinition('Seller'),
     new CrudPermissionDefinition('System'),
     new CrudPermissionDefinition('Zone'),
 ];

+ 5 - 1
packages/core/src/config/config.module.ts

@@ -88,8 +88,10 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             stockAllocationStrategy,
             activeOrderStrategy,
             changedPriceHandlingStrategy,
+            orderSellerStrategy,
         } = this.configService.orderOptions;
-        const { customFulfillmentProcess } = this.configService.shippingOptions;
+        const { customFulfillmentProcess, shippingLineAssignmentStrategy } =
+            this.configService.shippingOptions;
         const { customPaymentProcess } = this.configService.paymentOptions;
         const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
         const { entityIdStrategy } = this.configService.entityOptions;
@@ -125,6 +127,8 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             assetImportStrategy,
             changedPriceHandlingStrategy,
             ...(Array.isArray(activeOrderStrategy) ? activeOrderStrategy : [activeOrderStrategy]),
+            orderSellerStrategy,
+            shippingLineAssignmentStrategy,
         ];
     }
 

+ 1 - 0
packages/core/src/config/custom-field/custom-field-types.ts

@@ -217,6 +217,7 @@ export interface CustomFields {
     ProductOptionGroup?: CustomFieldConfig[];
     ProductVariant?: CustomFieldConfig[];
     Promotion?: CustomFieldConfig[];
+    Seller?: CustomFieldConfig[];
     ShippingMethod?: CustomFieldConfig[];
     TaxCategory?: CustomFieldConfig[];
     TaxRate?: CustomFieldConfig[];

+ 5 - 0
packages/core/src/config/default-config.ts

@@ -26,6 +26,7 @@ import { DefaultActiveOrderStrategy } from './order/default-active-order-strateg
 import { DefaultChangedPriceHandlingStrategy } from './order/default-changed-price-handling-strategy';
 import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-item-price-calculation-strategy';
 import { DefaultOrderPlacedStrategy } from './order/default-order-placed-strategy';
+import { DefaultOrderSellerStrategy } from './order/default-order-seller-strategy';
 import { DefaultStockAllocationStrategy } from './order/default-stock-allocation-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
 import { DefaultOrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
@@ -35,6 +36,7 @@ import { defaultPromotionActions, defaultPromotionConditions } from './promotion
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
+import { DefaultShippingLineAssignmentStrategy } from './shipping-method/default-shipping-line-assignment-strategy';
 import { DefaultTaxLineCalculationStrategy } from './tax/default-tax-line-calculation-strategy';
 import { DefaultTaxZoneStrategy } from './tax/default-tax-zone-strategy';
 import { RuntimeVendureConfig } from './vendure-config';
@@ -124,6 +126,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     shippingOptions: {
         shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
         shippingCalculators: [defaultShippingCalculator],
+        shippingLineAssignmentStrategy: new DefaultShippingLineAssignmentStrategy(),
         customFulfillmentProcess: [],
         fulfillmentHandlers: [manualFulfillmentHandler],
     },
@@ -140,6 +143,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         changedPriceHandlingStrategy: new DefaultChangedPriceHandlingStrategy(),
         orderPlacedStrategy: new DefaultOrderPlacedStrategy(),
         activeOrderStrategy: new DefaultActiveOrderStrategy(),
+        orderSellerStrategy: new DefaultOrderSellerStrategy(),
     },
     paymentOptions: {
         paymentMethodEligibilityCheckers: [],
@@ -182,6 +186,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         ProductOptionGroup: [],
         ProductVariant: [],
         Promotion: [],
+        Seller: [],
         ShippingMethod: [],
         TaxCategory: [],
         TaxRate: [],

+ 4 - 0
packages/core/src/config/index.ts

@@ -36,12 +36,14 @@ export * from './order/changed-price-handling-strategy';
 export * from './order/custom-order-process';
 export * from './order/default-changed-price-handling-strategy';
 export * from './order/default-order-placed-strategy';
+export * from './order/default-order-seller-strategy';
 export * from './order/default-stock-allocation-strategy';
 export * from './order/merge-orders-strategy';
 export * from './order/order-code-strategy';
 export * from './order/order-item-price-calculation-strategy';
 export * from './order/order-merge-strategy';
 export * from './order/order-placed-strategy';
+export * from './order/order-seller-strategy';
 export * from './order/stock-allocation-strategy';
 export * from './order/use-existing-strategy';
 export * from './order/use-guest-if-existing-empty-strategy';
@@ -57,8 +59,10 @@ export * from './session-cache/noop-session-cache-strategy';
 export * from './session-cache/session-cache-strategy';
 export * from './shipping-method/default-shipping-calculator';
 export * from './shipping-method/default-shipping-eligibility-checker';
+export * from './shipping-method/default-shipping-line-assignment-strategy';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
+export * from './shipping-method/shipping-line-assignment-strategy';
 export * from './system/health-check-strategy';
 export * from './tax/default-tax-line-calculation-strategy';
 export * from './tax/default-tax-zone-strategy';

+ 5 - 0
packages/core/src/config/order/default-order-seller-strategy.ts

@@ -0,0 +1,5 @@
+import { OrderSellerStrategy } from './order-seller-strategy';
+
+export class DefaultOrderSellerStrategy implements OrderSellerStrategy {
+    // By default, Orders will not get split.
+}

+ 1 - 0
packages/core/src/config/order/order-item-price-calculation-strategy.ts

@@ -58,5 +58,6 @@ export interface OrderItemPriceCalculationStrategy extends InjectableStrategy {
         productVariant: ProductVariant,
         orderLineCustomFields: { [key: string]: any },
         order: Order,
+        // TODO: v2 - pass the quantity to allow bulk discounts
     ): PriceCalculationResult | Promise<PriceCalculationResult>;
 }

+ 28 - 0
packages/core/src/config/order/order-seller-strategy.ts

@@ -0,0 +1,28 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/index';
+import { InjectableStrategy } from '../../common/index';
+import { Channel, Order, OrderLine, Payment, ShippingLine, Surcharge } from '../../entity/index';
+import { OrderState } from '../../service/index';
+
+export interface SplitOrderContents {
+    channelId: ID;
+    state: OrderState;
+    lines: OrderLine[];
+    surcharges: Surcharge[];
+    shippingLines: ShippingLine[];
+}
+
+export interface OrderSellerStrategy extends InjectableStrategy {
+    setOrderLineSellerChannel?(
+        ctx: RequestContext,
+        orderLine: OrderLine,
+    ): Channel | undefined | Promise<Channel | undefined>;
+    splitOrder?(ctx: RequestContext, order: Order): SplitOrderContents[] | Promise<SplitOrderContents[]>;
+    createSurcharges?(ctx: RequestContext, sellerOrder: Order): Surcharge[] | Promise<Surcharge[]>;
+    afterSellerOrdersCreated?(
+        ctx: RequestContext,
+        aggregateOrder: Order,
+        sellerOrders: Order[],
+    ): void | Promise<void>;
+}

+ 14 - 0
packages/core/src/config/shipping-method/default-shipping-line-assignment-strategy.ts

@@ -0,0 +1,14 @@
+import { RequestContext } from '../../api/index';
+import { Order, OrderLine, ShippingLine } from '../../entity/index';
+
+import { ShippingLineAssignmentStrategy } from './shipping-line-assignment-strategy';
+
+export class DefaultShippingLineAssignmentStrategy implements ShippingLineAssignmentStrategy {
+    assignShippingLineToOrderLines(
+        ctx: RequestContext,
+        shippingLine: ShippingLine,
+        order: Order,
+    ): OrderLine[] | Promise<OrderLine[]> {
+        return order.lines;
+    }
+}

+ 11 - 0
packages/core/src/config/shipping-method/shipping-line-assignment-strategy.ts

@@ -0,0 +1,11 @@
+import { RequestContext } from '../../api/index';
+import { InjectableStrategy } from '../../common/index';
+import { Order, OrderLine, ShippingLine } from '../../entity/index';
+
+export interface ShippingLineAssignmentStrategy extends InjectableStrategy {
+    assignShippingLineToOrderLines(
+        ctx: RequestContext,
+        shippingLine: ShippingLine,
+        order: Order,
+    ): OrderLine[] | Promise<OrderLine[]>;
+}

+ 4 - 1
packages/core/src/config/vendure-config.ts

@@ -34,6 +34,7 @@ import { OrderCodeStrategy } from './order/order-code-strategy';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
 import { OrderPlacedStrategy } from './order/order-placed-strategy';
+import { OrderSellerStrategy } from './order/order-seller-strategy';
 import { StockAllocationStrategy } from './order/stock-allocation-strategy';
 import { CustomPaymentProcess } from './payment/custom-payment-process';
 import { PaymentMethodEligibilityChecker } from './payment/payment-method-eligibility-checker';
@@ -43,6 +44,7 @@ import { PromotionCondition } from './promotion/promotion-condition';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
+import { ShippingLineAssignmentStrategy } from './shipping-method/shipping-line-assignment-strategy';
 import { HealthCheckStrategy } from './system/health-check-strategy';
 import { TaxLineCalculationStrategy } from './tax/tax-line-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
@@ -570,6 +572,7 @@ export interface OrderOptions {
      * @default DefaultActiveOrderStrategy
      */
     activeOrderStrategy?: ActiveOrderStrategy<any> | Array<ActiveOrderStrategy<any>>;
+    orderSellerStrategy?: OrderSellerStrategy;
 }
 
 /**
@@ -687,7 +690,7 @@ export interface ShippingOptions {
      * An array of available ShippingCalculators for use in configuring ShippingMethods
      */
     shippingCalculators?: Array<ShippingCalculator<any>>;
-
+    shippingLineAssignmentStrategy?: ShippingLineAssignmentStrategy;
     /**
      * @description
      * Allows the definition of custom states and transition logic for the fulfillment process state machine.

+ 12 - 1
packages/core/src/entity/channel/channel.entity.ts

@@ -1,9 +1,11 @@
 import { CurrencyCode, LanguageCode } from '@vendure/common/lib/generated-types';
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { Column, Entity, Index, ManyToOne } from 'typeorm';
 
 import { VendureEntity } from '../base/base.entity';
 import { CustomChannelFields } from '../custom-entity-fields';
+import { EntityId } from '../entity-id.decorator';
+import { Seller } from '../seller/seller.entity';
 import { Zone } from '../zone/zone.entity';
 
 /**
@@ -28,6 +30,15 @@ export class Channel extends VendureEntity {
     @Column({ unique: true })
     token: string;
 
+    @Column({ default: '', nullable: true })
+    description: string;
+
+    @ManyToOne(type => Seller)
+    seller?: Seller;
+
+    @EntityId({ nullable: true })
+    sellerId?: ID;
+
     @Column('varchar') defaultLanguageCode: LanguageCode;
 
     @Index()

+ 1 - 0
packages/core/src/entity/custom-entity-fields.ts

@@ -26,6 +26,7 @@ export class CustomProductOptionGroupFieldsTranslation {}
 export class CustomProductVariantFields {}
 export class CustomProductVariantFieldsTranslation {}
 export class CustomPromotionFields {}
+export class CustomSellerFields {}
 export class CustomShippingMethodFields {}
 export class CustomShippingMethodFieldsTranslation {}
 export class CustomTaxCategoryFields {}

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

@@ -41,6 +41,7 @@ import { Product } from './product/product.entity';
 import { Promotion } from './promotion/promotion.entity';
 import { Refund } from './refund/refund.entity';
 import { Role } from './role/role.entity';
+import { Seller } from './seller/seller.entity';
 import { AnonymousSession } from './session/anonymous-session.entity';
 import { AuthenticatedSession } from './session/authenticated-session.entity';
 import { Session } from './session/session.entity';
@@ -124,5 +125,6 @@ export const coreEntitiesMap = {
     TaxCategory,
     TaxRate,
     User,
+    Seller,
     Zone,
 };

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

@@ -1,5 +1,5 @@
 import { Adjustment, AdjustmentType, Discount, TaxLine } from '@vendure/common/lib/generated-types';
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { summate } from '@vendure/common/lib/shared-utils';
 import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm';
 
@@ -9,10 +9,13 @@ import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { Logger } from '../../config/index';
 import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
+import { Channel } from '../channel/channel.entity';
 import { CustomOrderLineFields } from '../custom-entity-fields';
+import { EntityId } from '../entity-id.decorator';
 import { OrderItem } from '../order-item/order-item.entity';
 import { Order } from '../order/order.entity';
 import { ProductVariant } from '../product-variant/product-variant.entity';
+import { ShippingLine } from '../shipping-line/shipping-line.entity';
 import { TaxCategory } from '../tax-category/tax-category.entity';
 
 /**
@@ -27,6 +30,19 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
         super(input);
     }
 
+    @ManyToOne(type => Channel, { nullable: true, onDelete: 'SET NULL' })
+    sellerChannel?: Channel;
+
+    @EntityId({ nullable: true })
+    sellerChannelId?: ID;
+
+    @Index()
+    @ManyToOne(type => ShippingLine, { nullable: true, onDelete: 'SET NULL' })
+    shippingLine?: ShippingLine;
+
+    @EntityId({ nullable: true })
+    shippingLineId?: ID;
+
     @Index()
     @ManyToOne(type => ProductVariant)
     productVariant: ProductVariant;

+ 15 - 0
packages/core/src/entity/order/aggregate-order.entity.ts

@@ -0,0 +1,15 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { ChildEntity, OneToMany } from 'typeorm';
+
+import { Order } from './order.entity';
+import { SellerOrder } from './seller-order.entity';
+
+@ChildEntity()
+export class AggregateOrder extends Order {
+    constructor(input?: DeepPartial<AggregateOrder>) {
+        super(input);
+    }
+
+    @OneToMany(type => SellerOrder, sellerOrder => sellerOrder.aggregateOrder)
+    sellerOrders: SellerOrder[];
+}

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

@@ -1,9 +1,9 @@
 import {
-    Adjustment,
     CurrencyCode,
     Discount,
     OrderAddress,
     OrderTaxSummary,
+    OrderType,
     TaxLine,
 } from '@vendure/common/lib/generated-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
@@ -45,6 +45,18 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
         super(input);
     }
 
+    @Column('varchar')
+    type: OrderType;
+
+    @OneToMany(type => Order, sellerOrder => sellerOrder.aggregateOrder)
+    sellerOrders: Order[];
+
+    @ManyToOne(type => Order, aggregateOrder => aggregateOrder.sellerOrders)
+    aggregateOrder: Order;
+
+    @EntityId({ nullable: true })
+    aggregateOrderId: ID;
+
     /**
      * @description
      * A unique code for the Order, generated according to the

+ 14 - 0
packages/core/src/entity/order/seller-order.entity.ts

@@ -0,0 +1,14 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { AggregateOrder } from './aggregate-order.entity';
+import { Order } from './order.entity';
+
+@ChildEntity()
+export class SellerOrder extends Order {
+    constructor(input?: DeepPartial<SellerOrder>) {
+        super(input);
+    }
+    @ManyToOne(type => AggregateOrder, aggregateOrder => aggregateOrder.sellerOrders)
+    aggregateOrder: AggregateOrder;
+}

+ 2 - 0
packages/core/src/entity/register-custom-entity-fields.ts

@@ -46,6 +46,7 @@ import {
     CustomProductVariantFields,
     CustomProductVariantFieldsTranslation,
     CustomPromotionFields,
+    CustomSellerFields,
     CustomShippingMethodFields,
     CustomShippingMethodFieldsTranslation,
     CustomTaxCategoryFields,
@@ -286,6 +287,7 @@ export function registerCustomEntityFields(config: VendureConfig) {
     registerCustomFieldsForEntity(config, 'TaxRate', CustomTaxRateFields);
     registerCustomFieldsForEntity(config, 'User', CustomUserFields);
     registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields);
+    registerCustomFieldsForEntity(config, 'Seller', CustomSellerFields);
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFields);
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFieldsTranslation, true);
     registerCustomFieldsForEntity(config, 'Zone', CustomZoneFields);

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

@@ -0,0 +1,28 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { Column, Entity } from 'typeorm';
+
+import { SoftDeletable } from '../../common/types/common-types';
+import { HasCustomFields } from '../../config/index';
+import { VendureEntity } from '../base/base.entity';
+import { CustomSellerFields } from '../custom-entity-fields';
+
+/**
+ * @description
+ * An administrative user who has access to the admin ui.
+ *
+ * @docsCategory entities
+ */
+@Entity()
+export class Seller extends VendureEntity implements SoftDeletable, HasCustomFields {
+    constructor(input?: DeepPartial<Seller>) {
+        super(input);
+    }
+
+    @Column({ type: Date, nullable: true })
+    deletedAt: Date | null;
+
+    @Column() name: string;
+
+    @Column(type => CustomSellerFields)
+    customFields: CustomSellerFields;
+}

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

@@ -21,7 +21,7 @@ export class ShippingLine extends VendureEntity {
 
     @Index()
     @ManyToOne(type => ShippingMethod)
-    shippingMethod: ShippingMethod | null;
+    shippingMethod: ShippingMethod;
 
     // TODO: v2 - Add `{ onDelete: 'CASCADE' }` constraint
     @Index()

+ 10 - 0
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -147,6 +147,16 @@ export class OrderModifier {
                 customFields,
             }),
         );
+        const { orderSellerStrategy } = this.configService.orderOptions;
+        if (typeof orderSellerStrategy.setOrderLineSellerChannel === 'function') {
+            orderLine.sellerChannel = await orderSellerStrategy.setOrderLineSellerChannel(ctx, orderLine);
+            await this.connection
+                .getRepository(ctx, OrderLine)
+                .createQueryBuilder()
+                .relation('sellerChannel')
+                .of(orderLine)
+                .set(orderLine.sellerChannel);
+        }
         await this.customFieldRelationService.updateRelations(ctx, OrderLine, { customFields }, orderLine);
         const lineWithRelations = await this.connection.getEntityOrThrow(ctx, OrderLine, orderLine.id, {
             relations: [

+ 141 - 0
packages/core/src/service/helpers/order-splitter/order-splitter.ts

@@ -0,0 +1,141 @@
+import { Injectable } from '@nestjs/common';
+import { OrderType } from '@vendure/common/lib/generated-types';
+import { pick } from '@vendure/common/lib/pick';
+
+import { RequestContext } from '../../../api/index';
+import { ConfigService } from '../../../config/index';
+import { TransactionalConnection } from '../../../connection/index';
+import { Channel, Order, OrderItem, OrderLine, ShippingLine, Surcharge } from '../../../entity/index';
+import { ChannelService } from '../../services/channel.service';
+
+@Injectable()
+export class OrderSplitter {
+    constructor(
+        private connection: TransactionalConnection,
+        private configService: ConfigService,
+        private channelService: ChannelService,
+    ) {}
+
+    async createSellerOrders(
+        ctx: RequestContext,
+        order: Order,
+        afterSellerOrderCreated: (sellerOrder: Order) => Promise<any>,
+    ): Promise<Order[]> {
+        const { orderSellerStrategy } = this.configService.orderOptions;
+        const partialOrders = await orderSellerStrategy.splitOrder?.(ctx, order);
+        if (!partialOrders || partialOrders.length === 0) {
+            // No split is needed
+            return [];
+        }
+        const defaultChannel = await this.channelService.getDefaultChannel(ctx);
+
+        order.type = OrderType.Aggregate;
+        order.sellerOrders = [];
+        for (const partialOrder of partialOrders) {
+            const lines: OrderLine[] = [];
+            for (const line of partialOrder.lines) {
+                lines.push(await this.duplicateOrderLine(ctx, line));
+            }
+            const shippingLines: ShippingLine[] = [];
+            for (const shippingLine of partialOrder.shippingLines) {
+                shippingLines.push(await this.duplicateShippingLine(ctx, shippingLine));
+            }
+            const surcharges: Surcharge[] = [];
+            for (const surcharge of partialOrder.surcharges) {
+                surcharges.push(await this.duplicateSurcharge(ctx, surcharge));
+            }
+            const sellerOrder = await this.connection.getRepository(ctx, Order).save(
+                new Order({
+                    type: OrderType.Seller,
+                    aggregateOrder: order,
+                    code: await this.configService.orderOptions.orderCodeStrategy.generate(ctx),
+                    active: false,
+                    orderPlacedAt: new Date(),
+                    customer: order.customer,
+                    channels: [new Channel({ id: partialOrder.channelId }), defaultChannel],
+                    state: partialOrder.state,
+                    lines,
+                    surcharges,
+                    shippingLines,
+                    couponCodes: order.couponCodes,
+                    modifications: [],
+                    shippingAddress: order.shippingAddress,
+                    billingAddress: order.billingAddress,
+                    subTotal: 0,
+                    subTotalWithTax: 0,
+                    currencyCode: order.currencyCode,
+                }),
+            );
+
+            order.sellerOrders.push(sellerOrder);
+            await afterSellerOrderCreated(sellerOrder);
+        }
+        await orderSellerStrategy.afterSellerOrdersCreated?.(ctx, order, order.sellerOrders);
+        return order.sellerOrders;
+    }
+
+    private async duplicateOrderLine(ctx: RequestContext, line: OrderLine): Promise<OrderLine> {
+        const newLine = await this.connection.getRepository(ctx, OrderLine).save(
+            new OrderLine({
+                ...pick(line, [
+                    'productVariant',
+                    'taxCategory',
+                    'featuredAsset',
+                    'shippingLine',
+                    'customFields',
+                ]),
+                items: [],
+            }),
+        );
+        newLine.items = line.items.map(
+            item =>
+                new OrderItem({
+                    ...pick(item, [
+                        'initialListPrice',
+                        'listPrice',
+                        'listPriceIncludesTax',
+                        'adjustments',
+                        'taxLines',
+                        'cancelled',
+                    ]),
+                    line: newLine,
+                }),
+        );
+        await this.connection.getRepository(ctx, OrderItem).save(newLine.items);
+        return newLine;
+    }
+
+    private async duplicateShippingLine(
+        ctx: RequestContext,
+        shippingLine: ShippingLine,
+    ): Promise<ShippingLine> {
+        return await this.connection.getRepository(ctx, ShippingLine).save(
+            new ShippingLine({
+                ...pick(shippingLine, [
+                    'shippingMethodId',
+                    'order',
+                    'listPrice',
+                    'listPriceIncludesTax',
+                    'adjustments',
+                    'taxLines',
+                ]),
+            }),
+        );
+    }
+
+    private async duplicateSurcharge(ctx: RequestContext, surcharge: Surcharge): Promise<Surcharge> {
+        return await this.connection.getRepository(ctx, Surcharge).save(
+            new Surcharge({
+                ...pick(surcharge, [
+                    'description',
+                    'listPrice',
+                    'listPriceIncludesTax',
+                    'sku',
+                    'taxLines',
+                    'order',
+                    'orderModification',
+                ]),
+            }),
+        );
+    }
+}

+ 2 - 0
packages/core/src/service/index.ts

@@ -10,6 +10,7 @@ export * from './helpers/order-calculator/order-calculator';
 export * from './helpers/order-calculator/prorate';
 export * from './helpers/order-merger/order-merger';
 export * from './helpers/order-modifier/order-modifier';
+export * from './helpers/order-splitter/order-splitter';
 export * from './helpers/order-state-machine/order-state';
 export * from './helpers/password-cipher/password-cipher';
 export * from './helpers/payment-state-machine/payment-state';
@@ -52,4 +53,5 @@ export * from './services/tag.service';
 export * from './services/tax-category.service';
 export * from './services/tax-rate.service';
 export * from './services/user.service';
+export * from './services/seller.service';
 export * from './services/zone.service';

+ 3 - 0
packages/core/src/service/initializer.service.ts

@@ -10,6 +10,7 @@ import { AdministratorService } from './services/administrator.service';
 import { ChannelService } from './services/channel.service';
 import { GlobalSettingsService } from './services/global-settings.service';
 import { RoleService } from './services/role.service';
+import { SellerService } from './services/seller.service';
 import { ShippingMethodService } from './services/shipping-method.service';
 import { TaxRateService } from './services/tax-rate.service';
 import { ZoneService } from './services/zone.service';
@@ -29,6 +30,7 @@ export class InitializerService {
         private shippingMethodService: ShippingMethodService,
         private globalSettingsService: GlobalSettingsService,
         private taxRateService: TaxRateService,
+        private sellerService: SellerService,
         private eventBus: EventBus,
     ) {}
 
@@ -42,6 +44,7 @@ export class InitializerService {
         // there is a default Channel to work with.
         await this.zoneService.initZones();
         await this.globalSettingsService.initGlobalSettings();
+        await this.sellerService.initSellers();
         await this.channelService.initChannels();
         await this.roleService.initRoles();
         await this.administratorService.initAdministrators();

+ 4 - 0
packages/core/src/service/service.module.ts

@@ -17,6 +17,7 @@ import { LocaleStringHydrator } from './helpers/locale-string-hydrator/locale-st
 import { OrderCalculator } from './helpers/order-calculator/order-calculator';
 import { OrderMerger } from './helpers/order-merger/order-merger';
 import { OrderModifier } from './helpers/order-modifier/order-modifier';
+import { OrderSplitter } from './helpers/order-splitter/order-splitter';
 import { OrderStateMachine } from './helpers/order-state-machine/order-state-machine';
 import { PasswordCipher } from './helpers/password-cipher/password-cipher';
 import { PaymentStateMachine } from './helpers/payment-state-machine/payment-state-machine';
@@ -53,6 +54,7 @@ import { ProductService } from './services/product.service';
 import { PromotionService } from './services/promotion.service';
 import { RoleService } from './services/role.service';
 import { SearchService } from './services/search.service';
+import { SellerService } from './services/seller.service';
 import { SessionService } from './services/session.service';
 import { ShippingMethodService } from './services/shipping-method.service';
 import { StockMovementService } from './services/stock-movement.service';
@@ -87,6 +89,7 @@ const services = [
     PromotionService,
     RoleService,
     SearchService,
+    SellerService,
     SessionService,
     ShippingMethodService,
     StockMovementService,
@@ -105,6 +108,7 @@ const helpers = [
     FulfillmentStateMachine,
     OrderMerger,
     OrderModifier,
+    OrderSplitter,
     PaymentStateMachine,
     ListQueryBuilder,
     ShippingCalculator,

+ 31 - 12
packages/core/src/service/services/channel.service.ts

@@ -12,6 +12,7 @@ import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 
+import { RelationPaths } from '../../api';
 import { RequestContext } from '../../api/common/request-context';
 import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
 import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors';
@@ -25,16 +26,17 @@ import { VendureEntity } from '../../entity/base/base.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariantPrice } from '../../entity/product-variant/product-variant-price.entity';
+import { Seller } from '../../entity/seller/seller.entity';
 import { Session } from '../../entity/session/session.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus';
 import { ChangeChannelEvent } from '../../event-bus/events/change-channel-event';
 import { ChannelEvent } from '../../event-bus/events/channel-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
+import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
+
 import { GlobalSettingsService } from './global-settings.service';
-import { RelationPaths } from '../../api';
-import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 
 /**
  * @description
@@ -76,11 +78,11 @@ export class ChannelService {
             name: 'ChannelService.allChannels',
             ttl: this.configService.entityOptions.channelCacheTtl,
             refresh: {
-                fn: async (ctx) => {
+                fn: async ctx => {
                     const { items } = await this.findAll(ctx);
-                    return items
+                    return items;
                 },
-                defaultArgs: [RequestContext.empty()]
+                defaultArgs: [RequestContext.empty()],
             },
         });
     }
@@ -196,7 +198,7 @@ export class ChannelService {
     findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Channel>,
-        relations?: RelationPaths<Channel>
+        relations?: RelationPaths<Channel>,
     ): Promise<PaginatedList<Channel>> {
         return this.listQueryBuilder
             .build(Channel, options, {
@@ -240,10 +242,15 @@ export class ChannelService {
             );
         }
         const newChannel = await this.connection.getRepository(ctx, Channel).save(channel);
+        if (input.sellerId) {
+            const seller = await this.connection.getEntityOrThrow(ctx, Seller, input.sellerId);
+            newChannel.seller = seller;
+            await this.connection.getRepository(ctx, Channel).save(newChannel);
+        }
         await this.customFieldRelationService.updateRelations(ctx, Channel, input, newChannel);
         await this.allChannels.refresh(ctx);
         this.eventBus.publish(new ChannelEvent(ctx, newChannel, 'created', input));
-        return channel;
+        return newChannel;
     }
 
     async update(
@@ -273,6 +280,10 @@ export class ChannelService {
                 input.defaultShippingZoneId,
             );
         }
+        if (input.sellerId) {
+            const seller = await this.connection.getEntityOrThrow(ctx, Seller, input.sellerId);
+            updatedChannel.seller = seller;
+        }
         await this.connection.getRepository(ctx, Channel).save(updatedChannel, { reload: false });
         await this.customFieldRelationService.updateRelations(ctx, Channel, input, updatedChannel);
         await this.allChannels.refresh(ctx);
@@ -324,29 +335,37 @@ export class ChannelService {
      */
     private async ensureDefaultChannelExists() {
         const { defaultChannelToken } = this.configService;
-        const defaultChannel = await this.connection.rawConnection.getRepository(Channel).findOne({
+        let defaultChannel = await this.connection.rawConnection.getRepository(Channel).findOne({
             where: {
                 code: DEFAULT_CHANNEL_CODE,
             },
+            relations: ['seller'],
         });
 
         if (!defaultChannel) {
-            const newDefaultChannel = new Channel({
+            defaultChannel = new Channel({
                 code: DEFAULT_CHANNEL_CODE,
                 defaultLanguageCode: this.configService.defaultLanguageCode,
                 pricesIncludeTax: false,
                 currencyCode: CurrencyCode.USD,
                 token: defaultChannelToken,
             });
-            await this.connection.rawConnection
-                .getRepository(Channel)
-                .save(newDefaultChannel, { reload: false });
         } else if (defaultChannelToken && defaultChannel.token !== defaultChannelToken) {
             defaultChannel.token = defaultChannelToken;
             await this.connection.rawConnection
                 .getRepository(Channel)
                 .save(defaultChannel, { reload: false });
         }
+        if (!defaultChannel.seller) {
+            const seller = await this.connection.rawConnection.getRepository(Seller).find();
+            if (seller.length === 0) {
+                throw new InternalServerError('No Sellers were found. Could not initialize default Channel.');
+            }
+            defaultChannel.seller = seller[0];
+            await this.connection.rawConnection
+                .getRepository(Channel)
+                .save(defaultChannel, { reload: false });
+        }
     }
 
     private async validateDefaultLanguageCode(

+ 109 - 32
packages/core/src/service/services/order.service.ts

@@ -27,6 +27,7 @@ import {
     OrderLineInput,
     OrderListOptions,
     OrderProcessState,
+    OrderType,
     RefundOrderInput,
     RefundOrderResult,
     SettlePaymentResult,
@@ -37,7 +38,6 @@ import {
 } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { summate } from '@vendure/common/lib/shared-utils';
-import { unique } from '@vendure/common/lib/unique';
 import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -72,7 +72,6 @@ import {
     PaymentDeclinedError,
     PaymentFailedError,
 } from '../../common/error/generated-graphql-shop-errors';
-import { EntityRelationPaths, EntityRelations } from '../../common/index';
 import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { ListQueryOptions, PaymentMetadata } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -82,7 +81,7 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Customer } from '../../entity/customer/customer.entity';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
-import { Session } from '../../entity/index';
+import { Channel, Session } from '../../entity/index';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../entity/order-modification/order-modification.entity';
@@ -96,17 +95,20 @@ import { Allocation } from '../../entity/stock-movement/allocation.entity';
 import { Surcharge } from '../../entity/surcharge/surcharge.entity';
 import { User } from '../../entity/user/user.entity';
 import { EventBus } from '../../event-bus/event-bus';
-import { CouponCodeEvent } from '../../event-bus/index';
-import { OrderEvent } from '../../event-bus/index';
-import { OrderStateTransitionEvent } from '../../event-bus/index';
-import { RefundStateTransitionEvent } from '../../event-bus/index';
-import { OrderLineEvent } from '../../event-bus/index';
+import {
+    CouponCodeEvent,
+    OrderEvent,
+    OrderLineEvent,
+    OrderStateTransitionEvent,
+    RefundStateTransitionEvent,
+} from '../../event-bus/index';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
 import { OrderModifier } from '../helpers/order-modifier/order-modifier';
+import { OrderSplitter } from '../helpers/order-splitter/order-splitter';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
 import { PaymentState } from '../helpers/payment-state-machine/payment-state';
@@ -152,6 +154,7 @@ export class OrderService {
         private shippingCalculator: ShippingCalculator,
         private orderStateMachine: OrderStateMachine,
         private orderMerger: OrderMerger,
+        private orderSplitter: OrderSplitter,
         private paymentService: PaymentService,
         private paymentStateMachine: PaymentStateMachine,
         private paymentMethodService: PaymentMethodService,
@@ -385,6 +388,32 @@ export class OrderService {
         });
     }
 
+    getSellerOrders(ctx: RequestContext, order: Order): Promise<Order[]> {
+        return this.connection.getRepository(ctx, Order).find({
+            where: {
+                aggregateOrderId: order.id,
+            },
+            relations: ['channels'],
+        });
+    }
+
+    async getAggregateOrder(ctx: RequestContext, order: Order): Promise<Order | undefined> {
+        return order.aggregateOrderId == null
+            ? undefined
+            : this.connection
+                  .getRepository(ctx, Order)
+                  .findOne(order.aggregateOrderId, { relations: ['channels', 'lines'] });
+    }
+
+    getOrderChannels(ctx: RequestContext, order: Order): Promise<Channel[]> {
+        return this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder('order')
+            .relation('channels')
+            .of(order)
+            .loadMany();
+    }
+
     /**
      * @description
      * Returns any Order associated with the specified User's Customer account
@@ -451,6 +480,7 @@ export class OrderService {
 
     private async createEmptyOrderEntity(ctx: RequestContext) {
         return new Order({
+            type: OrderType.Regular,
             code: await this.configService.orderOptions.orderCodeStrategy.generate(ctx),
             state: this.orderStateMachine.getInitialState(),
             lines: [],
@@ -853,38 +883,68 @@ export class OrderService {
     async setShippingMethod(
         ctx: RequestContext,
         orderId: ID,
-        shippingMethodId: ID,
+        shippingMethodIds: ID[],
     ): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const validationError = this.assertAddingItemsState(order);
         if (validationError) {
             return validationError;
         }
-        const shippingMethod = await this.shippingCalculator.getMethodIfEligible(
-            ctx,
-            order,
-            shippingMethodId,
-        );
-        if (!shippingMethod) {
-            return new IneligibleShippingMethodError();
-        }
-        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: [],
-                    listPrice: 0,
-                    listPriceIncludesTax: ctx.channel.pricesIncludeTax,
-                    taxLines: [],
-                }),
+        for (const [i, shippingMethodId] of shippingMethodIds.entries()) {
+            const shippingMethod = await this.shippingCalculator.getMethodIfEligible(
+                ctx,
+                order,
+                shippingMethodId,
             );
-            order.shippingLines = [shippingLine];
+            if (!shippingMethod) {
+                return new IneligibleShippingMethodError();
+            }
+            let shippingLine: ShippingLine | undefined = order.shippingLines[i];
+            if (shippingLine) {
+                shippingLine.shippingMethod = shippingMethod;
+            } else {
+                shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
+                    new ShippingLine({
+                        shippingMethod,
+                        order,
+                        adjustments: [],
+                        listPrice: 0,
+                        listPriceIncludesTax: ctx.channel.pricesIncludeTax,
+                        taxLines: [],
+                    }),
+                );
+                if (order.shippingLines) {
+                    order.shippingLines.push(shippingLine);
+                } else {
+                    order.shippingLines = [shippingLine];
+                }
+            }
+
+            await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
+        }
+        // remove any now-unused ShippingLines
+        if (shippingMethodIds.length < order.shippingLines.length) {
+            const shippingLinesToDelete = order.shippingLines.splice(shippingMethodIds.length - 1);
+            await this.connection.getRepository(ctx, ShippingLine).remove(shippingLinesToDelete);
         }
-        await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
+        // assign the ShippingLines to the OrderLines
+        await this.connection
+            .getRepository(ctx, OrderLine)
+            .createQueryBuilder('line')
+            .update({ shippingLine: undefined })
+            .whereInIds(order.lines.map(l => l.id));
+        const { shippingLineAssignmentStrategy } = this.configService.shippingOptions;
+        for (const shippingLine of order.shippingLines) {
+            const orderLinesForShippingLine =
+                await shippingLineAssignmentStrategy.assignShippingLineToOrderLines(ctx, shippingLine, order);
+            await this.connection
+                .getRepository(ctx, OrderLine)
+                .createQueryBuilder('line')
+                .update({ shippingLine })
+                .whereInIds(orderLinesForShippingLine.map(l => l.id))
+                .execute();
+        }
+
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         await this.applyPriceAdjustments(ctx, order);
         return this.connection.getRepository(ctx, Order).save(order);
@@ -902,12 +962,18 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         order.payments = await this.getOrderPayments(ctx, orderId);
         const fromState = order.state;
+        const fromActiveStatus = order.active;
         try {
             await this.orderStateMachine.transition(ctx, order, state);
         } catch (e: any) {
             const transitionError = ctx.translate(e.message, { fromState, toState: state });
             return new OrderStateTransitionError({ transitionError, fromState, toState: state });
         }
+        if (fromActiveStatus === true && order.active === false) {
+            await this.orderSplitter.createSellerOrders(ctx, order, sellerOrder =>
+                this.applyPriceAdjustments(ctx, sellerOrder),
+            );
+        }
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, state, ctx, order));
         return order;
@@ -923,6 +989,9 @@ export class OrderService {
         fulfillmentId: ID,
         state: FulfillmentState,
     ): Promise<Fulfillment | FulfillmentStateTransitionError> {
+        // TODO: v2: Extract this into a user-configurable area, i.e. CustomFulfillmentProcess,
+        // so that users are able to opt-out of such hard-coded transformations that rely on
+        // the default order states
         const result = await this.fulfillmentService.transitionToState(ctx, fulfillmentId, state);
         if (isGraphQlErrorResult(result)) {
             return result;
@@ -1095,6 +1164,14 @@ export class OrderService {
         return canTransitionToPaymentAuthorized && canTransitionToPaymentSettled;
     }
 
+    /**
+     * TODO: v2: Extract this into a user-configurable area, i.e. CustomPaymentProcess,
+     * so that users are able to opt-out of such hard-coded transformations that rely on
+     * the default order states
+     * @param ctx
+     * @param order
+     * @private
+     */
     private async transitionOrderIfTotalIsCovered(
         ctx: RequestContext,
         order: Order,

+ 75 - 0
packages/core/src/service/services/seller.service.ts

@@ -0,0 +1,75 @@
+import { Injectable } from '@nestjs/common';
+import {
+    CreateSellerInput,
+    DeletionResponse,
+    DeletionResult,
+    UpdateSellerInput,
+} from '@vendure/common/lib/generated-types';
+import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { ListQueryOptions } from '../../common/types/common-types';
+import { TransactionalConnection } from '../../connection/transactional-connection';
+import { Seller } from '../../entity/seller/seller.entity';
+import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+
+/**
+ * @description
+ * Contains methods relating to {@link Seller} entities.
+ *
+ * @docsCategory services
+ */
+@Injectable()
+export class SellerService {
+    constructor(private connection: TransactionalConnection, private listQueryBuilder: ListQueryBuilder) {}
+
+    async initSellers() {
+        await this.ensureDefaultSellerExists();
+    }
+
+    findAll(ctx: RequestContext, options?: ListQueryOptions<Seller>): Promise<PaginatedList<Seller>> {
+        return this.listQueryBuilder
+            .build(Seller, options, { ctx })
+            .getManyAndCount()
+            .then(([items, totalItems]) => ({
+                items,
+                totalItems,
+            }));
+    }
+
+    findOne(ctx: RequestContext, sellerId: ID): Promise<Seller | undefined> {
+        return this.connection.getRepository(ctx, Seller).findOne(sellerId);
+    }
+
+    create(ctx: RequestContext, input: CreateSellerInput) {
+        return this.connection.getRepository(ctx, Seller).save(new Seller(input));
+    }
+
+    async update(ctx: RequestContext, input: UpdateSellerInput) {
+        const seller = await this.connection.getEntityOrThrow(ctx, Seller, input.id);
+        if (input.name) {
+            seller.name = input.name;
+            await this.connection.getRepository(ctx, Seller).save(seller);
+        }
+        return seller;
+    }
+
+    async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
+        const seller = await this.connection.getEntityOrThrow(ctx, Seller, id);
+        await this.connection.getRepository(ctx, Seller).remove(seller);
+        return {
+            result: DeletionResult.DELETED,
+        };
+    }
+
+    private async ensureDefaultSellerExists() {
+        const sellers = await this.connection.rawConnection.getRepository(Seller).find();
+        if (sellers.length === 0) {
+            await this.connection.rawConnection.getRepository(Seller).save(
+                new Seller({
+                    name: 'Default Seller',
+                }),
+            );
+        }
+    }
+}

+ 3 - 1
packages/dev-server/dev-config.ts

@@ -15,6 +15,7 @@ import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
 import path from 'path';
 import { ConnectionOptions } from 'typeorm';
+import { MultivendorPlugin } from './test-plugins/multivendor-plugin/multivendor.plugin';
 
 /**
  * Config settings used during development
@@ -61,11 +62,12 @@ export const devConfig: VendureConfig = {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
     plugins: [
+        MultivendorPlugin,
         AssetServerPlugin.init({
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'assets'),
         }),
-        DefaultSearchPlugin.init({ bufferUpdates: true, indexStockStatus: false }),
+        DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: false }),
         // BullMQJobQueuePlugin.init({}),
         DefaultJobQueuePlugin.init({}),
         // JobQueueTestPlugin.init({ queueCount: 10 }),

+ 65 - 0
packages/dev-server/test-plugins/multivendor-plugin/config/mv-fulfillment-process.ts

@@ -0,0 +1,65 @@
+import {
+    CustomFulfillmentProcess,
+    Fulfillment,
+    FulfillmentState,
+    Order,
+    OrderService,
+    RequestContext,
+} from '@vendure/core';
+
+let orderService: OrderService;
+
+export const multivendorFulfillmentProcess: CustomFulfillmentProcess<FulfillmentState> = {
+    init(injector) {
+        orderService = injector.get(OrderService);
+    },
+    async onTransitionEnd(fromState, toState, data) {
+        if (toState === 'Shipped' || toState === 'Delivered') {
+            await checkAggregateOrderFulfillments({
+                ...data,
+                toState,
+                aggregateOrderHandler: async (aggregateOrder, allFulfillmentStates) => {
+                    if (allFulfillmentStates.every(state => state === 'Shipped')) {
+                        await orderService.transitionToState(data.ctx, aggregateOrder.id, 'Shipped');
+                    } else if (allFulfillmentStates.every(state => state === 'Delivered')) {
+                        await orderService.transitionToState(data.ctx, aggregateOrder.id, 'Delivered');
+                    } else if (allFulfillmentStates.some(state => state === 'Delivered')) {
+                        await orderService.transitionToState(
+                            data.ctx,
+                            aggregateOrder.id,
+                            'PartiallyDelivered',
+                        );
+                    } else if (allFulfillmentStates.some(state => state === 'Shipped')) {
+                        await orderService.transitionToState(data.ctx, aggregateOrder.id, 'PartiallyShipped');
+                    }
+                },
+            });
+        }
+    },
+};
+
+async function checkAggregateOrderFulfillments(input: {
+    ctx: RequestContext;
+    orders: Order[];
+    fulfillment: Fulfillment;
+    toState: FulfillmentState;
+    aggregateOrderHandler: (aggregateOrder: Order, allFulfillmentStates: FulfillmentState[]) => Promise<void>;
+}) {
+    const { ctx, orders, fulfillment, toState, aggregateOrderHandler } = input;
+    for (const order of orders) {
+        const aggregateOrder = await orderService.getAggregateOrder(ctx, order);
+        if (aggregateOrder) {
+            const sellerOrders = await orderService.getSellerOrders(ctx, aggregateOrder);
+            const allFulfillmentStates = (
+                await Promise.all(
+                    sellerOrders.map(
+                        async sellerOrder => await orderService.getOrderFulfillments(ctx, sellerOrder),
+                    ),
+                )
+            )
+                .flat()
+                .map(f => (f.id === fulfillment.id ? toState : f.state));
+            await aggregateOrderHandler(aggregateOrder, allFulfillmentStates);
+        }
+    }
+}

+ 13 - 0
packages/dev-server/test-plugins/multivendor-plugin/config/mv-order-process.ts

@@ -0,0 +1,13 @@
+import { CustomOrderProcess } from '@vendure/core';
+
+export const multivendorOrderProcess: CustomOrderProcess<any> = {
+    onTransitionStart(fromState, toState, data) {
+        if (fromState === 'AddingItems' && toState === 'ArrangingPayment') {
+            for (const line of data.order.lines) {
+                if (!line.shippingLineId) {
+                    return 'not all lines have shipping';
+                }
+            }
+        }
+    },
+};

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است