|
|
@@ -2,10 +2,12 @@ import { Injectable } from '@nestjs/common';
|
|
|
import { InjectConnection } from '@nestjs/typeorm';
|
|
|
import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
|
|
|
import {
|
|
|
+ AddNoteToCustomerInput,
|
|
|
CreateAddressInput,
|
|
|
CreateCustomerInput,
|
|
|
DeletionResponse,
|
|
|
DeletionResult,
|
|
|
+ HistoryEntryType,
|
|
|
UpdateAddressInput,
|
|
|
UpdateCustomerInput,
|
|
|
} from '@vendure/common/lib/generated-types';
|
|
|
@@ -13,13 +15,19 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
|
|
|
import { Connection } from 'typeorm';
|
|
|
|
|
|
import { RequestContext } from '../../api/common/request-context';
|
|
|
-import { EntityNotFoundError, IllegalOperationError, UserInputError } from '../../common/error/errors';
|
|
|
+import {
|
|
|
+ EntityNotFoundError,
|
|
|
+ IllegalOperationError,
|
|
|
+ InternalServerError,
|
|
|
+ UserInputError,
|
|
|
+} from '../../common/error/errors';
|
|
|
import { ListQueryOptions } from '../../common/types/common-types';
|
|
|
import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
|
|
|
import { ConfigService } from '../../config/config.service';
|
|
|
import { Address } from '../../entity/address/address.entity';
|
|
|
import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
|
|
|
import { Customer } from '../../entity/customer/customer.entity';
|
|
|
+import { Order } from '../../entity/order/order.entity';
|
|
|
import { User } from '../../entity/user/user.entity';
|
|
|
import { EventBus } from '../../event-bus/event-bus';
|
|
|
import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
|
|
|
@@ -27,11 +35,13 @@ import { IdentifierChangeEvent } from '../../event-bus/events/identifier-change-
|
|
|
import { IdentifierChangeRequestEvent } from '../../event-bus/events/identifier-change-request-event';
|
|
|
import { PasswordResetEvent } from '../../event-bus/events/password-reset-event';
|
|
|
import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
|
|
|
+import { addressToLine } from '../helpers/utils/address-to-line';
|
|
|
import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
|
|
|
import { patchEntity } from '../helpers/utils/patch-entity';
|
|
|
import { translateDeep } from '../helpers/utils/translate-entity';
|
|
|
|
|
|
import { CountryService } from './country.service';
|
|
|
+import { HistoryService } from './history.service';
|
|
|
import { UserService } from './user.service';
|
|
|
|
|
|
@Injectable()
|
|
|
@@ -43,6 +53,7 @@ export class CustomerService {
|
|
|
private countryService: CountryService,
|
|
|
private listQueryBuilder: ListQueryBuilder,
|
|
|
private eventBus: EventBus,
|
|
|
+ private historyService: HistoryService,
|
|
|
) {}
|
|
|
|
|
|
findAll(options: ListQueryOptions<Customer> | undefined): Promise<PaginatedList<Customer>> {
|
|
|
@@ -148,6 +159,12 @@ export class CustomerService {
|
|
|
firstName: input.firstName || '',
|
|
|
lastName: input.lastName || '',
|
|
|
});
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_REGISTERED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
if (!user) {
|
|
|
user = await this.userService.createCustomerUser(input.emailAddress, input.password || undefined);
|
|
|
} else if (!user.verified) {
|
|
|
@@ -157,6 +174,13 @@ export class CustomerService {
|
|
|
await this.connection.getRepository(Customer).save(customer, { reload: false });
|
|
|
if (!user.verified) {
|
|
|
this.eventBus.publish(new AccountRegistrationEvent(ctx, user));
|
|
|
+ } else {
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_VERIFIED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
@@ -172,11 +196,22 @@ export class CustomerService {
|
|
|
}
|
|
|
|
|
|
async verifyCustomerEmailAddress(
|
|
|
+ ctx: RequestContext,
|
|
|
verificationToken: string,
|
|
|
password: string,
|
|
|
): Promise<Customer | undefined> {
|
|
|
const user = await this.userService.verifyUserByToken(verificationToken, password);
|
|
|
if (user) {
|
|
|
+ const customer = await this.findOneByUserId(user.id);
|
|
|
+ if (!customer) {
|
|
|
+ throw new InternalServerError('error.cannot-locate-customer-for-user');
|
|
|
+ }
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_VERIFIED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
return this.findOneByUserId(user.id);
|
|
|
}
|
|
|
}
|
|
|
@@ -185,13 +220,37 @@ export class CustomerService {
|
|
|
const user = await this.userService.setPasswordResetToken(emailAddress);
|
|
|
if (user) {
|
|
|
this.eventBus.publish(new PasswordResetEvent(ctx, user));
|
|
|
+ const customer = await this.findOneByUserId(user.id);
|
|
|
+ if (!customer) {
|
|
|
+ throw new InternalServerError('error.cannot-locate-customer-for-user');
|
|
|
+ }
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_REQUESTED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async resetPassword(passwordResetToken: string, password: string): Promise<Customer | undefined> {
|
|
|
+ async resetPassword(
|
|
|
+ ctx: RequestContext,
|
|
|
+ passwordResetToken: string,
|
|
|
+ password: string,
|
|
|
+ ): Promise<Customer | undefined> {
|
|
|
const user = await this.userService.resetPasswordByToken(passwordResetToken, password);
|
|
|
if (user) {
|
|
|
- return this.findOneByUserId(user.id);
|
|
|
+ const customer = await this.findOneByUserId(user.id);
|
|
|
+ if (!customer) {
|
|
|
+ throw new InternalServerError('error.cannot-locate-customer-for-user');
|
|
|
+ }
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_REQUESTED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
+ return customer;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -208,22 +267,34 @@ export class CustomerService {
|
|
|
if (!user) {
|
|
|
return false;
|
|
|
}
|
|
|
+ const customer = await this.findOneByUserId(user.id);
|
|
|
+ if (!customer) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_REQUESTED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
if (this.configService.authOptions.requireVerification) {
|
|
|
user.pendingIdentifier = newEmailAddress;
|
|
|
await this.userService.setIdentifierChangeToken(user);
|
|
|
this.eventBus.publish(new IdentifierChangeRequestEvent(ctx, user));
|
|
|
return true;
|
|
|
} else {
|
|
|
- const customer = await this.findOneByUserId(user.id);
|
|
|
- if (!customer) {
|
|
|
- return false;
|
|
|
- }
|
|
|
const oldIdentifier = user.identifier;
|
|
|
user.identifier = newEmailAddress;
|
|
|
customer.emailAddress = newEmailAddress;
|
|
|
await this.connection.getRepository(User).save(user, { reload: false });
|
|
|
await this.connection.getRepository(Customer).save(customer, { reload: false });
|
|
|
this.eventBus.publish(new IdentifierChangeEvent(ctx, user, oldIdentifier));
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
@@ -240,6 +311,12 @@ export class CustomerService {
|
|
|
this.eventBus.publish(new IdentifierChangeEvent(ctx, user, oldIdentifier));
|
|
|
customer.emailAddress = user.identifier;
|
|
|
await this.connection.getRepository(Customer).save(customer, { reload: false });
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
|
|
|
+ data: {},
|
|
|
+ });
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -299,27 +376,54 @@ export class CustomerService {
|
|
|
customer.addresses.push(createdAddress);
|
|
|
await this.connection.manager.save(customer, { reload: false });
|
|
|
await this.enforceSingleDefaultAddress(createdAddress.id, input);
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_ADDRESS_CREATED,
|
|
|
+ data: { address: addressToLine(createdAddress) },
|
|
|
+ });
|
|
|
return createdAddress;
|
|
|
}
|
|
|
|
|
|
async updateAddress(ctx: RequestContext, input: UpdateAddressInput): Promise<Address> {
|
|
|
const address = await getEntityOrThrow(this.connection, Address, input.id, {
|
|
|
- relations: ['country'],
|
|
|
+ relations: ['customer', 'country'],
|
|
|
});
|
|
|
if (input.countryCode && input.countryCode !== address.country.code) {
|
|
|
address.country = await this.countryService.findOneByCode(ctx, input.countryCode);
|
|
|
} else {
|
|
|
address.country = translateDeep(address.country, ctx.languageCode);
|
|
|
}
|
|
|
- const updatedAddress = patchEntity(address, input);
|
|
|
- await this.connection.getRepository(Address).save(updatedAddress, { reload: false });
|
|
|
+ let updatedAddress = patchEntity(address, input);
|
|
|
+ updatedAddress = await this.connection.getRepository(Address).save(updatedAddress);
|
|
|
await this.enforceSingleDefaultAddress(input.id, input);
|
|
|
+
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: address.customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_ADDRESS_UPDATED,
|
|
|
+ data: {
|
|
|
+ address: addressToLine(updatedAddress),
|
|
|
+ input,
|
|
|
+ },
|
|
|
+ });
|
|
|
return updatedAddress;
|
|
|
}
|
|
|
|
|
|
- async deleteAddress(id: ID): Promise<boolean> {
|
|
|
- const address = await getEntityOrThrow(this.connection, Address, id);
|
|
|
+ async deleteAddress(ctx: RequestContext, id: ID): Promise<boolean> {
|
|
|
+ const address = await getEntityOrThrow(this.connection, Address, id, {
|
|
|
+ relations: ['customer', 'country'],
|
|
|
+ });
|
|
|
+ address.country = translateDeep(address.country, ctx.languageCode);
|
|
|
await this.reassignDefaultsForDeletedAddress(address);
|
|
|
+ await this.historyService.createHistoryEntryForCustomer({
|
|
|
+ customerId: address.customer.id,
|
|
|
+ ctx,
|
|
|
+ type: HistoryEntryType.CUSTOMER_ADDRESS_DELETED,
|
|
|
+ data: {
|
|
|
+ address: addressToLine(address),
|
|
|
+ },
|
|
|
+ });
|
|
|
await this.connection.getRepository(Address).remove(address);
|
|
|
return true;
|
|
|
}
|
|
|
@@ -334,6 +438,22 @@ export class CustomerService {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ async addNoteToCustomer(ctx: RequestContext, input: AddNoteToCustomerInput): Promise<Order> {
|
|
|
+ const customer = await getEntityOrThrow(this.connection, Customer, input.id);
|
|
|
+ await this.historyService.createHistoryEntryForCustomer(
|
|
|
+ {
|
|
|
+ ctx,
|
|
|
+ customerId: customer.id,
|
|
|
+ type: HistoryEntryType.CUSTOMER_NOTE,
|
|
|
+ data: {
|
|
|
+ note: input.note,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ input.isPublic,
|
|
|
+ );
|
|
|
+ return customer;
|
|
|
+ }
|
|
|
+
|
|
|
private async enforceSingleDefaultAddress(addressId: ID, input: CreateAddressInput | UpdateAddressInput) {
|
|
|
const result = await this.connection
|
|
|
.getRepository(Address)
|