Просмотр исходного кода

feat(core): Expand the range of events published by the EventBus (#1222)

Relates to #1219 

* feat(core): Added Vendure entity event base class (#1219)

* feat(core): Implemented CustomerAddressEvent with new entity event base class (#1219)

* feat(core): Implemented AssetEvent with new entity event base class (#1219)

* fix: Removed illegal import from src

* feat(core): Added verified-event (#1219)

* feat(core): Added multiple events (#1219)

* feat(core): Added role-change and zone entity events (#1219)

* chore(core): Updated documentation of new events and added todo

Co-authored-by: Kevin <kevin@fainin.com>
Drayke 4 лет назад
Родитель
Сommit
edc9d6900b
58 измененных файлов с 1100 добавлено и 82 удалено
  1. 17 0
      packages/core/src/event-bus/events/account-verified-event.ts
  2. 27 0
      packages/core/src/event-bus/events/administrator-event.ts
  3. 25 9
      packages/core/src/event-bus/events/asset-event.ts
  4. 27 0
      packages/core/src/event-bus/events/change-channel-event.ts
  5. 27 0
      packages/core/src/event-bus/events/channel-event.ts
  6. 27 0
      packages/core/src/event-bus/events/collection-event.ts
  7. 27 0
      packages/core/src/event-bus/events/country-event.ts
  8. 24 0
      packages/core/src/event-bus/events/coupon-code-event.ts
  9. 25 6
      packages/core/src/event-bus/events/customer-address-event.ts
  10. 26 6
      packages/core/src/event-bus/events/customer-event.ts
  11. 29 0
      packages/core/src/event-bus/events/customer-group-entity-event.ts
  12. 21 0
      packages/core/src/event-bus/events/customer-group-event.ts
  13. 27 0
      packages/core/src/event-bus/events/facet-event.ts
  14. 35 0
      packages/core/src/event-bus/events/facet-value-event.ts
  15. 31 0
      packages/core/src/event-bus/events/fulfillment-event.ts
  16. 20 0
      packages/core/src/event-bus/events/global-settings-event.ts
  17. 36 0
      packages/core/src/event-bus/events/history-entry-event.ts
  18. 17 0
      packages/core/src/event-bus/events/password-reset-verified-event.ts
  19. 27 0
      packages/core/src/event-bus/events/payment-method-event.ts
  20. 22 6
      packages/core/src/event-bus/events/product-event.ts
  21. 35 0
      packages/core/src/event-bus/events/product-option-event.ts
  22. 24 0
      packages/core/src/event-bus/events/product-option-group-change-event.ts
  23. 33 0
      packages/core/src/event-bus/events/product-option-group-event.ts
  24. 22 6
      packages/core/src/event-bus/events/product-variant-event.ts
  25. 27 0
      packages/core/src/event-bus/events/promotion-event.ts
  26. 25 0
      packages/core/src/event-bus/events/role-change-event.ts
  27. 27 0
      packages/core/src/event-bus/events/role-event.ts
  28. 27 0
      packages/core/src/event-bus/events/shipping-method-event.ts
  29. 27 0
      packages/core/src/event-bus/events/tax-category-event.ts
  30. 27 0
      packages/core/src/event-bus/events/tax-rate-event.ts
  31. 1 0
      packages/core/src/event-bus/events/tax-rate-modification-event.ts
  32. 27 0
      packages/core/src/event-bus/events/zone-event.ts
  33. 24 0
      packages/core/src/event-bus/events/zone-members-event.ts
  34. 31 0
      packages/core/src/event-bus/vendure-entity-event.ts
  35. 17 0
      packages/core/src/service/services/administrator.service.ts
  36. 3 3
      packages/core/src/service/services/asset.service.ts
  37. 16 2
      packages/core/src/service/services/channel.service.ts
  38. 10 1
      packages/core/src/service/services/collection.service.ts
  39. 6 1
      packages/core/src/service/services/country.service.ts
  40. 10 2
      packages/core/src/service/services/customer-group.service.ts
  41. 14 9
      packages/core/src/service/services/customer.service.ts
  42. 7 1
      packages/core/src/service/services/facet-value.service.ts
  43. 12 2
      packages/core/src/service/services/facet.service.ts
  44. 10 3
      packages/core/src/service/services/fulfillment.service.ts
  45. 4 0
      packages/core/src/service/services/global-settings.service.ts
  46. 17 4
      packages/core/src/service/services/history.service.ts
  47. 3 0
      packages/core/src/service/services/order.service.ts
  48. 9 1
      packages/core/src/service/services/payment-method.service.ts
  49. 11 1
      packages/core/src/service/services/product-option-group.service.ts
  50. 6 1
      packages/core/src/service/services/product-option.service.ts
  51. 3 3
      packages/core/src/service/services/product-variant.service.ts
  52. 6 3
      packages/core/src/service/services/product.service.ts
  53. 8 1
      packages/core/src/service/services/promotion.service.ts
  54. 9 2
      packages/core/src/service/services/role.service.ts
  55. 12 1
      packages/core/src/service/services/shipping-method.service.ts
  56. 6 1
      packages/core/src/service/services/tax-category.service.ts
  57. 5 0
      packages/core/src/service/services/tax-rate.service.ts
  58. 22 7
      packages/core/src/service/services/zone.service.ts

+ 17 - 0
packages/core/src/event-bus/events/account-verified-event.ts

@@ -0,0 +1,17 @@
+import { RequestContext } from '../../api/common/request-context';
+import { Customer } from '../../entity/customer/customer.entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired when a users email address successfully gets verified after
+ * the `verifyCustomerAccount` mutation was executed.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class AccountVerifiedEvent extends VendureEvent {
+    constructor(public ctx: RequestContext, public customer: Customer) {
+        super();
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/administrator-event.ts

@@ -0,0 +1,27 @@
+import { CreateAdministratorInput, UpdateAdministratorInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { Administrator } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type AdministratorInputTypes = CreateAdministratorInput | UpdateAdministratorInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Administrator} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class AdministratorEvent extends VendureEntityEvent<Administrator, AdministratorInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Administrator,
+        type: 'created' | 'updated' | 'deleted',
+        input: AdministratorInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 25 - 9
packages/core/src/event-bus/events/asset-event.ts

@@ -1,21 +1,37 @@
-import { RequestContext } from '../../api/common/request-context';
+import { CreateAssetInput, DeleteAssetInput, UpdateAssetInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
 import { Asset } from '../../entity';
-import { VendureEvent } from '../vendure-event';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type AssetInputTypes = CreateAssetInput | UpdateAssetInput | DeleteAssetInput | ID;
 
 /**
  * @description
- * This event is fired whenever aa {@link Asset} is added, updated
- * or deleted.
+ * This event is fired whenever a {@link Asset} is added, updated or deleted.
  *
  * @docsCategory events
  * @docsPage Event Types
+ * @since 1.4
  */
-export class AssetEvent extends VendureEvent {
+export class AssetEvent extends VendureEntityEvent<Asset, AssetInputTypes> {
     constructor(
-        public ctx: RequestContext,
-        public asset: Asset,
-        public type: 'created' | 'updated' | 'deleted',
+        ctx: RequestContext,
+        entity: Asset,
+        type: 'created' | 'updated' | 'deleted',
+        input: AssetInputTypes,
     ) {
-        super();
+        super(entity, type, ctx, input);
+    }
+
+    /**
+     * Return an asset field to become compatible with the
+     * deprecated old version of AssetEvent
+     * @deprecated Use `entity` instead
+     * @since 1.4
+     */
+    get asset(): Asset {
+        return this.entity;
     }
 }

+ 27 - 0
packages/core/src/event-bus/events/change-channel-event.ts

@@ -0,0 +1,27 @@
+import { ID, Type } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { ChannelAware } from '../../common';
+import { VendureEntity } from '../../entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever an {@link ChannelAware} entity is assigned or removed
+ * from a channel. The entity property contains the value before updating the channels.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class ChangeChannelEvent<T extends ChannelAware & VendureEntity> extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public entity: T,
+        public channelIds: ID[],
+        public type: 'assigned' | 'removed',
+        public entityType?: Type<T>,
+    ) {
+        super();
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/channel-event.ts

@@ -0,0 +1,27 @@
+import { CreateChannelInput, UpdateChannelInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { Channel } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ChannelInputTypes = CreateChannelInput | UpdateChannelInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Channel} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class ChannelEvent extends VendureEntityEvent<Channel, ChannelInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Channel,
+        type: 'created' | 'updated' | 'deleted',
+        input: ChannelInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/collection-event.ts

@@ -0,0 +1,27 @@
+import { CreateCollectionInput, UpdateCollectionInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { Collection } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type CollectionInputTypes = CreateCollectionInput | UpdateCollectionInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Collection} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class CollectionEvent extends VendureEntityEvent<Collection, CollectionInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Collection,
+        type: 'created' | 'updated' | 'deleted',
+        input: CollectionInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/country-event.ts

@@ -0,0 +1,27 @@
+import { CreateCountryInput, UpdateCountryInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { Country } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type CountryInputTypes = CreateCountryInput | UpdateCountryInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Country} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class CountryEvent extends VendureEntityEvent<Country, CountryInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Country,
+        type: 'created' | 'updated' | 'deleted',
+        input: CountryInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 24 - 0
packages/core/src/event-bus/events/coupon-code-event.ts

@@ -0,0 +1,24 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever an coupon code of an active {@link Promotion}
+ * is assigned or removed to an {@link Order}.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class CouponCodeEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public couponCode: string,
+        public orderId: ID,
+        public type: 'assigned' | 'removed',
+    ) {
+        super();
+    }
+}

+ 25 - 6
packages/core/src/event-bus/events/customer-address-event.ts

@@ -1,21 +1,40 @@
-import { RequestContext } from '../../api/common/request-context';
+import { CreateAddressInput, UpdateAddressInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
 import { Address } from '../../entity/address/address.entity';
-import { VendureEvent } from '../vendure-event';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+/**
+ * Possible input types for Address mutations
+ */
+type CustomerAddressInputTypes = CreateAddressInput | UpdateAddressInput | ID;
 
 /**
  * @description
- * This event is fired whenever a {@link Customer} is added, updated
+ * This event is fired whenever a {@link Address} is added, updated
  * or deleted.
  *
  * @docsCategory events
  * @docsPage Event Types
+ * @since 1.4
  */
-export class CustomerAddressEvent extends VendureEvent {
+export class CustomerAddressEvent extends VendureEntityEvent<Address, CustomerAddressInputTypes> {
     constructor(
         public ctx: RequestContext,
-        public address: Address,
+        public entity: Address,
         public type: 'created' | 'updated' | 'deleted',
+        public input: CustomerAddressInputTypes,
     ) {
-        super();
+        super(entity, type, ctx, input);
+    }
+
+    /**
+     * Return an address field to become compatible with the
+     * deprecated old version of CustomerAddressEvent
+     * @deprecated Use `entity` instead
+     */
+    get address(): Address {
+        return this.entity;
     }
 }

+ 26 - 6
packages/core/src/event-bus/events/customer-event.ts

@@ -1,6 +1,15 @@
+import { CreateCustomerInput, UpdateCustomerInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
 import { RequestContext } from '../../api/common/request-context';
 import { Customer } from '../../entity/customer/customer.entity';
-import { VendureEvent } from '../vendure-event';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type CustomerInputTypes =
+    | CreateCustomerInput
+    | UpdateCustomerInput
+    | (Partial<CreateCustomerInput> & { emailAddress: string })
+    | ID;
 
 /**
  * @description
@@ -10,12 +19,23 @@ import { VendureEvent } from '../vendure-event';
  * @docsCategory events
  * @docsPage Event Types
  */
-export class CustomerEvent extends VendureEvent {
+export class CustomerEvent extends VendureEntityEvent<Customer, CustomerInputTypes> {
     constructor(
-        public ctx: RequestContext,
-        public customer: Customer,
-        public type: 'created' | 'updated' | 'deleted',
+        ctx: RequestContext,
+        entity: Customer,
+        type: 'created' | 'updated' | 'deleted',
+        input: CustomerInputTypes,
     ) {
-        super();
+        super(entity, type, ctx, input);
+    }
+
+    /**
+     * Return an customer field to become compatible with the
+     * deprecated old version of CustomerEvent
+     * @deprecated Use `entity` instead
+     * @since 1.4
+     */
+    get customer(): Customer {
+        return this.entity;
     }
 }

+ 29 - 0
packages/core/src/event-bus/events/customer-group-entity-event.ts

@@ -0,0 +1,29 @@
+import { CreateCustomerGroupInput, UpdateCustomerGroupInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { CustomerGroup } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type CustomerGroupInputTypes = CreateCustomerGroupInput | UpdateCustomerGroupInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link CustomerGroup} is added, updated or deleted.
+ * Use this event instead of {@link CustomerGroupEvent} until the next major version!
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class CustomerGroupEntityEvent extends VendureEntityEvent<CustomerGroup, CustomerGroupInputTypes> {
+    // TODO: Rename to CustomerGroupEvent in v2
+    constructor(
+        ctx: RequestContext,
+        entity: CustomerGroup,
+        type: 'created' | 'updated' | 'deleted',
+        input: CustomerGroupInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 21 - 0
packages/core/src/event-bus/events/customer-group-event.ts

@@ -10,6 +10,7 @@ import { VendureEvent } from '../vendure-event';
  *
  * @docsCategory events
  * @docsPage Event Types
+ * @deprecated Use {@link CustomerGroupChangeEvent} instead
  */
 export class CustomerGroupEvent extends VendureEvent {
     constructor(
@@ -21,3 +22,23 @@ export class CustomerGroupEvent extends VendureEvent {
         super();
     }
 }
+
+/**
+ * @description
+ * This event is fired whenever one or more {@link Customer} is assigned to or removed from a
+ * {@link CustomerGroup}.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class CustomerGroupChangeEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public customers: Customer[],
+        public customGroup: CustomerGroup,
+        public type: 'assigned' | 'removed',
+    ) {
+        super();
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/facet-event.ts

@@ -0,0 +1,27 @@
+import { CreateFacetInput, UpdateFacetInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { Facet } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type FacetInputTypes = CreateFacetInput | UpdateFacetInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Facet} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class FacetEvent extends VendureEntityEvent<Facet, FacetInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Facet,
+        type: 'created' | 'updated' | 'deleted',
+        input: FacetInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 35 - 0
packages/core/src/event-bus/events/facet-value-event.ts

@@ -0,0 +1,35 @@
+import {
+    CreateFacetValueInput,
+    CreateFacetValueWithFacetInput,
+    UpdateFacetValueInput,
+} from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api';
+import { FacetValue } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type FacetValueInputTypes =
+    | CreateFacetValueInput
+    | CreateFacetValueWithFacetInput
+    | UpdateFacetValueInput
+    | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link FacetValue} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class FacetValueEvent extends VendureEntityEvent<FacetValue, FacetValueInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: FacetValue,
+        type: 'created' | 'updated' | 'deleted',
+        input: FacetValueInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 31 - 0
packages/core/src/event-bus/events/fulfillment-event.ts

@@ -0,0 +1,31 @@
+import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
+
+import { RequestContext } from '../../api';
+import { Order, OrderItem } from '../../entity';
+import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+/**
+ * @description
+ * The inputs used to create a new fulfillment
+ * @since 1.4
+ */
+type CreateFulfillmentInput = {
+    orders: Order[];
+    items: OrderItem[];
+    handler: ConfigurableOperationInput;
+};
+
+/**
+ * @description
+ * This event is fired whenever a {@link Fulfillment} is added. The type is always `created`.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class FulfillmentEvent extends VendureEntityEvent<Fulfillment, CreateFulfillmentInput> {
+    constructor(ctx: RequestContext, entity: Fulfillment, input: CreateFulfillmentInput) {
+        super(entity, 'created', ctx, input);
+    }
+}

+ 20 - 0
packages/core/src/event-bus/events/global-settings-event.ts

@@ -0,0 +1,20 @@
+import { UpdateGlobalSettingsInput } from '@vendure/common/lib/generated-types';
+
+import { RequestContext } from '../../api';
+import { GlobalSettings } from '../../entity/global-settings/global-settings.entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+/**
+ * @description
+ * This event is fired whenever a {@link GlobalSettings} is added. The type is always `updated`, because it's
+ * only created once and never deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class GlobalSettingsEvent extends VendureEntityEvent<GlobalSettings, UpdateGlobalSettingsInput> {
+    constructor(ctx: RequestContext, entity: GlobalSettings, input: UpdateGlobalSettingsInput) {
+        super(entity, 'updated', ctx, input);
+    }
+}

+ 36 - 0
packages/core/src/event-bus/events/history-entry-event.ts

@@ -0,0 +1,36 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type HistoryInput =
+    | {
+          type: HistoryEntryType;
+          data?: any;
+      }
+    | ID;
+
+/**
+ * @description
+ * This event is fired whenever one {@link HistoryEntry} is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class HistoryEntryEvent extends VendureEntityEvent<HistoryEntry, HistoryInput> {
+    public readonly historyType: 'order' | 'customer' | string;
+
+    constructor(
+        ctx: RequestContext,
+        entity: HistoryEntry,
+        type: 'created' | 'updated' | 'deleted',
+        historyType: 'order' | 'customer' | string,
+        input: HistoryInput,
+    ) {
+        super(entity, type, ctx, input);
+        this.historyType = historyType;
+    }
+}

+ 17 - 0
packages/core/src/event-bus/events/password-reset-verified-event.ts

@@ -0,0 +1,17 @@
+import { RequestContext } from '../../api/common/request-context';
+import { User } from '../../entity/user/user.entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired when a password reset is executed with a verified token.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class PasswordResetVerifiedEvent extends VendureEvent {
+    constructor(public ctx: RequestContext, public user: User) {
+        super();
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/payment-method-event.ts

@@ -0,0 +1,27 @@
+import { CreatePaymentMethodInput, UpdatePaymentMethodInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { PaymentMethod } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type PaymentMethodInputTypes = CreatePaymentMethodInput | UpdatePaymentMethodInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link PaymentMethod} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class PaymentMethodEvent extends VendureEntityEvent<PaymentMethod, PaymentMethodInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: PaymentMethod,
+        type: 'created' | 'updated' | 'deleted',
+        input: PaymentMethodInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 22 - 6
packages/core/src/event-bus/events/product-event.ts

@@ -1,6 +1,11 @@
+import { CreateProductInput, UpdateProductInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
 import { RequestContext } from '../../api/common/request-context';
 import { Product } from '../../entity';
-import { VendureEvent } from '../vendure-event';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ProductInputTypes = CreateProductInput | UpdateProductInput | ID;
 
 /**
  * @description
@@ -10,12 +15,23 @@ import { VendureEvent } from '../vendure-event';
  * @docsCategory events
  * @docsPage Event Types
  */
-export class ProductEvent extends VendureEvent {
+export class ProductEvent extends VendureEntityEvent<Product, ProductInputTypes> {
     constructor(
-        public ctx: RequestContext,
-        public product: Product,
-        public type: 'created' | 'updated' | 'deleted',
+        ctx: RequestContext,
+        entity: Product,
+        type: 'created' | 'updated' | 'deleted',
+        input: ProductInputTypes,
     ) {
-        super();
+        super(entity, type, ctx, input);
+    }
+
+    /**
+     * Return an product field to become compatible with the
+     * deprecated old version of ProductEvent
+     * @deprecated Use `entity` instead
+     * @since 1.4
+     */
+    get product(): Product {
+        return this.entity;
     }
 }

+ 35 - 0
packages/core/src/event-bus/events/product-option-event.ts

@@ -0,0 +1,35 @@
+import {
+    CreateGroupOptionInput,
+    CreateProductOptionInput,
+    UpdateProductOptionInput,
+} from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { ProductOption, ProductOptionGroup } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ProductOptionInputTypes =
+    | CreateGroupOptionInput
+    | CreateProductOptionInput
+    | UpdateProductOptionInput
+    | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link ProductOption} is added or updated.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class ProductOptionEvent extends VendureEntityEvent<ProductOption, ProductOptionInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: ProductOption,
+        type: 'created' | 'updated',
+        input: ProductOptionInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 24 - 0
packages/core/src/event-bus/events/product-option-group-change-event.ts

@@ -0,0 +1,24 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Product } from '../../entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever a {@link ProductOptionGroup} is assigned or removed from a {@link Product}.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class ProductOptionGroupChangeEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public product: Product,
+        public optionGroupId: ID,
+        public type: 'assigned' | 'removed',
+    ) {
+        super();
+    }
+}

+ 33 - 0
packages/core/src/event-bus/events/product-option-group-event.ts

@@ -0,0 +1,33 @@
+import {
+    CreateProductOptionGroupInput,
+    UpdateProductOptionGroupInput,
+} from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { ProductOptionGroup } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ProductOptionGroupInputTypes = CreateProductOptionGroupInput | UpdateProductOptionGroupInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link ProductOptionGroup} is added or updated.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class ProductOptionGroupEvent extends VendureEntityEvent<
+    ProductOptionGroup,
+    ProductOptionGroupInputTypes
+> {
+    constructor(
+        ctx: RequestContext,
+        entity: ProductOptionGroup,
+        type: 'created' | 'updated',
+        input: ProductOptionGroupInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 22 - 6
packages/core/src/event-bus/events/product-variant-event.ts

@@ -1,6 +1,11 @@
+import { CreateProductVariantInput, UpdateProductVariantInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
 import { RequestContext } from '../../api/common/request-context';
 import { ProductVariant } from '../../entity';
-import { VendureEvent } from '../vendure-event';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ProductVariantInputTypes = CreateProductVariantInput[] | UpdateProductVariantInput[] | ID | ID[];
 
 /**
  * @description
@@ -10,12 +15,23 @@ import { VendureEvent } from '../vendure-event';
  * @docsCategory events
  * @docsPage Event Types
  */
-export class ProductVariantEvent extends VendureEvent {
+export class ProductVariantEvent extends VendureEntityEvent<ProductVariant[], ProductVariantInputTypes> {
     constructor(
-        public ctx: RequestContext,
-        public variants: ProductVariant[],
-        public type: 'created' | 'updated' | 'deleted',
+        ctx: RequestContext,
+        entity: ProductVariant[],
+        type: 'created' | 'updated' | 'deleted',
+        input: ProductVariantInputTypes,
     ) {
-        super();
+        super(entity, type, ctx, input);
+    }
+
+    /**
+     * Return an variants field to become compatible with the
+     * deprecated old version of ProductEvent
+     * @deprecated Use `entity` instead
+     * @since 1.4
+     */
+    get variants(): ProductVariant[] {
+        return this.entity;
     }
 }

+ 27 - 0
packages/core/src/event-bus/events/promotion-event.ts

@@ -0,0 +1,27 @@
+import { CreatePromotionInput, UpdatePromotionInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Promotion } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type PromotionInputTypes = CreatePromotionInput | UpdatePromotionInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Promotion} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class PromotionEvent extends VendureEntityEvent<Promotion, PromotionInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Promotion,
+        type: 'created' | 'updated' | 'deleted',
+        input: PromotionInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 25 - 0
packages/core/src/event-bus/events/role-change-event.ts

@@ -0,0 +1,25 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Administrator, Role } from '../../entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever one {@link Role} is assigned or removed from a user.
+ * The property `roleIds` only contains the removed or assigned role ids.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class RoleChangeEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public admin: Administrator,
+        public roleIds: ID[],
+        public type: 'assigned' | 'removed',
+    ) {
+        super();
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/role-event.ts

@@ -0,0 +1,27 @@
+import { CreateRoleInput, UpdateRoleInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Role } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type RoleInputTypes = CreateRoleInput | UpdateRoleInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever one {@link Role} is  is added, updated or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ * @since 1.4
+ */
+export class RoleEvent extends VendureEntityEvent<Role, RoleInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Role,
+        type: 'created' | 'updated' | 'deleted',
+        input: RoleInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/shipping-method-event.ts

@@ -0,0 +1,27 @@
+import { CreateShippingMethodInput, UpdateShippingMethodInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { ShippingMethod } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ShippingMethodInputTypes = CreateShippingMethodInput | UpdateShippingMethodInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link ShippingMethod} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class ShippingMethodEvent extends VendureEntityEvent<ShippingMethod, ShippingMethodInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: ShippingMethod,
+        type: 'created' | 'updated' | 'deleted',
+        input: ShippingMethodInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/tax-category-event.ts

@@ -0,0 +1,27 @@
+import { CreateTaxCategoryInput, UpdateTaxCategoryInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { TaxCategory } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type TaxCategoryInputTypes = CreateTaxCategoryInput | UpdateTaxCategoryInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link TaxCategory} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class TaxCategoryEvent extends VendureEntityEvent<TaxCategory, TaxCategoryInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: TaxCategory,
+        type: 'created' | 'updated' | 'deleted',
+        input: TaxCategoryInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 27 - 0
packages/core/src/event-bus/events/tax-rate-event.ts

@@ -0,0 +1,27 @@
+import { CreateTaxRateInput, UpdateTaxRateInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { TaxRate } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type TaxRateInputTypes = CreateTaxRateInput | UpdateTaxRateInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link TaxRate} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class TaxRateEvent extends VendureEntityEvent<TaxRate, TaxRateInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: TaxRate,
+        type: 'created' | 'updated' | 'deleted',
+        input: TaxRateInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 1 - 0
packages/core/src/event-bus/events/tax-rate-modification-event.ts

@@ -8,6 +8,7 @@ import { VendureEvent } from '../vendure-event';
  *
  * @docsCategory events
  * @docsPage Event Types
+ * @deprecated Use TaxRateEvent instead
  */
 export class TaxRateModificationEvent extends VendureEvent {
     constructor(public ctx: RequestContext, public taxRate: TaxRate) {

+ 27 - 0
packages/core/src/event-bus/events/zone-event.ts

@@ -0,0 +1,27 @@
+import { CreateZoneInput, UpdateZoneInput } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Zone } from '../../entity';
+import { VendureEntityEvent } from '../vendure-entity-event';
+
+type ZoneInputTypes = CreateZoneInput | UpdateZoneInput | ID;
+
+/**
+ * @description
+ * This event is fired whenever a {@link Zone} is added, updated
+ * or deleted.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class ZoneEvent extends VendureEntityEvent<Zone, ZoneInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: Zone,
+        type: 'created' | 'updated' | 'deleted',
+        input: ZoneInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}

+ 24 - 0
packages/core/src/event-bus/events/zone-members-event.ts

@@ -0,0 +1,24 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { Zone } from '../../entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever a {@link Zone} gets {@link Country} members assigned or removed
+ * The `entity` property contains the zone with the already updated member field.
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class ZoneMembersEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public entity: Zone,
+        public type: 'assigned' | 'removed',
+        public memberIds: ID[],
+    ) {
+        super();
+    }
+}

+ 31 - 0
packages/core/src/event-bus/vendure-entity-event.ts

@@ -0,0 +1,31 @@
+import { RequestContext } from '../api';
+
+import { VendureEvent } from './vendure-event';
+
+/**
+ * @description
+ * The base class for all entity events used by the EventBus system.
+ * * For event type `'updated'` the entity is the one before applying the patch (if not documented otherwise).
+ * * For event type `'deleted'` the input will most likely be an `id: ID`
+ *
+ * @docsCategory events
+ * */
+export abstract class VendureEntityEvent<Entity, Input = any> extends VendureEvent {
+    public readonly entity: Entity;
+    public readonly type: 'created' | 'updated' | 'deleted';
+    public readonly ctx: RequestContext;
+    public readonly input: Input;
+
+    protected constructor(
+        entity: Entity,
+        type: 'created' | 'updated' | 'deleted',
+        ctx: RequestContext,
+        input: Input,
+    ) {
+        super();
+        this.entity = entity;
+        this.type = type;
+        this.ctx = ctx;
+        this.input = input;
+    }
+}

+ 17 - 0
packages/core/src/service/services/administrator.service.ts

@@ -14,6 +14,9 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
 import { User } from '../../entity/user/user.entity';
+import { EventBus } from '../../event-bus';
+import { AdministratorEvent } from '../../event-bus/events/administrator-event';
+import { RoleChangeEvent } from '../../event-bus/events/role-change-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PasswordCipher } from '../helpers/password-cipher/password-cipher';
@@ -38,6 +41,7 @@ export class AdministratorService {
         private userService: UserService,
         private roleService: RoleService,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     /** @internal */
@@ -111,6 +115,7 @@ export class AdministratorService {
             input,
             createdAdministrator,
         );
+        this.eventBus.publish(new AdministratorEvent(ctx, createdAdministrator, 'created', input));
         return createdAdministrator;
     }
 
@@ -139,11 +144,21 @@ export class AdministratorService {
             }
         }
         if (input.roleIds) {
+            const removeIds = administrator.user.roles
+                .map(role => role.id)
+                .filter(roleId => (input.roleIds as ID[]).indexOf(roleId) === -1);
+
+            const addIds = (input.roleIds as ID[]).filter(
+                roleId => !administrator.user.roles.some(role => role.id === roleId),
+            );
+
             administrator.user.roles = [];
             await this.connection.getRepository(ctx, User).save(administrator.user, { reload: false });
             for (const roleId of input.roleIds) {
                 updatedAdministrator = await this.assignRole(ctx, administrator.id, roleId);
             }
+            this.eventBus.publish(new RoleChangeEvent(ctx, administrator, addIds, 'assigned'));
+            this.eventBus.publish(new RoleChangeEvent(ctx, administrator, removeIds, 'removed'));
         }
         await this.customFieldRelationService.updateRelations(
             ctx,
@@ -151,6 +166,7 @@ export class AdministratorService {
             input,
             updatedAdministrator,
         );
+        this.eventBus.publish(new AdministratorEvent(ctx, administrator, 'updated', input));
         return updatedAdministrator;
     }
 
@@ -183,6 +199,7 @@ export class AdministratorService {
         await this.connection.getRepository(ctx, Administrator).update({ id }, { deletedAt: new Date() });
         // tslint:disable-next-line:no-non-null-assertion
         await this.userService.softDelete(ctx, administrator.user!.id);
+        this.eventBus.publish(new AdministratorEvent(ctx, administrator, 'deleted', id));
         return {
             result: DeletionResult.DELETED,
         };

+ 3 - 3
packages/core/src/service/services/asset.service.ts

@@ -266,7 +266,7 @@ export class AssetService {
                 result.tags = tags;
                 await this.connection.getRepository(ctx, Asset).save(result);
             }
-            this.eventBus.publish(new AssetEvent(ctx, result, 'created'));
+            this.eventBus.publish(new AssetEvent(ctx, result, 'created', input));
             resolve(result);
         });
     }
@@ -284,7 +284,7 @@ export class AssetService {
             asset.tags = await this.tagService.valuesToTags(ctx, input.tags);
         }
         const updatedAsset = await this.connection.getRepository(ctx, Asset).save(asset);
-        this.eventBus.publish(new AssetEvent(ctx, updatedAsset, 'updated'));
+        this.eventBus.publish(new AssetEvent(ctx, updatedAsset, 'updated', input));
         return updatedAsset;
     }
 
@@ -419,7 +419,7 @@ export class AssetService {
             } catch (e) {
                 Logger.error(`error.could-not-delete-asset-file`, undefined, e.stack);
             }
-            this.eventBus.publish(new AssetEvent(ctx, deletedAsset, 'deleted'));
+            this.eventBus.publish(new AssetEvent(ctx, deletedAsset, 'deleted', deletedAsset.id));
         }
         return {
             result: DeletionResult.DELETED,

+ 16 - 2
packages/core/src/service/services/channel.service.ts

@@ -26,6 +26,9 @@ import { Channel } from '../../entity/channel/channel.entity';
 import { ProductVariantPrice } from '../../entity/product-variant/product-variant-price.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 { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -46,6 +49,7 @@ export class ChannelService {
         private configService: ConfigService,
         private globalSettingsService: GlobalSettingsService,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     /**
@@ -68,10 +72,14 @@ export class ChannelService {
      * Assigns a ChannelAware entity to the default Channel as well as any channel
      * specified in the RequestContext.
      */
-    async assignToCurrentChannel<T extends ChannelAware>(entity: T, ctx: RequestContext): Promise<T> {
+    async assignToCurrentChannel<T extends ChannelAware & VendureEntity>(
+        entity: T,
+        ctx: RequestContext,
+    ): Promise<T> {
         const defaultChannel = await this.getDefaultChannel();
         const channelIds = unique([ctx.channelId, defaultChannel.id]);
         entity.channels = channelIds.map(id => ({ id })) as any;
+        this.eventBus.publish(new ChangeChannelEvent(ctx, entity, [ctx.channelId], 'assigned'));
         return entity;
     }
 
@@ -93,6 +101,7 @@ export class ChannelService {
             entity.channels.push(channel);
         }
         await this.connection.getRepository(ctx, entityType).save(entity as any, { reload: false });
+        this.eventBus.publish(new ChangeChannelEvent(ctx, entity, channelIds, 'assigned', entityType));
         return entity;
     }
 
@@ -116,6 +125,7 @@ export class ChannelService {
             entity.channels = entity.channels.filter(c => !idsAreEqual(c.id, id));
         }
         await this.connection.getRepository(ctx, entityType).save(entity as any, { reload: false });
+        this.eventBus.publish(new ChangeChannelEvent(ctx, entity, channelIds, 'removed', entityType));
         return entity;
     }
 
@@ -189,6 +199,7 @@ export class ChannelService {
         const newChannel = await this.connection.getRepository(ctx, Channel).save(channel);
         await this.customFieldRelationService.updateRelations(ctx, Channel, input, newChannel);
         await this.allChannels.refresh(ctx);
+        this.eventBus.publish(new ChannelEvent(ctx, newChannel, 'created', input));
         return channel;
     }
 
@@ -222,16 +233,19 @@ export class ChannelService {
         await this.connection.getRepository(ctx, Channel).save(updatedChannel, { reload: false });
         await this.customFieldRelationService.updateRelations(ctx, Channel, input, updatedChannel);
         await this.allChannels.refresh(ctx);
+        this.eventBus.publish(new ChannelEvent(ctx, channel, 'updated', input));
         return assertFound(this.findOne(ctx, channel.id));
     }
 
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
-        await this.connection.getEntityOrThrow(ctx, Channel, id);
+        const channel = await this.connection.getEntityOrThrow(ctx, Channel, id);
         await this.connection.getRepository(ctx, Session).delete({ activeChannelId: id });
         await this.connection.getRepository(ctx, Channel).delete(id);
         await this.connection.getRepository(ctx, ProductVariantPrice).delete({
             channelId: id,
         });
+        this.eventBus.publish(new ChannelEvent(ctx, channel, 'deleted', id));
+
         return {
             result: DeletionResult.DELETED,
         };

+ 10 - 1
packages/core/src/service/services/collection.service.ts

@@ -27,6 +27,7 @@ import { CollectionTranslation } from '../../entity/collection/collection-transl
 import { Collection } from '../../entity/collection/collection.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { EventBus } from '../../event-bus/event-bus';
+import { CollectionEvent } from '../../event-bus/events/collection-event';
 import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
@@ -369,11 +370,17 @@ export class CollectionService implements OnModuleInit {
             },
         });
         await this.assetService.updateEntityAssets(ctx, collection, input);
-        await this.customFieldRelationService.updateRelations(ctx, Collection, input, collection);
+        const collectionWithRelations = await this.customFieldRelationService.updateRelations(
+            ctx,
+            Collection,
+            input,
+            collection,
+        );
         await this.applyFiltersQueue.add({
             ctx: ctx.serialize(),
             collectionIds: [collection.id],
         });
+        this.eventBus.publish(new CollectionEvent(ctx, collectionWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, collection.id));
     }
 
@@ -403,6 +410,7 @@ export class CollectionService implements OnModuleInit {
             const affectedVariantIds = await this.getCollectionProductVariantIds(collection);
             this.eventBus.publish(new CollectionModificationEvent(ctx, collection, affectedVariantIds));
         }
+        this.eventBus.publish(new CollectionEvent(ctx, collection, 'updated', input));
         return assertFound(this.findOne(ctx, collection.id));
     }
 
@@ -416,6 +424,7 @@ export class CollectionService implements OnModuleInit {
             await this.connection.getRepository(ctx, Collection).remove(coll);
             this.eventBus.publish(new CollectionModificationEvent(ctx, coll, affectedVariantIds));
         }
+        this.eventBus.publish(new CollectionEvent(ctx, collection, 'deleted', id));
         return {
             result: DeletionResult.DELETED,
         };

+ 6 - 1
packages/core/src/service/services/country.service.ts

@@ -16,6 +16,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Address } from '../../entity';
 import { CountryTranslation } from '../../entity/country/country-translation.entity';
 import { Country } from '../../entity/country/country.entity';
+import { EventBus } from '../../event-bus';
+import { CountryEvent } from '../../event-bus/events/country-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -34,7 +36,7 @@ export class CountryService {
         private connection: TransactionalConnection,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
-        private zoneService: ZoneService,
+        private eventBus: EventBus,
     ) {}
 
     findAll(
@@ -94,6 +96,7 @@ export class CountryService {
             entityType: Country,
             translationType: CountryTranslation,
         });
+        this.eventBus.publish(new CountryEvent(ctx, country, 'created', input));
         return assertFound(this.findOne(ctx, country.id));
     }
 
@@ -104,6 +107,7 @@ export class CountryService {
             entityType: Country,
             translationType: CountryTranslation,
         });
+        this.eventBus.publish(new CountryEvent(ctx, country, 'updated', input));
         return assertFound(this.findOne(ctx, country.id));
     }
 
@@ -122,6 +126,7 @@ export class CountryService {
             };
         } else {
             await this.connection.getRepository(ctx, Country).remove(country);
+            this.eventBus.publish(new CountryEvent(ctx, country, 'deleted', id));
             return {
                 result: DeletionResult.DELETED,
                 message: '',

+ 10 - 2
packages/core/src/service/services/customer-group.service.ts

@@ -19,7 +19,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { EventBus } from '../../event-bus/event-bus';
-import { CustomerGroupEvent } from '../../event-bus/events/customer-group-event';
+import { CustomerGroupEntityEvent } from '../../event-bus/events/customer-group-entity-event';
+import { CustomerGroupChangeEvent, CustomerGroupEvent } from '../../event-bus/events/customer-group-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -89,13 +90,16 @@ export class CustomerGroupService {
             }
             await this.connection.getRepository(ctx, Customer).save(customers);
         }
-        return assertFound(this.findOne(ctx, newCustomerGroup.id));
+        const savedCustomerGroup = await assertFound(this.findOne(ctx, newCustomerGroup.id));
+        this.eventBus.publish(new CustomerGroupEntityEvent(ctx, savedCustomerGroup, 'created', input));
+        return savedCustomerGroup;
     }
 
     async update(ctx: RequestContext, input: UpdateCustomerGroupInput): Promise<CustomerGroup> {
         const customerGroup = await this.connection.getEntityOrThrow(ctx, CustomerGroup, input.id);
         const updatedCustomerGroup = patchEntity(customerGroup, input);
         await this.connection.getRepository(ctx, CustomerGroup).save(updatedCustomerGroup, { reload: false });
+        this.eventBus.publish(new CustomerGroupEntityEvent(ctx, customerGroup, 'updated', input));
         return assertFound(this.findOne(ctx, customerGroup.id));
     }
 
@@ -103,6 +107,7 @@ export class CustomerGroupService {
         const group = await this.connection.getEntityOrThrow(ctx, CustomerGroup, id);
         try {
             await this.connection.getRepository(ctx, CustomerGroup).remove(group);
+            this.eventBus.publish(new CustomerGroupEntityEvent(ctx, group, 'deleted', id));
             return {
                 result: DeletionResult.DELETED,
             };
@@ -136,6 +141,8 @@ export class CustomerGroupService {
 
         await this.connection.getRepository(ctx, Customer).save(customers, { reload: false });
         this.eventBus.publish(new CustomerGroupEvent(ctx, customers, group, 'assigned'));
+        this.eventBus.publish(new CustomerGroupChangeEvent(ctx, customers, group, 'assigned'));
+
         return assertFound(this.findOne(ctx, group.id));
     }
 
@@ -161,6 +168,7 @@ export class CustomerGroupService {
         }
         await this.connection.getRepository(ctx, Customer).save(customers, { reload: false });
         this.eventBus.publish(new CustomerGroupEvent(ctx, customers, group, 'removed'));
+        this.eventBus.publish(new CustomerGroupChangeEvent(ctx, customers, group, 'removed'));
         return assertFound(this.findOne(ctx, group.id));
     }
 

+ 14 - 9
packages/core/src/service/services/customer.service.ts

@@ -46,11 +46,13 @@ import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
 import { User } from '../../entity/user/user.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
+import { AccountVerifiedEvent } from '../../event-bus/events/account-verified-event';
 import { CustomerAddressEvent } from '../../event-bus/events/customer-address-event';
 import { CustomerEvent } from '../../event-bus/events/customer-event';
 import { IdentifierChangeEvent } from '../../event-bus/events/identifier-change-event';
 import { IdentifierChangeRequestEvent } from '../../event-bus/events/identifier-change-request-event';
 import { PasswordResetEvent } from '../../event-bus/events/password-reset-event';
+import { PasswordResetVerifiedEvent } from '../../event-bus/events/password-reset-verified-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { addressToLine } from '../helpers/utils/address-to-line';
@@ -261,7 +263,7 @@ export class CustomerService {
                 },
             });
         }
-        this.eventBus.publish(new CustomerEvent(ctx, createdCustomer, 'created'));
+        this.eventBus.publish(new CustomerEvent(ctx, createdCustomer, 'created', input));
         return createdCustomer;
     }
 
@@ -326,7 +328,7 @@ export class CustomerService {
                 input,
             },
         });
-        this.eventBus.publish(new CustomerEvent(ctx, customer, 'updated'));
+        this.eventBus.publish(new CustomerEvent(ctx, customer, 'updated', input));
         return assertFound(this.findOne(ctx, customer.id));
     }
 
@@ -459,7 +461,9 @@ export class CustomerService {
                 strategy: NATIVE_AUTH_STRATEGY_NAME,
             },
         });
-        return assertFound(this.findOneByUserId(ctx, result.id));
+        const user = assertFound(this.findOneByUserId(ctx, result.id));
+        this.eventBus.publish(new AccountVerifiedEvent(ctx, customer));
+        return user;
     }
 
     /**
@@ -508,6 +512,7 @@ export class CustomerService {
             type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED,
             data: {},
         });
+        this.eventBus.publish(new PasswordResetVerifiedEvent(ctx, result));
         return result;
     }
 
@@ -636,7 +641,7 @@ export class CustomerService {
         } else {
             customer = await this.connection.getRepository(ctx, Customer).save(new Customer(input));
             await this.channelService.assignToCurrentChannel(customer, ctx);
-            this.eventBus.publish(new CustomerEvent(ctx, customer, 'created'));
+            this.eventBus.publish(new CustomerEvent(ctx, customer, 'created', input));
         }
         return this.connection.getRepository(ctx, Customer).save(customer);
     }
@@ -668,7 +673,7 @@ export class CustomerService {
             type: HistoryEntryType.CUSTOMER_ADDRESS_CREATED,
             data: { address: addressToLine(createdAddress) },
         });
-        this.eventBus.publish(new CustomerAddressEvent(ctx, createdAddress, 'created'));
+        this.eventBus.publish(new CustomerAddressEvent(ctx, createdAddress, 'created', input));
         return createdAddress;
     }
 
@@ -676,7 +681,7 @@ export class CustomerService {
         const address = await this.connection.getEntityOrThrow(ctx, Address, input.id, {
             relations: ['customer', 'country'],
         });
-        const customer = await this.connection.findOneInChannel(
+        const customer = await this.connection.findOneInChannel<Customer>(
             ctx,
             Customer,
             address.customer.id,
@@ -704,7 +709,7 @@ export class CustomerService {
                 input,
             },
         });
-        this.eventBus.publish(new CustomerAddressEvent(ctx, updatedAddress, 'updated'));
+        this.eventBus.publish(new CustomerAddressEvent(ctx, updatedAddress, 'updated', input));
         return updatedAddress;
     }
 
@@ -732,7 +737,7 @@ export class CustomerService {
             },
         });
         await this.connection.getRepository(ctx, Address).remove(address);
-        this.eventBus.publish(new CustomerAddressEvent(ctx, address, 'deleted'));
+        this.eventBus.publish(new CustomerAddressEvent(ctx, address, 'deleted', id));
         return true;
     }
 
@@ -745,7 +750,7 @@ export class CustomerService {
             .update({ id: customerId }, { deletedAt: new Date() });
         // tslint:disable-next-line:no-non-null-assertion
         await this.userService.softDelete(ctx, customer.user!.id);
-        this.eventBus.publish(new CustomerEvent(ctx, customer, 'deleted'));
+        this.eventBus.publish(new CustomerEvent(ctx, customer, 'deleted', customerId));
         return {
             result: DeletionResult.DELETED,
         };

+ 7 - 1
packages/core/src/service/services/facet-value.service.ts

@@ -18,6 +18,8 @@ import { Product, ProductVariant } from '../../entity';
 import { FacetValueTranslation } from '../../entity/facet-value/facet-value-translation.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { Facet } from '../../entity/facet/facet.entity';
+import { EventBus } from '../../event-bus';
+import { FacetValueEvent } from '../../event-bus/events/facet-value-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -38,6 +40,7 @@ export class FacetValueService {
         private configService: ConfigService,
         private customFieldRelationService: CustomFieldRelationService,
         private channelService: ChannelService,
+        private eventBus: EventBus,
     ) {}
 
     findAll(lang: LanguageCode): Promise<Array<Translated<FacetValue>>> {
@@ -97,12 +100,13 @@ export class FacetValueService {
                 await this.channelService.assignToCurrentChannel(fv, ctx);
             },
         });
-        await this.customFieldRelationService.updateRelations(
+        const facetValueWithRelations = await this.customFieldRelationService.updateRelations(
             ctx,
             FacetValue,
             input as CreateFacetValueInput,
             facetValue,
         );
+        this.eventBus.publish(new FacetValueEvent(ctx, facetValueWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, facetValue.id));
     }
 
@@ -114,6 +118,7 @@ export class FacetValueService {
             translationType: FacetValueTranslation,
         });
         await this.customFieldRelationService.updateRelations(ctx, FacetValue, input, facetValue);
+        this.eventBus.publish(new FacetValueEvent(ctx, facetValue, 'updated', input));
         return assertFound(this.findOne(ctx, facetValue.id));
     }
 
@@ -133,6 +138,7 @@ export class FacetValueService {
         } else if (force) {
             const facetValue = await this.connection.getEntityOrThrow(ctx, FacetValue, id);
             await this.connection.getRepository(ctx, FacetValue).remove(facetValue);
+            this.eventBus.publish(new FacetValueEvent(ctx, facetValue, 'deleted', id));
             message = ctx.translate('message.facet-value-force-deleted', i18nVars);
             result = DeletionResult.DELETED;
         } else {

+ 12 - 2
packages/core/src/service/services/facet.service.ts

@@ -6,7 +6,6 @@ import {
     LanguageCode,
     UpdateFacetInput,
 } from '@vendure/common/lib/generated-types';
-import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -17,6 +16,8 @@ import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { FacetTranslation } from '../../entity/facet/facet-translation.entity';
 import { Facet } from '../../entity/facet/facet.entity';
+import { EventBus } from '../../event-bus';
+import { FacetEvent } from '../../event-bus/events/facet-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
@@ -41,6 +42,7 @@ export class FacetService {
         private configService: ConfigService,
         private channelService: ChannelService,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     findAll(
@@ -112,7 +114,13 @@ export class FacetService {
                 await this.channelService.assignToCurrentChannel(f, ctx);
             },
         });
-        await this.customFieldRelationService.updateRelations(ctx, Facet, input, facet);
+        const facetWithRelations = await this.customFieldRelationService.updateRelations(
+            ctx,
+            Facet,
+            input,
+            facet,
+        );
+        this.eventBus.publish(new FacetEvent(ctx, facetWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, facet.id));
     }
 
@@ -127,6 +135,7 @@ export class FacetService {
             },
         });
         await this.customFieldRelationService.updateRelations(ctx, Facet, input, facet);
+        this.eventBus.publish(new FacetEvent(ctx, facet, 'updated', input));
         return assertFound(this.findOne(ctx, facet.id));
     }
 
@@ -159,6 +168,7 @@ export class FacetService {
             await this.connection.getRepository(ctx, Facet).remove(facet);
             message = ctx.translate('message.facet-force-deleted', i18nVars);
             result = DeletionResult.DELETED;
+            this.eventBus.publish(new FacetEvent(ctx, facet, 'deleted', id));
         } else {
             message = ctx.translate('message.facet-used', i18nVars);
             result = DeletionResult.NOT_DELETED;

+ 10 - 3
packages/core/src/service/services/fulfillment.service.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
-import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
+import { ID } from '@vendure/common/lib/shared-types';
 import { isObject } from '@vendure/common/lib/shared-utils';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -9,13 +9,13 @@ import {
     FulfillmentStateTransitionError,
     InvalidFulfillmentHandlerError,
 } from '../../common/error/generated-graphql-admin-errors';
-import { OrderStateTransitionError } from '../../common/error/generated-graphql-shop-errors';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { Order } from '../../entity/order/order.entity';
 import { EventBus } from '../../event-bus/event-bus';
+import { FulfillmentEvent } from '../../event-bus/events/fulfillment-event';
 import { FulfillmentStateTransitionEvent } from '../../event-bus/events/fulfillment-state-transition-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
@@ -80,12 +80,19 @@ export class FulfillmentService {
                 handlerCode: fulfillmentHandler.code,
             }),
         );
-        await this.customFieldRelationService.updateRelations(
+        const fulfillmentWithRelations = await this.customFieldRelationService.updateRelations(
             ctx,
             Fulfillment,
             fulfillmentPartial,
             newFulfillment,
         );
+        this.eventBus.publish(
+            new FulfillmentEvent(ctx, fulfillmentWithRelations, {
+                orders,
+                items,
+                handler,
+            }),
+        );
         return newFulfillment;
     }
 

+ 4 - 0
packages/core/src/service/services/global-settings.service.ts

@@ -6,6 +6,8 @@ import { InternalServerError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { GlobalSettings } from '../../entity/global-settings/global-settings.entity';
+import { EventBus } from '../../event-bus';
+import { GlobalSettingsEvent } from '../../event-bus/events/global-settings-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -21,6 +23,7 @@ export class GlobalSettingsService {
         private connection: TransactionalConnection,
         private configService: ConfigService,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     /**
@@ -64,6 +67,7 @@ export class GlobalSettingsService {
 
     async updateSettings(ctx: RequestContext, input: UpdateGlobalSettingsInput): Promise<GlobalSettings> {
         const settings = await this.getSettings(ctx);
+        this.eventBus.publish(new GlobalSettingsEvent(ctx, settings, input));
         patchEntity(settings, input);
         await this.customFieldRelationService.updateRelations(ctx, GlobalSettings, input, settings);
         return this.connection.getRepository(ctx, GlobalSettings).save(settings);

+ 17 - 4
packages/core/src/service/services/history.service.ts

@@ -14,6 +14,8 @@ import { Administrator } from '../../entity/administrator/administrator.entity';
 import { CustomerHistoryEntry } from '../../entity/history-entry/customer-history-entry.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
 import { OrderHistoryEntry } from '../../entity/history-entry/order-history-entry.entity';
+import { EventBus } from '../../event-bus';
+import { HistoryEntryEvent } from '../../event-bus/events/history-entry-event';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderState } from '../helpers/order-state-machine/order-state';
@@ -150,6 +152,7 @@ export class HistoryService {
         private connection: TransactionalConnection,
         private administratorService: AdministratorService,
         private listQueryBuilder: ListQueryBuilder,
+        private eventBus: EventBus,
     ) {}
 
     async getHistoryForOrder(
@@ -187,7 +190,9 @@ export class HistoryService {
             order: { id: orderId },
             administrator,
         });
-        return this.connection.getRepository(ctx, OrderHistoryEntry).save(entry);
+        const history = await this.connection.getRepository(ctx, OrderHistoryEntry).save(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, history, 'created', 'order', { type, data }));
+        return history;
     }
 
     async getHistoryForCustomer(
@@ -226,7 +231,9 @@ export class HistoryService {
             customer: { id: customerId },
             administrator,
         });
-        return this.connection.getRepository(ctx, CustomerHistoryEntry).save(entry);
+        const history = await this.connection.getRepository(ctx, CustomerHistoryEntry).save(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, history, 'created', 'customer', { type, data }));
+        return history;
     }
 
     async updateOrderHistoryEntry<T extends keyof OrderHistoryEntryData>(
@@ -247,12 +254,15 @@ export class HistoryService {
         if (administrator) {
             entry.administrator = administrator;
         }
-        return this.connection.getRepository(ctx, OrderHistoryEntry).save(entry);
+        const newEntry = await this.connection.getRepository(ctx, OrderHistoryEntry).save(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, entry, 'updated', 'order', args));
+        return newEntry;
     }
 
     async deleteOrderHistoryEntry(ctx: RequestContext, id: ID): Promise<void> {
         const entry = await this.connection.getEntityOrThrow(ctx, OrderHistoryEntry, id);
         await this.connection.getRepository(ctx, OrderHistoryEntry).remove(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, entry, 'deleted', 'order', id));
     }
 
     async updateCustomerHistoryEntry<T extends keyof CustomerHistoryEntryData>(
@@ -270,12 +280,15 @@ export class HistoryService {
         if (administrator) {
             entry.administrator = administrator;
         }
-        return this.connection.getRepository(ctx, CustomerHistoryEntry).save(entry);
+        const newEntry = await this.connection.getRepository(ctx, CustomerHistoryEntry).save(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, entry, 'updated', 'customer', args));
+        return newEntry;
     }
 
     async deleteCustomerHistoryEntry(ctx: RequestContext, id: ID): Promise<void> {
         const entry = await this.connection.getEntityOrThrow(ctx, CustomerHistoryEntry, id);
         await this.connection.getRepository(ctx, CustomerHistoryEntry).remove(entry);
+        this.eventBus.publish(new HistoryEntryEvent(ctx, entry, 'deleted', 'customer', id));
     }
 
     private async getAdministratorFromContext(ctx: RequestContext): Promise<Administrator | undefined> {

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

@@ -88,6 +88,7 @@ import { ShippingLine } from '../../entity/shipping-line/shipping-line.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/events/coupon-code-event';
 import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
 import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
@@ -627,6 +628,7 @@ export class OrderService {
             type: HistoryEntryType.ORDER_COUPON_APPLIED,
             data: { couponCode, promotionId: validationResult.id },
         });
+        this.eventBus.publish(new CouponCodeEvent(ctx, couponCode, orderId, 'assigned'));
         return this.applyPriceAdjustments(ctx, order);
     }
 
@@ -654,6 +656,7 @@ export class OrderService {
                 type: HistoryEntryType.ORDER_COUPON_REMOVED,
                 data: { couponCode },
             });
+            this.eventBus.publish(new CouponCodeEvent(ctx, couponCode, orderId, 'removed'));
             const result = await this.applyPriceAdjustments(ctx, order);
             await this.connection.getRepository(ctx, OrderItem).save(affectedOrderItems);
             return result;

+ 9 - 1
packages/core/src/service/services/payment-method.service.ts

@@ -22,6 +22,7 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Order } from '../../entity/order/order.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { EventBus } from '../../event-bus/event-bus';
+import { PaymentMethodEvent } from '../../event-bus/events/payment-method-event';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -72,7 +73,11 @@ export class PaymentMethodService {
             );
         }
         await this.channelService.assignToCurrentChannel(paymentMethod, ctx);
-        return this.connection.getRepository(ctx, PaymentMethod).save(paymentMethod);
+        const savedPaymentMethod = await this.connection
+            .getRepository(ctx, PaymentMethod)
+            .save(paymentMethod);
+        this.eventBus.publish(new PaymentMethodEvent(ctx, savedPaymentMethod, 'created', input));
+        return savedPaymentMethod;
     }
 
     async update(ctx: RequestContext, input: UpdatePaymentMethodInput): Promise<PaymentMethod> {
@@ -90,6 +95,7 @@ export class PaymentMethodService {
         if (input.handler) {
             paymentMethod.handler = this.configArgService.parseInput('PaymentMethodHandler', input.handler);
         }
+        this.eventBus.publish(new PaymentMethodEvent(ctx, paymentMethod, 'updated', input));
         return this.connection.getRepository(ctx, PaymentMethod).save(updatedPaymentMethod);
     }
 
@@ -115,6 +121,7 @@ export class PaymentMethodService {
             }
             try {
                 await this.connection.getRepository(ctx, PaymentMethod).remove(paymentMethod);
+                this.eventBus.publish(new PaymentMethodEvent(ctx, paymentMethod, 'deleted', paymentMethodId));
                 return {
                     result: DeletionResult.DELETED,
                 };
@@ -129,6 +136,7 @@ export class PaymentMethodService {
             // but will remove from the current channel
             paymentMethod.channels = paymentMethod.channels.filter(c => !idsAreEqual(c.id, ctx.channelId));
             await this.connection.getRepository(ctx, PaymentMethod).save(paymentMethod);
+            this.eventBus.publish(new PaymentMethodEvent(ctx, paymentMethod, 'deleted', paymentMethodId));
             return {
                 result: DeletionResult.DELETED,
             };

+ 11 - 1
packages/core/src/service/services/product-option-group.service.ts

@@ -12,6 +12,8 @@ import { assertFound } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { ProductOptionGroupTranslation } from '../../entity/product-option-group/product-option-group-translation.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
+import { EventBus } from '../../event-bus';
+import { ProductOptionGroupEvent } from '../../event-bus/events/product-option-group-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -28,6 +30,7 @@ export class ProductOptionGroupService {
         private connection: TransactionalConnection,
         private translatableSaver: TranslatableSaver,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     findAll(ctx: RequestContext, filterTerm?: string): Promise<Array<Translated<ProductOptionGroup>>> {
@@ -79,7 +82,13 @@ export class ProductOptionGroupService {
             entityType: ProductOptionGroup,
             translationType: ProductOptionGroupTranslation,
         });
-        await this.customFieldRelationService.updateRelations(ctx, ProductOptionGroup, input, group);
+        const groupWithRelations = await this.customFieldRelationService.updateRelations(
+            ctx,
+            ProductOptionGroup,
+            input,
+            group,
+        );
+        this.eventBus.publish(new ProductOptionGroupEvent(ctx, groupWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, group.id));
     }
 
@@ -94,6 +103,7 @@ export class ProductOptionGroupService {
             translationType: ProductOptionGroupTranslation,
         });
         await this.customFieldRelationService.updateRelations(ctx, ProductOptionGroup, input, group);
+        this.eventBus.publish(new ProductOptionGroupEvent(ctx, group, 'updated', input));
         return assertFound(this.findOne(ctx, group.id));
     }
 }

+ 6 - 1
packages/core/src/service/services/product-option.service.ts

@@ -13,6 +13,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
+import { EventBus } from '../../event-bus';
+import { ProductOptionEvent } from '../../event-bus/events/product-option-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -29,6 +31,7 @@ export class ProductOptionService {
         private connection: TransactionalConnection,
         private translatableSaver: TranslatableSaver,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     findAll(ctx: RequestContext): Promise<Array<Translated<ProductOption>>> {
@@ -65,12 +68,13 @@ export class ProductOptionService {
             translationType: ProductOptionTranslation,
             beforeSave: po => (po.group = productOptionGroup),
         });
-        await this.customFieldRelationService.updateRelations(
+        const optionWithRelations = await this.customFieldRelationService.updateRelations(
             ctx,
             ProductOption,
             input as CreateProductOptionInput,
             option,
         );
+        this.eventBus.publish(new ProductOptionEvent(ctx, optionWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, option.id));
     }
 
@@ -82,6 +86,7 @@ export class ProductOptionService {
             translationType: ProductOptionTranslation,
         });
         await this.customFieldRelationService.updateRelations(ctx, ProductOption, input, option);
+        this.eventBus.publish(new ProductOptionEvent(ctx, option, 'updated', input));
         return assertFound(this.findOne(ctx, option.id));
     }
 }

+ 3 - 3
packages/core/src/service/services/product-variant.service.ts

@@ -355,7 +355,7 @@ export class ProductVariantService {
             ids.push(id);
         }
         const createdVariants = await this.findByIds(ctx, ids);
-        this.eventBus.publish(new ProductVariantEvent(ctx, createdVariants, 'created'));
+        this.eventBus.publish(new ProductVariantEvent(ctx, createdVariants, 'created', input));
         return createdVariants;
     }
 
@@ -370,7 +370,7 @@ export class ProductVariantService {
             ctx,
             input.map(i => i.id),
         );
-        this.eventBus.publish(new ProductVariantEvent(ctx, updatedVariants, 'updated'));
+        this.eventBus.publish(new ProductVariantEvent(ctx, updatedVariants, 'updated', input));
         return updatedVariants;
     }
 
@@ -540,7 +540,7 @@ export class ProductVariantService {
             variant.deletedAt = new Date();
         }
         await this.connection.getRepository(ctx, ProductVariant).save(variants, { reload: false });
-        this.eventBus.publish(new ProductVariantEvent(ctx, variants, 'deleted'));
+        this.eventBus.publish(new ProductVariantEvent(ctx, variants, 'deleted', id));
         return {
             result: DeletionResult.DELETED,
         };

+ 6 - 3
packages/core/src/service/services/product.service.ts

@@ -28,6 +28,7 @@ import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { ProductChannelEvent } from '../../event-bus/events/product-channel-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
+import { ProductOptionGroupChangeEvent } from '../../event-bus/events/product-option-group-change-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { SlugValidator } from '../helpers/slug-validator/slug-validator';
@@ -193,7 +194,7 @@ export class ProductService {
         });
         await this.customFieldRelationService.updateRelations(ctx, Product, input, product);
         await this.assetService.updateEntityAssets(ctx, product, input);
-        this.eventBus.publish(new ProductEvent(ctx, product, 'created'));
+        this.eventBus.publish(new ProductEvent(ctx, product, 'created', input));
         return assertFound(this.findOne(ctx, product.id));
     }
 
@@ -223,7 +224,7 @@ export class ProductService {
             },
         });
         await this.customFieldRelationService.updateRelations(ctx, Product, input, updatedProduct);
-        this.eventBus.publish(new ProductEvent(ctx, updatedProduct, 'updated'));
+        this.eventBus.publish(new ProductEvent(ctx, updatedProduct, 'updated', input));
         return assertFound(this.findOne(ctx, updatedProduct.id));
     }
 
@@ -234,7 +235,7 @@ export class ProductService {
         });
         product.deletedAt = new Date();
         await this.connection.getRepository(ctx, Product).save(product, { reload: false });
-        this.eventBus.publish(new ProductEvent(ctx, product, 'deleted'));
+        this.eventBus.publish(new ProductEvent(ctx, product, 'deleted', productId));
         await this.productVariantService.softDelete(
             ctx,
             product.variants.map(v => v.id),
@@ -327,6 +328,7 @@ export class ProductService {
         }
 
         await this.connection.getRepository(ctx, Product).save(product, { reload: false });
+        this.eventBus.publish(new ProductOptionGroupChangeEvent(ctx, product, optionGroupId, 'assigned'));
         return assertFound(this.findOne(ctx, productId));
     }
 
@@ -346,6 +348,7 @@ export class ProductService {
         product.optionGroups = product.optionGroups.filter(g => g.id !== optionGroupId);
 
         await this.connection.getRepository(ctx, Product).save(product, { reload: false });
+        this.eventBus.publish(new ProductOptionGroupChangeEvent(ctx, product, optionGroupId, 'removed'));
         return assertFound(this.findOne(ctx, productId));
     }
 

+ 8 - 1
packages/core/src/service/services/promotion.service.ts

@@ -34,6 +34,8 @@ import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Order } from '../../entity/order/order.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
+import { EventBus } from '../../event-bus';
+import { PromotionEvent } from '../../event-bus/events/promotion-event';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -57,6 +59,7 @@ export class PromotionService {
         private channelService: ChannelService,
         private listQueryBuilder: ListQueryBuilder,
         private configArgService: ConfigArgService,
+        private eventBus: EventBus,
     ) {
         this.availableConditions = this.configService.promotionOptions.promotionConditions || [];
         this.availableActions = this.configService.promotionOptions.promotionActions || [];
@@ -116,6 +119,7 @@ export class PromotionService {
         }
         await this.channelService.assignToCurrentChannel(promotion, ctx);
         const newPromotion = await this.connection.getRepository(ctx, Promotion).save(promotion);
+        this.eventBus.publish(new PromotionEvent(ctx, newPromotion, 'created', input));
         return assertFound(this.findOne(ctx, newPromotion.id));
     }
 
@@ -142,14 +146,17 @@ export class PromotionService {
         }
         promotion.priorityScore = this.calculatePriorityScore(input);
         await this.connection.getRepository(ctx, Promotion).save(updatedPromotion, { reload: false });
+        this.eventBus.publish(new PromotionEvent(ctx, promotion, 'updated', input));
         return assertFound(this.findOne(ctx, updatedPromotion.id));
     }
 
     async softDeletePromotion(ctx: RequestContext, promotionId: ID): Promise<DeletionResponse> {
-        await this.connection.getEntityOrThrow(ctx, Promotion, promotionId);
+        const promotion = await this.connection.getEntityOrThrow(ctx, Promotion, promotionId);
         await this.connection
             .getRepository(ctx, Promotion)
             .update({ id: promotionId }, { deletedAt: new Date() });
+        this.eventBus.publish(new PromotionEvent(ctx, promotion, 'deleted', promotionId));
+
         return {
             result: DeletionResult.DELETED,
         };

+ 9 - 2
packages/core/src/service/services/role.service.ts

@@ -30,6 +30,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Channel } from '../../entity/channel/channel.entity';
 import { Role } from '../../entity/role/role.entity';
 import { User } from '../../entity/user/user.entity';
+import { EventBus } from '../../event-bus';
+import { RoleEvent } from '../../event-bus/events/role-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getUserChannelsPermissions } from '../helpers/utils/get-user-channels-permissions';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -49,6 +51,7 @@ export class RoleService {
         private channelService: ChannelService,
         private listQueryBuilder: ListQueryBuilder,
         private configService: ConfigService,
+        private eventBus: EventBus,
     ) {}
 
     async initRoles() {
@@ -143,7 +146,9 @@ export class RoleService {
         } else {
             targetChannels = [ctx.channel];
         }
-        return this.createRoleForChannels(ctx, input, targetChannels);
+        const role = await this.createRoleForChannels(ctx, input, targetChannels);
+        this.eventBus.publish(new RoleEvent(ctx, role, 'created', input));
+        return role;
     }
 
     async update(ctx: RequestContext, input: UpdateRoleInput): Promise<Role> {
@@ -166,7 +171,8 @@ export class RoleService {
             updatedRole.channels = await this.getPermittedChannels(ctx, input.channelIds);
         }
         await this.connection.getRepository(ctx, Role).save(updatedRole, { reload: false });
-        return assertFound(this.findOne(ctx, role.id));
+        this.eventBus.publish(new RoleEvent(ctx, role, 'updated', input));
+        return await assertFound(this.findOne(ctx, role.id));
     }
 
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
@@ -178,6 +184,7 @@ export class RoleService {
             throw new InternalServerError(`error.cannot-delete-role`, { roleCode: role.code });
         }
         await this.connection.getRepository(ctx, Role).remove(role);
+        this.eventBus.publish(new RoleEvent(ctx, role, 'deleted', id));
         return {
             result: DeletionResult.DELETED,
         };

+ 12 - 1
packages/core/src/service/services/shipping-method.service.ts

@@ -19,6 +19,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Channel } from '../../entity/channel/channel.entity';
 import { ShippingMethodTranslation } from '../../entity/shipping-method/shipping-method-translation.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
+import { EventBus } from '../../event-bus';
+import { ShippingMethodEvent } from '../../event-bus/events/shipping-method-event';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
@@ -43,6 +45,7 @@ export class ShippingMethodService {
         private configArgService: ConfigArgService,
         private translatableSaver: TranslatableSaver,
         private customFieldRelationService: CustomFieldRelationService,
+        private eventBus: EventBus,
     ) {}
 
     /** @internal */
@@ -113,7 +116,13 @@ export class ShippingMethodService {
         const newShippingMethod = await this.connection
             .getRepository(ctx, ShippingMethod)
             .save(shippingMethod);
-        await this.customFieldRelationService.updateRelations(ctx, ShippingMethod, input, newShippingMethod);
+        const shippingMethodWithRelations = await this.customFieldRelationService.updateRelations(
+            ctx,
+            ShippingMethod,
+            input,
+            newShippingMethod,
+        );
+        this.eventBus.publish(new ShippingMethodEvent(ctx, shippingMethodWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, newShippingMethod.id));
     }
 
@@ -155,6 +164,7 @@ export class ShippingMethodService {
             input,
             updatedShippingMethod,
         );
+        this.eventBus.publish(new ShippingMethodEvent(ctx, shippingMethod, 'updated', input));
         return assertFound(this.findOne(ctx, shippingMethod.id));
     }
 
@@ -165,6 +175,7 @@ export class ShippingMethodService {
         });
         shippingMethod.deletedAt = new Date();
         await this.connection.getRepository(ctx, ShippingMethod).save(shippingMethod, { reload: false });
+        this.eventBus.publish(new ShippingMethodEvent(ctx, shippingMethod, 'deleted', id));
         return {
             result: DeletionResult.DELETED,
         };

+ 6 - 1
packages/core/src/service/services/tax-category.service.ts

@@ -13,6 +13,8 @@ import { assertFound } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
+import { EventBus } from '../../event-bus';
+import { TaxCategoryEvent } from '../../event-bus/events/tax-category-event';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 /**
@@ -23,7 +25,7 @@ import { patchEntity } from '../helpers/utils/patch-entity';
  */
 @Injectable()
 export class TaxCategoryService {
-    constructor(private connection: TransactionalConnection) {}
+    constructor(private connection: TransactionalConnection, private eventBus: EventBus) {}
 
     findAll(ctx: RequestContext): Promise<TaxCategory[]> {
         return this.connection.getRepository(ctx, TaxCategory).find();
@@ -41,6 +43,7 @@ export class TaxCategoryService {
                 .update({ isDefault: true }, { isDefault: false });
         }
         const newTaxCategory = await this.connection.getRepository(ctx, TaxCategory).save(taxCategory);
+        this.eventBus.publish(new TaxCategoryEvent(ctx, newTaxCategory, 'created', input));
         return assertFound(this.findOne(ctx, newTaxCategory.id));
     }
 
@@ -56,6 +59,7 @@ export class TaxCategoryService {
                 .update({ isDefault: true }, { isDefault: false });
         }
         await this.connection.getRepository(ctx, TaxCategory).save(updatedTaxCategory, { reload: false });
+        this.eventBus.publish(new TaxCategoryEvent(ctx, taxCategory, 'updated', input));
         return assertFound(this.findOne(ctx, taxCategory.id));
     }
 
@@ -78,6 +82,7 @@ export class TaxCategoryService {
 
         try {
             await this.connection.getRepository(ctx, TaxCategory).remove(taxCategory);
+            this.eventBus.publish(new TaxCategoryEvent(ctx, taxCategory, 'deleted', id));
             return {
                 result: DeletionResult.DELETED,
             };

+ 5 - 0
packages/core/src/service/services/tax-rate.service.ts

@@ -18,6 +18,7 @@ import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus/event-bus';
+import { TaxRateEvent } from '../../event-bus/events/tax-rate-event';
 import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -76,6 +77,7 @@ export class TaxRateService {
         const newTaxRate = await this.connection.getRepository(ctx, TaxRate).save(taxRate);
         await this.updateActiveTaxRates(ctx);
         this.eventBus.publish(new TaxRateModificationEvent(ctx, newTaxRate));
+        this.eventBus.publish(new TaxRateEvent(ctx, newTaxRate, 'created', input));
         return assertFound(this.findOne(ctx, newTaxRate.id));
     }
 
@@ -110,6 +112,8 @@ export class TaxRateService {
         await this.connection.commitOpenTransaction(ctx);
 
         this.eventBus.publish(new TaxRateModificationEvent(ctx, updatedTaxRate));
+        this.eventBus.publish(new TaxRateEvent(ctx, updatedTaxRate, 'updated', input));
+
         return assertFound(this.findOne(ctx, taxRate.id));
     }
 
@@ -117,6 +121,7 @@ export class TaxRateService {
         const taxRate = await this.connection.getEntityOrThrow(ctx, TaxRate, id);
         try {
             await this.connection.getRepository(ctx, TaxRate).remove(taxRate);
+            this.eventBus.publish(new TaxRateEvent(ctx, taxRate, 'deleted', id));
             return {
                 result: DeletionResult.DELETED,
             };

+ 22 - 7
packages/core/src/service/services/zone.service.ts

@@ -18,6 +18,9 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Channel, TaxRate } from '../../entity';
 import { Country } from '../../entity/country/country.entity';
 import { Zone } from '../../entity/zone/zone.entity';
+import { EventBus } from '../../event-bus';
+import { ZoneEvent } from '../../event-bus/events/zone-event';
+import { ZoneMembersEvent } from '../../event-bus/events/zone-members-event';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
@@ -33,7 +36,11 @@ export class ZoneService {
      * We cache all Zones to avoid hitting the DB many times per request.
      */
     private zones: SelfRefreshingCache<Zone[], [RequestContext]>;
-    constructor(private connection: TransactionalConnection, private configService: ConfigService) {}
+    constructor(
+        private connection: TransactionalConnection,
+        private configService: ConfigService,
+        private eventBus: EventBus,
+    ) {}
 
     /** @internal */
     async initZones() {
@@ -80,6 +87,7 @@ export class ZoneService {
         }
         const newZone = await this.connection.getRepository(ctx, Zone).save(zone);
         await this.zones.refresh(ctx);
+        this.eventBus.publish(new ZoneEvent(ctx, newZone, 'created', input));
         return assertFound(this.findOne(ctx, newZone.id));
     }
 
@@ -88,6 +96,7 @@ export class ZoneService {
         const updatedZone = patchEntity(zone, input);
         await this.connection.getRepository(ctx, Zone).save(updatedZone, { reload: false });
         await this.zones.refresh(ctx);
+        this.eventBus.publish(new ZoneEvent(ctx, zone, 'updated', input));
         return assertFound(this.findOne(ctx, zone.id));
     }
 
@@ -126,6 +135,7 @@ export class ZoneService {
         } else {
             await this.connection.getRepository(ctx, Zone).remove(zone);
             await this.zones.refresh(ctx);
+            this.eventBus.publish(new ZoneEvent(ctx, zone, 'deleted', id));
             return {
                 result: DeletionResult.DELETED,
                 message: '',
@@ -133,28 +143,33 @@ export class ZoneService {
         }
     }
 
-    async addMembersToZone(ctx: RequestContext, input: MutationAddMembersToZoneArgs): Promise<Zone> {
-        const countries = await this.getCountriesFromIds(ctx, input.memberIds);
-        const zone = await this.connection.getEntityOrThrow(ctx, Zone, input.zoneId, {
+    async addMembersToZone(
+        ctx: RequestContext,
+        { memberIds, zoneId }: MutationAddMembersToZoneArgs,
+    ): Promise<Zone> {
+        const countries = await this.getCountriesFromIds(ctx, memberIds);
+        const zone = await this.connection.getEntityOrThrow(ctx, Zone, zoneId, {
             relations: ['members'],
         });
         const members = unique(zone.members.concat(countries), 'id');
         zone.members = members;
         await this.connection.getRepository(ctx, Zone).save(zone, { reload: false });
         await this.zones.refresh(ctx);
+        this.eventBus.publish(new ZoneMembersEvent(ctx, zone, 'assigned', memberIds));
         return assertFound(this.findOne(ctx, zone.id));
     }
 
     async removeMembersFromZone(
         ctx: RequestContext,
-        input: MutationRemoveMembersFromZoneArgs,
+        { memberIds, zoneId }: MutationRemoveMembersFromZoneArgs,
     ): Promise<Zone> {
-        const zone = await this.connection.getEntityOrThrow(ctx, Zone, input.zoneId, {
+        const zone = await this.connection.getEntityOrThrow(ctx, Zone, zoneId, {
             relations: ['members'],
         });
-        zone.members = zone.members.filter(country => !input.memberIds.includes(country.id));
+        zone.members = zone.members.filter(country => !memberIds.includes(country.id));
         await this.connection.getRepository(ctx, Zone).save(zone, { reload: false });
         await this.zones.refresh(ctx);
+        this.eventBus.publish(new ZoneMembersEvent(ctx, zone, 'removed', memberIds));
         return assertFound(this.findOne(ctx, zone.id));
     }