Merged PR 860: パートナー情報更新API実装

## 概要
[Task3937: パートナー情報更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3937)

- パートナーアカウント情報更新APIとUTを実装しました。

## レビューポイント
- エラーケースの出し分けは適切でしょうか?
- テストケースは過不足ないでしょうか?

## UIの変更
- なし

## クエリの変更
- 新規追加のため変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規追加なので問題なし。
This commit is contained in:
maruyama.t 2024-04-05 02:37:58 +00:00 committed by makabe.t
parent 0288292058
commit 915483c109
9 changed files with 844 additions and 4 deletions

View File

@ -88,4 +88,5 @@ export const ErrorCodes = [
'E017004', // 親アカウント変更不可エラー(国が同一でない)
'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない)
'E019001', // パートナーアカウント取得不可エラー(階層構造が不正)
'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない)
] as const;

View File

@ -78,4 +78,5 @@ export const errors: Errors = {
E017004: 'Parent account switch failed Error: country mismatch',
E018001: 'Partner account delete failed Error: not satisfied conditions',
E019001: 'Partner account get failed Error: hierarchy mismatch',
E020001: 'Partner account change failed Error: not satisfied conditions',
};

View File

@ -2602,7 +2602,7 @@ export class AccountsController {
@Req() req: Request,
@Body() body: UpdatePartnerInfoRequest,
): Promise<UpdatePartnerInfoResponse> {
const { targetAccountId } = body;
const { targetAccountId, primaryAdminUserId, companyName } = body;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
@ -2639,12 +2639,14 @@ export class AccountsController {
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
// TODO: 仮実装
/*await this.accountService.updatePartnerAccount(
await this.accountService.updatePartnerInfo(
context,
userId,
targetAccountId,
primaryAdminUserId,
companyName,
);
*/
return {};
}
}

View File

@ -9635,3 +9635,492 @@ describe('getPartnerUsers', () => {
}
});
});
describe('updatePartnerInfo', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
logger: new TestLogger('none'),
logging: true,
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {
await truncateAllTable(source);
}
});
afterAll(async () => {
await source?.destroy();
source = null;
});
it('パートナーアカウントの会社名を変更できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 新規親アカウントのアカウントを作成する
const { account: parent, admin: parentAdmin } = await makeTestAccount(
source,
{ tier: 3 },
{ external_id: 'parent_external_id' },
);
// 子アカウントを作成する
const { account: partner, admin: partnerAdmin } = await makeTestAccount(
source,
{
tier: 4,
parent_account_id: parent.id,
company_name: 'oldCompanyName',
},
);
// 作成したデータを確認
{
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.company_name).toBe('oldCompanyName');
}
const service = module.get<AccountsService>(AccountsService);
let _subject = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
_subject = subject;
},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
});
// テスト実行
const context = makeContext(parentAdmin.external_id, 'requestId');
await service.updatePartnerInfo(
context,
parentAdmin.external_id,
partner.id,
partnerAdmin.id,
'newCompanyName',
);
{
// DB内が想定通りになっているか確認
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.company_name).toBe('newCompanyName');
// パートナーアカウント情報変更完了通知が送信されていること
expect(_subject).toBe('Partner Account Edit Notification [U-124]');
}
});
it('パートナーアカウントのプライマリ管理者を変更できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 新規親アカウントのアカウントを作成する
const { account: parent, admin: parentAdmin } = await makeTestAccount(
source,
{ tier: 3 },
{ external_id: 'parent_external_id' },
);
// 子アカウントを作成する
const { account: partner, admin: partnerAdmin } = await makeTestAccount(
source,
{
tier: 4,
parent_account_id: parent.id,
},
);
const newPartnerAdmin = await makeTestUser(source, {
account_id: partner.id,
});
// 作成したデータを確認
{
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id);
}
const service = module.get<AccountsService>(AccountsService);
let _subject = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
_subject = subject;
},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
});
// テスト実行
const context = makeContext(parentAdmin.external_id, 'requestId');
await service.updatePartnerInfo(
context,
parentAdmin.external_id,
partner.id,
newPartnerAdmin.id,
partner.company_name,
);
{
// DB内が想定通りになっているか確認
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.primary_admin_user_id).toBe(newPartnerAdmin.id);
// パートナーアカウント情報変更完了通知が送信されていること
expect(_subject).toBe('Partner Account Edit Notification [U-124]');
}
});
it('変更対象アカウントが実行者のパートナーアカウントでない場合、エラーなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 新規親アカウントのアカウントを作成する
const { admin: parentAdmin } = await makeTestAccount(
source,
{ tier: 3 },
{ external_id: 'parent_external_id' },
);
// 子アカウントを作成する
const { account: partner, admin: partnerAdmin } = await makeTestAccount(
source,
{ tier: 4 },
);
// 作成したデータを確認
{
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id);
}
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
});
// テスト実行
const context = makeContext(parentAdmin.external_id, 'requestId');
try {
await service.updatePartnerInfo(
context,
parentAdmin.external_id,
partner.id,
partnerAdmin.id,
partner.company_name,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E020001'));
} else {
fail();
}
}
});
it('DBアクセスがエラーの場合、エラーなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 新規親アカウントのアカウントを作成する
const { account: parent, admin: parentAdmin } = await makeTestAccount(
source,
{ tier: 3 },
{ external_id: 'parent_external_id' },
);
// 子アカウントを作成する
const { account: partner, admin: partnerAdmin } = await makeTestAccount(
source,
{ tier: 4, parent_account_id: parent.id },
);
// 作成したデータを確認
{
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id);
}
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
});
//DBアクセスに失敗するようにする
const accountsRepositoryService = module.get<AccountsRepositoryService>(
AccountsRepositoryService,
);
accountsRepositoryService.updatePartnerInfo = jest
.fn()
.mockRejectedValue('DB failed');
// テスト実行
const context = makeContext(parentAdmin.external_id, 'requestId');
try {
await service.updatePartnerInfo(
context,
parentAdmin.external_id,
partner.id,
partnerAdmin.id,
partner.company_name,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
it('メール送信に失敗した場合でも、エラーとならず成功となること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 新規親アカウントのアカウントを作成する
const { account: parent, admin: parentAdmin } = await makeTestAccount(
source,
{ tier: 3 },
{ external_id: 'parent_external_id' },
);
// 子アカウントを作成する
const { account: partner, admin: partnerAdmin } = await makeTestAccount(
source,
{ tier: 4, parent_account_id: parent.id, company_name: 'oldCompanyName' },
);
// 作成したデータを確認
{
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.company_name).toBe('oldCompanyName');
}
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
throw new Error('sendMail failed');
},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
});
// テスト実行
const context = makeContext(parentAdmin.external_id, 'requestId');
await service.updatePartnerInfo(
context,
parentAdmin.external_id,
partner.id,
partnerAdmin.id,
'newCompanyName',
);
{
// DB内が想定通りになっているか確認
const partnerRecord = await getAccount(source, partner.id);
expect(partnerRecord?.company_name).toBe('newCompanyName');
}
});
});

View File

@ -3090,4 +3090,124 @@ export class AccountsService {
);
}
}
/**
*
* @param context
* @param externalId
* @param targetAccountId // 更新対象のアカウントID
* @param primaryAdminUserId // 更新後のプライマリ管理者のユーザーID
* @param companyName // 更新後の会社名
* @returns partner account
*/
async updatePartnerInfo(
context: Context,
externalId: string,
targetAccountId: number,
primaryAdminUserId: number,
companyName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updatePartnerInfo.name
} | params: { ` +
`externalId: ${externalId}, ` +
`targetAccountId: ${targetAccountId}, ` +
`primaryAdminUserId: ${primaryAdminUserId}, ` +
`companyName: ${companyName}, };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account: parentAccount } =
await this.usersRepository.findUserByExternalId(context, externalId);
if (parentAccount === null) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
// アカウント情報更新処理
await this.accountRepository.updatePartnerInfo(
context,
parentAccount.id,
targetAccountId,
primaryAdminUserId,
companyName,
);
// メール送信処理
try {
// 実行者のアカウント情報
const { companyName: parentCompanyName, adminEmails: parentEmails } =
await this.getAccountInformation(context, parentAccount.id);
// 更新後のパートナーのプライマリ管理者
const primaryAdmin = await this.usersRepository.findUserById(
context,
primaryAdminUserId,
);
const adb2cAdmin = await this.adB2cService.getUser(
context,
primaryAdmin.external_id,
);
const {
displayName: primaryAdminName,
emailAddress: primaryAdminEmail,
} = getUserNameAndMailAddress(adb2cAdmin);
if (!primaryAdminEmail) {
throw new Error(
`adb2c user mail not found. externalId: ${primaryAdmin.external_id}`,
);
}
await this.sendgridService.sendMailWithU124(
context,
companyName,
primaryAdminName,
primaryAdminEmail,
parentCompanyName,
parentEmails,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
case AdminUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010502'),
HttpStatus.BAD_REQUEST,
);
case HierarchyMismatchError:
throw new HttpException(
makeErrorResponse('E020001'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updatePartnerInfo.name}`,
);
}
}
}

View File

@ -89,6 +89,8 @@ export class SendGridService {
private readonly templateU122NoParentText: string;
private readonly templateU123Html: string;
private readonly templateU123Text: string;
private readonly templateU124Html: string;
private readonly templateU124Text: string;
constructor(private readonly configService: ConfigService) {
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
@ -338,6 +340,14 @@ export class SendGridService {
path.resolve(__dirname, `../../templates/template_U_123.txt`),
'utf-8',
);
this.templateU124Html = readFileSync(
path.resolve(__dirname, `../../templates/template_U_124.html`),
'utf-8',
);
this.templateU124Text = readFileSync(
path.resolve(__dirname, `../../templates/template_U_124.txt`),
'utf-8',
);
}
}
@ -1488,6 +1498,59 @@ export class SendGridService {
}
}
/**
* U-124使
* @param context
* @param partnerAccountName
* @param partnerPrimaryName
* @param partnerPrimaryMail
* @param dealerAccountName
* @param dealerEmails
* @returns mail with u124
*/
async sendMailWithU124(
context: Context,
partnerAccountName: string,
partnerPrimaryName: string,
partnerPrimaryMail: string,
dealerAccountName: string,
dealerEmails: string[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`,
);
try {
const subject = 'Partner Account Edit Notification [U-124]';
const url = new URL(this.appDomain).href;
const html = this.templateU124Html
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(TOP_URL, url);
const text = this.templateU124Text
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(TOP_URL, url);
// メールを送信する
await this.sendMail(
context,
[partnerPrimaryMail],
dealerEmails,
this.mailFrom,
subject,
text,
html,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`,
);
}
}
/**
*
* @param context

View File

@ -35,6 +35,7 @@ import {
AccountNotFoundError,
AdminUserNotFoundError,
DealerAccountNotFoundError,
HierarchyMismatchError,
PartnerAccountDeletionError,
} from './errors/types';
import {
@ -1768,4 +1769,74 @@ export class AccountsRepositoryService {
return users;
});
}
/**
*
* @param context
* @param parentAccountId
* @param targetAccountId
* @param primaryAdminUserId
* @param companyName
* @returns partner info
*/
async updatePartnerInfo(
context: Context,
parentAccountId: number,
targetAccountId: number,
primaryAdminUserId: number,
companyName: string,
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepo = entityManager.getRepository(Account);
const userRepo = entityManager.getRepository(User);
// 指定したプライマリ管理者が対象アカウント内に存在するかチェック
const primaryAdminUser = await userRepo.findOne({
where: {
id: primaryAdminUserId,
account_id: targetAccountId,
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!primaryAdminUser) {
throw new AdminUserNotFoundError(
`Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${targetAccountId}`,
);
}
// 対象アカウントが存在するかチェック
const targetAccount = await accountRepo.findOne({
where: { id: targetAccountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!targetAccount) {
throw new AccountNotFoundError(
`Account is not found. id: ${targetAccountId}`,
);
}
// 実行者のアカウントが対象アカウントの親アカウントであるか確認する
if (parentAccountId !== targetAccount.parent_account_id) {
throw new HierarchyMismatchError(
`Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`,
);
}
// accountsテーブルレコード更新
await updateEntity(
accountRepo,
{ id: targetAccountId },
{
company_name: companyName,
primary_admin_user_id: primaryAdminUserId,
},
this.isCommentOut,
context,
);
});
}
}

View File

@ -0,0 +1,58 @@
<html>
<head>
<title>Storage Usage Exceeded Notification [U-119]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$</p>
<p>Your account information has been edited by $DEALER_NAME$.</p>
<p>
To check or change your account information, please log in to the ODMS
Cloud.<br />
URL: $TOP_URL$
</p>
<p>
If you received this e-mail in error, please delete this e-mail from
your system.<br />
This is an automatically generated e-mail and this mailbox is not
monitored. Please do not reply.
</p>
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$</p>
<p>Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet.</p>
<p>
Um Ihre Kontoinformationen zu überprüfen oder zu ändern, melden Sie sich
bitte bei der ODMS Cloud an.<br />
URL: $TOP_URL$
</p>
<p>
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese
E-Mail bitte aus Ihrem System.<br />
Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht
überwacht. Bitte antworten Sie nicht.
</p>
</div>
<div>
<h3>&lt;Français&gt;</h3>
<p>Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$</p>
<p>
Les informations de votre compte ont été modifiées par $DEALER_NAME$.
</p>
<p>
Pour vérifier ou modifier les informations de votre compte, veuillez
vous connecter au ODMS Cloud.<br />
URL : $TOP_URL$
</p>
<p>
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail
de votre système.<br />
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres
n'est pas surveillée. Merci de ne pas répondre.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,35 @@
<English>
Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$
Your account information has been edited by $DEALER_NAME$.
To check or change your account information, please log in to the ODMS Cloud.
URL: $TOP_URL$
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
<Deutsch>
Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$
Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet.
Um Ihre Kontoinformationen zu überprüfen oder zu ändern, melden Sie sich bitte bei der ODMS Cloud an.
URL: $TOP_URL$
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht.
<Français>
Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$
Les informations de votre compte ont été modifiées par $DEALER_NAME$.
Pour vérifier ou modifier les informations de votre compte, veuillez vous connecter au ODMS Cloud.
URL : $TOP_URL$
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.