@vendure/core
Version:
A modern, headless ecommerce framework
790 lines • 40.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomerService = void 0;
const common_1 = require("@nestjs/common");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const typeorm_1 = require("typeorm");
const error_result_1 = require("../../common/error/error-result");
const errors_1 = require("../../common/error/errors");
const generated_graphql_admin_errors_1 = require("../../common/error/generated-graphql-admin-errors");
const generated_graphql_shop_errors_1 = require("../../common/error/generated-graphql-shop-errors");
const instrument_decorator_1 = require("../../common/instrument-decorator");
const utils_1 = require("../../common/utils");
const native_authentication_strategy_1 = require("../../config/auth/native-authentication-strategy");
const config_service_1 = require("../../config/config.service");
const transactional_connection_1 = require("../../connection/transactional-connection");
const address_entity_1 = require("../../entity/address/address.entity");
const native_authentication_method_entity_1 = require("../../entity/authentication-method/native-authentication-method.entity");
const channel_entity_1 = require("../../entity/channel/channel.entity");
const customer_entity_1 = require("../../entity/customer/customer.entity");
const user_entity_1 = require("../../entity/user/user.entity");
const event_bus_1 = require("../../event-bus/event-bus");
const account_registration_event_1 = require("../../event-bus/events/account-registration-event");
const account_verified_event_1 = require("../../event-bus/events/account-verified-event");
const customer_address_event_1 = require("../../event-bus/events/customer-address-event");
const customer_event_1 = require("../../event-bus/events/customer-event");
const identifier_change_event_1 = require("../../event-bus/events/identifier-change-event");
const identifier_change_request_event_1 = require("../../event-bus/events/identifier-change-request-event");
const password_reset_event_1 = require("../../event-bus/events/password-reset-event");
const password_reset_verified_event_1 = require("../../event-bus/events/password-reset-verified-event");
const custom_field_relation_service_1 = require("../helpers/custom-field-relation/custom-field-relation.service");
const list_query_builder_1 = require("../helpers/list-query-builder/list-query-builder");
const translator_service_1 = require("../helpers/translator/translator.service");
const address_to_line_1 = require("../helpers/utils/address-to-line");
const patch_entity_1 = require("../helpers/utils/patch-entity");
const channel_service_1 = require("./channel.service");
const country_service_1 = require("./country.service");
const history_service_1 = require("./history.service");
const user_service_1 = require("./user.service");
/**
* @description
* Contains methods relating to {@link Customer} entities.
*
* @docsCategory services
*/
let CustomerService = class CustomerService {
constructor(connection, configService, userService, countryService, listQueryBuilder, eventBus, historyService, channelService, customFieldRelationService, translator) {
this.connection = connection;
this.configService = configService;
this.userService = userService;
this.countryService = countryService;
this.listQueryBuilder = listQueryBuilder;
this.eventBus = eventBus;
this.historyService = historyService;
this.channelService = channelService;
this.customFieldRelationService = customFieldRelationService;
this.translator = translator;
}
findAll(ctx, options, relations = []) {
const customPropertyMap = {};
const hasPostalCodeFilter = this.listQueryBuilder.filterObjectHasProperty(options === null || options === void 0 ? void 0 : options.filter, 'postalCode');
if (hasPostalCodeFilter) {
relations.push('addresses');
customPropertyMap.postalCode = 'addresses.postalCode';
}
return this.listQueryBuilder
.build(customer_entity_1.Customer, options, {
relations,
channelId: ctx.channelId,
where: { deletedAt: (0, typeorm_1.IsNull)() },
ctx,
customPropertyMap,
})
.getManyAndCount()
.then(([items, totalItems]) => ({ items, totalItems }));
}
findOne(ctx, id, relations = []) {
return this.connection
.findOneInChannel(ctx, customer_entity_1.Customer, id, ctx.channelId, {
relations,
where: { deletedAt: (0, typeorm_1.IsNull)() },
})
.then(result => result !== null && result !== void 0 ? result : undefined);
}
/**
* @description
* Returns the Customer entity associated with the given userId, if one exists.
* Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned
* to the current active Channel only.
*/
findOneByUserId(ctx, userId, filterOnChannel = true) {
let query = this.connection
.getRepository(ctx, customer_entity_1.Customer)
.createQueryBuilder('customer')
.leftJoin('customer.channels', 'channel')
.leftJoinAndSelect('customer.user', 'user')
.where('user.id = :userId', { userId })
.andWhere('customer.deletedAt is null');
if (filterOnChannel) {
query = query.andWhere('channel.id = :channelId', { channelId: ctx.channelId });
}
return query.getOne().then(result => result !== null && result !== void 0 ? result : undefined);
}
/**
* @description
* Returns all {@link Address} entities associated with the specified Customer.
*/
findAddressesByCustomerId(ctx, customerId) {
return this.connection
.getRepository(ctx, address_entity_1.Address)
.createQueryBuilder('address')
.leftJoinAndSelect('address.country', 'country')
.leftJoinAndSelect('country.translations', 'countryTranslation')
.where('address.customer = :id', { id: customerId })
.getMany()
.then(addresses => {
addresses.forEach(address => {
address.country = this.translator.translate(address.country, ctx);
});
return addresses;
});
}
/**
* @description
* Returns a list of all {@link CustomerGroup} entities.
*/
async getCustomerGroups(ctx, customerId) {
const customerWithGroups = await this.connection.findOneInChannel(ctx, customer_entity_1.Customer, customerId, ctx === null || ctx === void 0 ? void 0 : ctx.channelId, {
relations: ['groups'],
where: {
deletedAt: (0, typeorm_1.IsNull)(),
},
});
if (customerWithGroups) {
return customerWithGroups.groups;
}
else {
return [];
}
}
/**
* @description
* Creates a new Customer, including creation of a new User with the special `customer` Role.
*
* If the `password` argument is specified, the Customer will be immediately verified. If not,
* then an {@link AccountRegistrationEvent} is published, so that the customer can have their
* email address verified and set their password in a later step using the `verifyCustomerEmailAddress()`
* method.
*
* This method is intended to be used in admin-created Customer flows.
*/
async create(ctx, input, password) {
var _a;
input.emailAddress = (0, utils_1.normalizeEmailAddress)(input.emailAddress);
const customer = new customer_entity_1.Customer(input);
const existingCustomerInChannel = await this.connection
.getRepository(ctx, customer_entity_1.Customer)
.createQueryBuilder('customer')
.leftJoin('customer.channels', 'channel')
.where('channel.id = :channelId', { channelId: ctx.channelId })
.andWhere('customer.emailAddress = :emailAddress', { emailAddress: input.emailAddress })
.andWhere('customer.deletedAt is null')
.getOne();
if (existingCustomerInChannel) {
return new generated_graphql_admin_errors_1.EmailAddressConflictError();
}
const existingCustomer = await this.connection.getRepository(ctx, customer_entity_1.Customer).findOne({
relations: ['channels'],
where: {
emailAddress: input.emailAddress,
deletedAt: (0, typeorm_1.IsNull)(),
},
});
const existingUser = await this.userService.getUserByEmailAddress(ctx, input.emailAddress, 'customer');
if (existingCustomer && existingUser) {
// Customer already exists, bring to this Channel
const updatedCustomer = (0, patch_entity_1.patchEntity)(existingCustomer, input);
updatedCustomer.channels.push(ctx.channel);
return this.connection.getRepository(ctx, customer_entity_1.Customer).save(updatedCustomer);
}
else if (existingCustomer || existingUser) {
// Not sure when this situation would occur
return new generated_graphql_admin_errors_1.EmailAddressConflictError();
}
const customerUser = await this.userService.createCustomerUser(ctx, input.emailAddress, password);
if ((0, error_result_1.isGraphQlErrorResult)(customerUser)) {
throw customerUser;
}
customer.user = customerUser;
if (password && password !== '') {
const verificationToken = customer.user.getNativeAuthenticationMethod().verificationToken;
if (verificationToken) {
const result = await this.userService.verifyUserByToken(ctx, verificationToken);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
// In theory this should never be reached, so we will just
// throw the result
throw result;
}
else {
customer.user = result;
}
}
}
await this.eventBus.publish(new account_registration_event_1.AccountRegistrationEvent(ctx, customer.user));
await this.channelService.assignToCurrentChannel(customer, ctx);
const createdCustomer = await this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer);
await this.customFieldRelationService.updateRelations(ctx, customer_entity_1.Customer, input, createdCustomer);
await this.historyService.createHistoryEntryForCustomer({
ctx,
customerId: createdCustomer.id,
type: generated_types_1.HistoryEntryType.CUSTOMER_REGISTERED,
data: {
strategy: native_authentication_strategy_1.NATIVE_AUTH_STRATEGY_NAME,
},
});
if ((_a = customer.user) === null || _a === void 0 ? void 0 : _a.verified) {
await this.historyService.createHistoryEntryForCustomer({
ctx,
customerId: createdCustomer.id,
type: generated_types_1.HistoryEntryType.CUSTOMER_VERIFIED,
data: {
strategy: native_authentication_strategy_1.NATIVE_AUTH_STRATEGY_NAME,
},
});
}
await this.eventBus.publish(new customer_event_1.CustomerEvent(ctx, createdCustomer, 'created', input));
return createdCustomer;
}
async update(ctx, input) {
const hasEmailAddress = (i) => Object.hasOwnProperty.call(i, 'emailAddress');
const customer = await this.connection.getEntityOrThrow(ctx, customer_entity_1.Customer, input.id, {
channelId: ctx.channelId,
});
if (hasEmailAddress(input)) {
input.emailAddress = (0, utils_1.normalizeEmailAddress)(input.emailAddress);
if (input.emailAddress !== customer.emailAddress) {
const existingCustomerInChannel = await this.connection
.getRepository(ctx, customer_entity_1.Customer)
.createQueryBuilder('customer')
.leftJoin('customer.channels', 'channel')
.where('channel.id = :channelId', { channelId: ctx.channelId })
.andWhere('customer.emailAddress = :emailAddress', {
emailAddress: input.emailAddress,
})
.andWhere('customer.id != :customerId', { customerId: input.id })
.andWhere('customer.deletedAt is null')
.getOne();
if (existingCustomerInChannel) {
return new generated_graphql_admin_errors_1.EmailAddressConflictError();
}
if (customer.user) {
const existingUserWithEmailAddress = await this.userService.getUserByEmailAddress(ctx, input.emailAddress, 'customer');
if (existingUserWithEmailAddress &&
!(0, utils_1.idsAreEqual)(customer.user.id, existingUserWithEmailAddress.id)) {
return new generated_graphql_admin_errors_1.EmailAddressConflictError();
}
await this.userService.changeUserAndNativeIdentifier(ctx, customer.user.id, input.emailAddress);
}
}
}
const updatedCustomer = (0, patch_entity_1.patchEntity)(customer, input);
await this.connection.getRepository(ctx, customer_entity_1.Customer).save(updatedCustomer, { reload: false });
await this.customFieldRelationService.updateRelations(ctx, customer_entity_1.Customer, input, updatedCustomer);
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_DETAIL_UPDATED,
data: {
input,
},
});
await this.eventBus.publish(new customer_event_1.CustomerEvent(ctx, customer, 'updated', input));
return (0, utils_1.assertFound)(this.findOne(ctx, customer.id));
}
/**
* @description
* Registers a new Customer account with the {@link NativeAuthenticationStrategy} and starts
* the email verification flow (unless {@link AuthOptions} `requireVerification` is set to `false`)
* by publishing an {@link AccountRegistrationEvent}.
*
* This method is intended to be used in storefront Customer-creation flows.
*/
async registerCustomerAccount(ctx, input) {
if (!this.configService.authOptions.requireVerification) {
if (!input.password) {
return new generated_graphql_shop_errors_1.MissingPasswordError();
}
}
let user = await this.userService.getUserByEmailAddress(ctx, input.emailAddress);
const hasNativeAuthMethod = !!(user === null || user === void 0 ? void 0 : user.authenticationMethods.find(m => m instanceof native_authentication_method_entity_1.NativeAuthenticationMethod));
if (user && user.verified) {
if (hasNativeAuthMethod) {
// If the user has already been verified and has already
// registered with the native authentication strategy, do nothing.
return { success: true };
}
}
const customFields = input.customFields;
const customer = await this.createOrUpdate(ctx, Object.assign({ emailAddress: input.emailAddress, title: input.title || '', firstName: input.firstName || '', lastName: input.lastName || '', phoneNumber: input.phoneNumber || '' }, (customFields ? { customFields } : {})));
if ((0, error_result_1.isGraphQlErrorResult)(customer)) {
return customer;
}
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_REGISTERED,
data: {
strategy: native_authentication_strategy_1.NATIVE_AUTH_STRATEGY_NAME,
},
});
if (!user) {
const customerUser = await this.userService.createCustomerUser(ctx, input.emailAddress, input.password || undefined);
if ((0, error_result_1.isGraphQlErrorResult)(customerUser)) {
return customerUser;
}
else {
user = customerUser;
}
}
if (!hasNativeAuthMethod) {
const addAuthenticationResult = await this.userService.addNativeAuthenticationMethod(ctx, user, input.emailAddress, input.password || undefined);
if ((0, error_result_1.isGraphQlErrorResult)(addAuthenticationResult)) {
return addAuthenticationResult;
}
else {
user = addAuthenticationResult;
}
}
if (!user.verified) {
user = await this.userService.setVerificationToken(ctx, user);
}
customer.user = user;
await this.connection.getRepository(ctx, user_entity_1.User).save(user, { reload: false });
await this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer, { reload: false });
if (!user.verified) {
await this.eventBus.publish(new account_registration_event_1.AccountRegistrationEvent(ctx, user));
}
else {
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_VERIFIED,
data: {
strategy: native_authentication_strategy_1.NATIVE_AUTH_STRATEGY_NAME,
},
});
}
return { success: true };
}
/**
* @description
* Refreshes a stale email address verification token by generating a new one and
* publishing a {@link AccountRegistrationEvent}.
*/
async refreshVerificationToken(ctx, emailAddress) {
const user = await this.userService.getUserByEmailAddress(ctx, emailAddress);
if (user && !user.verified) {
await this.userService.setVerificationToken(ctx, user);
await this.eventBus.publish(new account_registration_event_1.AccountRegistrationEvent(ctx, user));
}
}
/**
* @description
* Given a valid verification token which has been published in an {@link AccountRegistrationEvent}, this
* method is used to set the Customer as `verified` as part of the account registration flow.
*/
async verifyCustomerEmailAddress(ctx, verificationToken, password) {
const result = await this.userService.verifyUserByToken(ctx, verificationToken, password);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
return result;
}
const customer = await this.findOneByUserId(ctx, result.id, false);
if (!customer) {
throw new errors_1.InternalServerError('error.cannot-locate-customer-for-user');
}
if (ctx.channelId) {
await this.channelService.assignToChannels(ctx, customer_entity_1.Customer, customer.id, [ctx.channelId]);
}
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_VERIFIED,
data: {
strategy: native_authentication_strategy_1.NATIVE_AUTH_STRATEGY_NAME,
},
});
const user = (0, utils_1.assertFound)(this.findOneByUserId(ctx, result.id));
await this.eventBus.publish(new account_verified_event_1.AccountVerifiedEvent(ctx, customer));
return user;
}
/**
* @description
* Publishes a new {@link PasswordResetEvent} for the given email address. This event creates
* a token which can be used in the `resetPassword()` method.
*/
async requestPasswordReset(ctx, emailAddress) {
const user = await this.userService.setPasswordResetToken(ctx, emailAddress);
if (user) {
await this.eventBus.publish(new password_reset_event_1.PasswordResetEvent(ctx, user));
const customer = await this.findOneByUserId(ctx, user.id);
if (!customer) {
throw new errors_1.InternalServerError('error.cannot-locate-customer-for-user');
}
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_PASSWORD_RESET_REQUESTED,
data: {},
});
}
}
/**
* @description
* Given a valid password reset token created by a call to the `requestPasswordReset()` method,
* this method will change the Customer's password to that given as the `password` argument.
*/
async resetPassword(ctx, passwordResetToken, password) {
const result = await this.userService.resetPasswordByToken(ctx, passwordResetToken, password);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
return result;
}
const customer = await this.findOneByUserId(ctx, result.id);
if (!customer) {
throw new errors_1.InternalServerError('error.cannot-locate-customer-for-user');
}
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED,
data: {},
});
await this.eventBus.publish(new password_reset_verified_event_1.PasswordResetVerifiedEvent(ctx, result));
return result;
}
/**
* @description
* Publishes a {@link IdentifierChangeRequestEvent} for the given User. This event contains a token
* which is then used in the `updateEmailAddress()` method to change the email address of the User &
* Customer.
*/
async requestUpdateEmailAddress(ctx, userId, newEmailAddress) {
const normalizedEmailAddress = (0, utils_1.normalizeEmailAddress)(newEmailAddress);
const userWithConflictingIdentifier = await this.userService.getUserByEmailAddress(ctx, newEmailAddress);
if (userWithConflictingIdentifier) {
return new generated_graphql_shop_errors_1.EmailAddressConflictError();
}
const user = await this.userService.getUserById(ctx, userId);
if (!user) {
return false;
}
const customer = await this.findOneByUserId(ctx, user.id);
if (!customer) {
return false;
}
const oldEmailAddress = customer.emailAddress;
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_EMAIL_UPDATE_REQUESTED,
data: {
oldEmailAddress,
newEmailAddress: normalizedEmailAddress,
},
});
if (this.configService.authOptions.requireVerification) {
user.getNativeAuthenticationMethod().pendingIdentifier = normalizedEmailAddress;
await this.userService.setIdentifierChangeToken(ctx, user);
await this.eventBus.publish(new identifier_change_request_event_1.IdentifierChangeRequestEvent(ctx, user));
return true;
}
else {
const oldIdentifier = user.identifier;
user.identifier = normalizedEmailAddress;
customer.emailAddress = normalizedEmailAddress;
await this.connection.getRepository(ctx, user_entity_1.User).save(user, { reload: false });
await this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer, { reload: false });
await this.eventBus.publish(new identifier_change_event_1.IdentifierChangeEvent(ctx, user, oldIdentifier));
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
data: {
oldEmailAddress,
newEmailAddress: normalizedEmailAddress,
},
});
return true;
}
}
/**
* @description
* Given a valid email update token published in a {@link IdentifierChangeRequestEvent}, this method
* will update the Customer & User email address.
*/
async updateEmailAddress(ctx, token) {
const result = await this.userService.changeIdentifierByToken(ctx, token);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
return result;
}
const { user, oldIdentifier } = result;
if (!user) {
return false;
}
const customer = await this.findOneByUserId(ctx, user.id);
if (!customer) {
return false;
}
await this.eventBus.publish(new identifier_change_event_1.IdentifierChangeEvent(ctx, user, oldIdentifier));
customer.emailAddress = user.identifier;
await this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer, { reload: false });
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
data: {
oldEmailAddress: oldIdentifier,
newEmailAddress: customer.emailAddress,
},
});
return true;
}
/**
* @description
* For guest checkouts, we assume that a matching email address is the same customer.
*/
async createOrUpdate(ctx, input, errorOnExistingUser = false) {
input.emailAddress = (0, utils_1.normalizeEmailAddress)(input.emailAddress);
let customer;
const existing = await this.connection.getRepository(ctx, customer_entity_1.Customer).findOne({
relations: ['channels'],
where: {
emailAddress: input.emailAddress,
deletedAt: (0, typeorm_1.IsNull)(),
},
});
if (existing) {
if (existing.user && errorOnExistingUser) {
// It is not permitted to modify an existing *registered* Customer
return new generated_graphql_shop_errors_1.EmailAddressConflictError();
}
customer = (0, patch_entity_1.patchEntity)(existing, input);
customer.channels.push(await this.connection.getEntityOrThrow(ctx, channel_entity_1.Channel, ctx.channelId));
}
else {
customer = await this.connection.getRepository(ctx, customer_entity_1.Customer).save(new customer_entity_1.Customer(input));
await this.channelService.assignToCurrentChannel(customer, ctx);
await this.eventBus.publish(new customer_event_1.CustomerEvent(ctx, customer, 'created', input));
}
return this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer);
}
/**
* @description
* Creates a new {@link Address} for the given Customer.
*/
async createAddress(ctx, customerId, input) {
const customer = await this.connection.getEntityOrThrow(ctx, customer_entity_1.Customer, customerId, {
where: { deletedAt: (0, typeorm_1.IsNull)() },
relations: ['addresses'],
channelId: ctx.channelId,
});
const country = await this.countryService.findOneByCode(ctx, input.countryCode);
const address = new address_entity_1.Address(Object.assign(Object.assign({}, input), { country }));
const createdAddress = await this.connection.getRepository(ctx, address_entity_1.Address).save(address);
await this.customFieldRelationService.updateRelations(ctx, address_entity_1.Address, input, createdAddress);
customer.addresses.push(createdAddress);
await this.connection.getRepository(ctx, customer_entity_1.Customer).save(customer, { reload: false });
await this.enforceSingleDefaultAddress(ctx, createdAddress.id, input);
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_ADDRESS_CREATED,
data: { address: (0, address_to_line_1.addressToLine)(createdAddress) },
});
createdAddress.customer = customer;
await this.eventBus.publish(new customer_address_event_1.CustomerAddressEvent(ctx, createdAddress, 'created', input));
return createdAddress;
}
async updateAddress(ctx, input) {
const address = await this.connection.getEntityOrThrow(ctx, address_entity_1.Address, input.id, {
relations: ['customer', 'country'],
});
const customer = await this.connection.findOneInChannel(ctx, customer_entity_1.Customer, address.customer.id, ctx.channelId);
if (!customer) {
throw new errors_1.EntityNotFoundError('Address', input.id);
}
if (input.countryCode && input.countryCode !== address.country.code) {
address.country = await this.countryService.findOneByCode(ctx, input.countryCode);
}
else {
address.country = this.translator.translate(address.country, ctx);
}
let updatedAddress = (0, patch_entity_1.patchEntity)(address, input);
updatedAddress = await this.connection.getRepository(ctx, address_entity_1.Address).save(updatedAddress);
await this.customFieldRelationService.updateRelations(ctx, address_entity_1.Address, input, updatedAddress);
await this.enforceSingleDefaultAddress(ctx, input.id, input);
await this.historyService.createHistoryEntryForCustomer({
customerId: address.customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_ADDRESS_UPDATED,
data: {
address: (0, address_to_line_1.addressToLine)(updatedAddress),
input,
},
});
updatedAddress.customer = customer;
await this.eventBus.publish(new customer_address_event_1.CustomerAddressEvent(ctx, updatedAddress, 'updated', input));
return updatedAddress;
}
async deleteAddress(ctx, id) {
const address = await this.connection.getEntityOrThrow(ctx, address_entity_1.Address, id, {
relations: ['customer', 'country'],
});
const customer = await this.connection.findOneInChannel(ctx, customer_entity_1.Customer, address.customer.id, ctx.channelId);
if (!customer) {
throw new errors_1.EntityNotFoundError('Address', id);
}
address.country = this.translator.translate(address.country, ctx);
await this.reassignDefaultsForDeletedAddress(ctx, address);
await this.historyService.createHistoryEntryForCustomer({
customerId: address.customer.id,
ctx,
type: generated_types_1.HistoryEntryType.CUSTOMER_ADDRESS_DELETED,
data: {
address: (0, address_to_line_1.addressToLine)(address),
},
});
const deletedAddress = new address_entity_1.Address(address);
await this.connection.getRepository(ctx, address_entity_1.Address).remove(address);
address.customer = customer;
await this.eventBus.publish(new customer_address_event_1.CustomerAddressEvent(ctx, deletedAddress, 'deleted', id));
return true;
}
async softDelete(ctx, customerId) {
const customer = await this.connection.getEntityOrThrow(ctx, customer_entity_1.Customer, customerId, {
channelId: ctx.channelId,
});
await this.connection
.getRepository(ctx, customer_entity_1.Customer)
.update({ id: customerId }, { deletedAt: new Date() });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (customer.user) {
await this.userService.softDelete(ctx, customer.user.id);
}
await this.eventBus.publish(new customer_event_1.CustomerEvent(ctx, customer, 'deleted', customerId));
return {
result: generated_types_1.DeletionResult.DELETED,
};
}
/**
* @description
* If the Customer associated with the given Order does not yet have any Addresses,
* this method will create new Address(es) based on the Order's shipping & billing
* addresses.
*/
async createAddressesForNewCustomer(ctx, order) {
var _a;
if (!order.customer) {
return;
}
const addresses = await this.findAddressesByCustomerId(ctx, order.customer.id);
// If the Customer has no addresses yet, use the shipping/billing address data
// to populate the initial default Address.
if (addresses.length === 0 && ((_a = order.shippingAddress) === null || _a === void 0 ? void 0 : _a.country)) {
const shippingAddress = order.shippingAddress;
const billingAddress = order.billingAddress;
const hasSeparateBillingAddress = (billingAddress === null || billingAddress === void 0 ? void 0 : billingAddress.streetLine1) && !this.addressesAreEqual(shippingAddress, billingAddress);
if (shippingAddress.streetLine1) {
await this.createAddress(ctx, order.customer.id, Object.assign(Object.assign({}, shippingAddress), { company: shippingAddress.company || '', streetLine1: shippingAddress.streetLine1 || '', streetLine2: shippingAddress.streetLine2 || '', countryCode: shippingAddress.countryCode || '', defaultBillingAddress: !hasSeparateBillingAddress, defaultShippingAddress: true }));
}
if (hasSeparateBillingAddress) {
await this.createAddress(ctx, order.customer.id, Object.assign(Object.assign({}, billingAddress), { company: billingAddress.company || '', streetLine1: billingAddress.streetLine1 || '', streetLine2: billingAddress.streetLine2 || '', countryCode: billingAddress.countryCode || '', defaultBillingAddress: true, defaultShippingAddress: false }));
}
}
}
addressesAreEqual(address1, address2) {
return (address1.streetLine1 === address2.streetLine1 &&
address1.streetLine2 === address2.streetLine2 &&
address1.postalCode === address2.postalCode);
}
async addNoteToCustomer(ctx, input) {
const customer = await this.connection.getEntityOrThrow(ctx, customer_entity_1.Customer, input.id, {
channelId: ctx.channelId,
});
await this.historyService.createHistoryEntryForCustomer({
ctx,
customerId: customer.id,
type: generated_types_1.HistoryEntryType.CUSTOMER_NOTE,
data: {
note: input.note,
},
}, input.isPublic);
return customer;
}
async updateCustomerNote(ctx, input) {
return this.historyService.updateCustomerHistoryEntry(ctx, {
type: generated_types_1.HistoryEntryType.CUSTOMER_NOTE,
data: input.note ? { note: input.note } : undefined,
ctx,
entryId: input.noteId,
});
}
async deleteCustomerNote(ctx, id) {
try {
await this.historyService.deleteCustomerHistoryEntry(ctx, id);
return {
result: generated_types_1.DeletionResult.DELETED,
};
}
catch (e) {
return {
result: generated_types_1.DeletionResult.NOT_DELETED,
message: e.message,
};
}
}
async enforceSingleDefaultAddress(ctx, addressId, input) {
const result = await this.connection
.getRepository(ctx, address_entity_1.Address)
.findOne({ where: { id: addressId }, relations: ['customer', 'customer.addresses'] });
if (result) {
const customerAddressIds = result.customer.addresses
.map(a => a.id)
.filter(id => !(0, utils_1.idsAreEqual)(id, addressId));
if (customerAddressIds.length) {
if (input.defaultBillingAddress === true) {
await this.connection.getRepository(ctx, address_entity_1.Address).update(customerAddressIds, {
defaultBillingAddress: false,
});
}
if (input.defaultShippingAddress === true) {
await this.connection.getRepository(ctx, address_entity_1.Address).update(customerAddressIds, {
defaultShippingAddress: false,
});
}
}
}
}
/**
* If a Customer Address is to be deleted, check if it is assigned as a default for shipping or
* billing. If so, attempt to transfer default status to one of the other addresses if there are
* any.
*/
async reassignDefaultsForDeletedAddress(ctx, addressToDelete) {
if (!addressToDelete.defaultBillingAddress && !addressToDelete.defaultShippingAddress) {
return;
}
const result = await this.connection
.getRepository(ctx, address_entity_1.Address)
.findOne({ where: { id: addressToDelete.id }, relations: ['customer', 'customer.addresses'] });
if (result) {
const customerAddresses = result.customer.addresses;
if (1 < customerAddresses.length) {
const otherAddresses = customerAddresses
.filter(address => !(0, utils_1.idsAreEqual)(address.id, addressToDelete.id))
.sort((a, b) => (a.id < b.id ? -1 : 1));
if (addressToDelete.defaultShippingAddress) {
otherAddresses[0].defaultShippingAddress = true;
}
if (addressToDelete.defaultBillingAddress) {
otherAddresses[0].defaultBillingAddress = true;
}
await this.connection.getRepository(ctx, address_entity_1.Address).save(otherAddresses[0], { reload: false });
}
}
}
};
exports.CustomerService = CustomerService;
exports.CustomerService = CustomerService = __decorate([
(0, common_1.Injectable)(),
(0, instrument_decorator_1.Instrument)(),
__metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection,
config_service_1.ConfigService,
user_service_1.UserService,
country_service_1.CountryService,
list_query_builder_1.ListQueryBuilder,
event_bus_1.EventBus,
history_service_1.HistoryService,
channel_service_1.ChannelService,
custom_field_relation_service_1.CustomFieldRelationService,
translator_service_1.TranslatorService])
], CustomerService);
//# sourceMappingURL=customer.service.js.map