Merged PR 797: API実装(一括登録)

## 概要
[Task3752: API実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3752)

- ユーザー一括登録APIとテストを実装しました。
  - メール文面はまだ翻訳が来ていないので日本語のものを使用しています。別タスクで多言語対応します。

## レビューポイント
- ファイル名は認識通りでしょうか?
- ファイルの内容は認識通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2024-03-05 10:27:50 +00:00
parent 7ff563f644
commit 31de71f743
14 changed files with 734 additions and 9 deletions

View File

@ -20,12 +20,15 @@ STORAGE_TOKEN_EXPIRE_TIME=30
STORAGE_ACCOUNT_NAME_US=saxxxxusxxx
STORAGE_ACCOUNT_NAME_AU=saxxxxauxxx
STORAGE_ACCOUNT_NAME_EU=saxxxxeuxxx
STORAGE_ACCOUNT_NAME_IMPORTS=saxxxximportsxxx
STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_KEY_IMPORTS=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
STORAGE_ACCOUNT_ENDPOINT_US=https://xxxxxxxxxxxx.blob.core.windows.net/
STORAGE_ACCOUNT_ENDPOINT_AU=https://xxxxxxxxxxxx.blob.core.windows.net/
STORAGE_ACCOUNT_ENDPOINT_EU=https://xxxxxxxxxxxx.blob.core.windows.net/
STORAGE_ACCOUNT_ENDPOINT_IMPORTS=https://xxxxxxxxxxxx.blob.core.windows.net/
ACCESS_TOKEN_LIFETIME_WEB=7200000
REFRESH_TOKEN_LIFETIME_WEB=86400000
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000

View File

@ -180,6 +180,11 @@ export const overrideBlobstorageService = <TService>(
accountId: number,
country: string,
) => Promise<string>;
uploadImportsBlob?: (
context: Context,
fileName: string,
content: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -220,6 +225,12 @@ export const overrideBlobstorageService = <TService>(
writable: true,
});
}
if (overrides.uploadImportsBlob) {
Object.defineProperty(obj, obj.uploadImportsBlob.name, {
value: overrides.uploadImportsBlob,
writable: true,
});
}
};
/**

View File

@ -19,6 +19,7 @@ import {
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
import { ADB2C_SIGN_IN_TYPE } from '../../../constants';
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
export type SortCriteriaRepositoryMockValue = {
updateSortCriteria: SortCriteria | Error;
@ -89,6 +90,8 @@ export const makeUsersServiceMock = async (
return makeSortCriteriaRepositoryMock(
sortCriteriaRepositoryMockValue,
);
case BlobstorageService:
return {};
}
})
.compile();

View File

@ -4,7 +4,6 @@ import {
Get,
HttpException,
HttpStatus,
Ip,
Logger,
Post,
Query,
@ -1068,7 +1067,9 @@ export class UsersController {
const context = makeContext(userId, requestId, delegateUserId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
// TODO: 処理を実装
// 登録処理
const { users, filename } = body;
await this.usersService.multipleImports(context, userId, filename, users);
return {};
}

View File

@ -9,6 +9,7 @@ import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
@Module({
imports: [
@ -19,6 +20,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
AdB2cModule,
SendGridModule,
ConfigModule,
BlobstorageModule,
],
controllers: [UsersController],
providers: [UsersService, AuthService],

View File

@ -31,6 +31,7 @@ import { makeTestingModule } from '../../common/test/modules';
import { Context, makeContext } from '../../common/log';
import {
overrideAdB2cService,
overrideBlobstorageService,
overrideSendgridService,
overrideUsersRepositoryService,
} from '../../common/test/overrides';
@ -3656,6 +3657,206 @@ describe('UsersService.deleteUser', () => {
});
});
describe('UsersService.multipleImports', () => {
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が行われるため注意
});
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: dealer } = await makeTestAccount(source, {
company_name: 'dealerCompany',
tier: 4,
});
const { account, admin } = await makeTestAccount(source, {
tier: 5,
company_name: 'company',
parent_account_id: dealer.id,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
overrideAdB2cService(service, {
getUsers: async () => {
return [
{
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'admin@example.com',
},
],
},
];
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {
uploadImportsBlob: async () => {},
});
// メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。
const spy = jest
.spyOn(service['sendgridService'], 'sendMailWithU120')
.mockImplementation();
const now = new Date();
const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
const filename = 'fileName.csv';
const users = [
{
name: 'name1',
email: 'mail1@example.com',
role: 1,
authorId: 'HOGE1',
autoRenew: 0,
notification: 0,
encryption: 0,
encryptionPassword: 'string',
prompt: 0,
},
{
name: 'name2',
email: 'mail2@example.com',
role: 2,
autoRenew: 0,
notification: 0,
},
];
await service.multipleImports(context, admin.external_id, filename, users);
// メール送信メソッドが呼ばれているか確認
expect(spy).toHaveBeenCalledWith(
context,
['admin@example.com'],
account.company_name,
dealer.company_name,
date,
filename,
);
});
it('Blobアップロードに失敗した場合はエラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { account: dealer } = await makeTestAccount(source, {
company_name: 'dealerCompany',
tier: 4,
});
const { account, admin } = await makeTestAccount(source, {
tier: 5,
company_name: 'company',
parent_account_id: dealer.id,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
overrideAdB2cService(service, {
getUsers: async () => {
return [
{
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'admin@example.com',
},
],
},
];
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {
uploadImportsBlob: async () => {
throw new Error('error');
},
});
const now = new Date();
const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
const filename = 'fileName.csv';
const users = [
{
name: 'name1',
email: 'mail1@example.com',
role: 1,
authorId: 'HOGE1',
autoRenew: 0,
notification: 0,
encryption: 0,
encryptionPassword: 'string',
prompt: 0,
},
{
name: 'name2',
email: 'mail2@example.com',
role: 2,
autoRenew: 0,
notification: 0,
},
];
try {
await service.multipleImports(
context,
admin.external_id,
filename,
users,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});
describe('UsersService.multipleImportsComplate', () => {
let source: DataSource | null = null;

View File

@ -24,6 +24,7 @@ import {
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import {
MultipleImportUser,
GetRelationsResponse,
MultipleImportErrors,
User,
@ -63,12 +64,11 @@ import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
private readonly mailFrom: string;
private readonly appDomain: string;
constructor(
private readonly accountsRepository: AccountsRepositoryService,
private readonly usersRepository: UsersRepositoryService,
@ -77,10 +77,8 @@ export class UsersService {
private readonly adB2cService: AdB2cService,
private readonly configService: ConfigService,
private readonly sendgridService: SendGridService,
) {
this.mailFrom = this.configService.getOrThrow<string>('MAIL_FROM');
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
}
private readonly blobStorageService: BlobstorageService,
) {}
/**
* Confirms user
@ -1600,6 +1598,118 @@ export class UsersService {
}
}
/**
* Blobにアップロードする
* @param context
* @param externalId
* @param fileName
* @param users
* @returns imports
*/
async multipleImports(
context: Context,
externalId: string,
fileName: string,
users: MultipleImportUser[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.multipleImports.name
} | params: { externalId: ${externalId}, ` +
`fileName: ${fileName}, ` +
`users.length: ${users.length} };`,
);
try {
// ユーザー情報を取得
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
if (user == null) {
throw new Error(`user not found. externalId=${externalId}`);
}
const now = new Date();
// 日時を生成(YYYYMMDD_HHMMSS)
const dateTime =
`${now.getFullYear().toString().padStart(4, '0')}` +
`${(now.getMonth() + 1).toString().padStart(2, '0')}` + // 月は0から始まるため+1する
`${now.getDate().toString().padStart(2, '0')}` +
`_${now.getHours().toString().padStart(2, '0')}` +
`${now.getMinutes().toString().padStart(2, '0')}` +
`${now.getSeconds().toString().padStart(2, '0')}`;
// ファイル名を生成(U_YYYYMMDD_HHMMSS_アカウントID_ユーザーID.json)
const jsonFileName = `U_${dateTime}_${user.account_id}_${user.id}.json`;
// ユーザー情報をJSON形式に変換
const usersJson = JSON.stringify({
account_id: user.account_id,
user_id: user.id,
user_role: user.role,
external_id: user.external_id,
file_name: fileName,
date: Math.floor(now.getTime() / 1000),
data: users,
});
// Blobにファイルをアップロードユーザー一括登録用
await this.blobStorageService.uploadImportsBlob(
context,
jsonFileName,
usersJson,
);
// 受付完了メールを送信
try {
// アカウント・管理者情報を取得
const { adminEmails, companyName } = await this.getAccountInformation(
context,
user.account_id,
);
// Dealer情報を取得
const dealer = await this.accountsRepository.findParentAccount(
context,
user.account_id,
);
const dealerName = dealer?.company_name ?? null;
const now = new Date();
// 日時を生成(YYYY.MM.DD)
const date = `${now.getFullYear()}.${
now.getMonth() + 1 // 月は0から始まるため+1する
}.${now.getDate()}`;
await this.sendgridService.sendMailWithU120(
context,
adminEmails,
companyName,
dealerName,
date,
fileName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.multipleImports.name}`,
);
}
}
/**
* IDを指定して
* @param context

View File

@ -22,9 +22,11 @@ export class BlobstorageService {
private readonly blobServiceClientUS: BlobServiceClient;
private readonly blobServiceClientEU: BlobServiceClient;
private readonly blobServiceClientAU: BlobServiceClient;
private readonly blobServiceClientImports: BlobServiceClient;
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
private readonly sharedKeyCredentialImports: StorageSharedKeyCredential;
private readonly sasTokenExpireHour: number;
constructor(private readonly configService: ConfigService) {
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
@ -39,6 +41,11 @@ export class BlobstorageService {
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_EU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_EU'),
);
this.sharedKeyCredentialImports = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_IMPORTS'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_IMPORTS'),
);
this.blobServiceClientUS = new BlobServiceClient(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_US'),
this.sharedKeyCredentialUS,
@ -51,6 +58,10 @@ export class BlobstorageService {
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.sharedKeyCredentialEU,
);
this.blobServiceClientImports = new BlobServiceClient(
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_IMPORTS'),
this.sharedKeyCredentialImports,
);
this.sasTokenExpireHour = this.configService.getOrThrow<number>(
'STORAGE_TOKEN_EXPIRE_TIME',
);
@ -457,6 +468,45 @@ export class BlobstorageService {
return url.toString();
}
/**
* Blobを作成します
* @param context
* @param fileName
* @param content
* @returns imports blob
*/
async uploadImportsBlob(
context: Context,
fileName: string,
content: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.uploadImportsBlob.name
} | params: { fileName: ${fileName} };`,
);
try {
const containerClient =
this.blobServiceClientImports.getContainerClient('import-users');
const blockBlobClient = containerClient.getBlockBlobClient(fileName);
const result = await blockBlobClient.upload(content, content.length);
if (result.errorCode) {
throw new Error(
`upload failed. errorCode: ${result.errorCode} fileName: ${fileName}`,
);
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw e;
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.uploadImportsBlob.name}`,
);
}
}
/**
* Gets container client
* @param companyName

View File

@ -75,6 +75,10 @@ export class SendGridService {
private readonly templateU119Text: string;
private readonly templateU119NoParentHtml: string;
private readonly templateU119NoParentText: string;
private readonly templateU120Html: string;
private readonly templateU120Text: string;
private readonly templateU120NoParentHtml: string;
private readonly templateU120NoParentText: string;
private readonly templateU121Html: string;
private readonly templateU121Text: string;
private readonly templateU121NoParentHtml: string;
@ -267,6 +271,25 @@ export class SendGridService {
path.resolve(__dirname, `../../templates/template_U_119_no_parent.txt`),
'utf-8',
);
this.templateU120Html = readFileSync(
path.resolve(__dirname, `../../templates/template_U_120.html`),
'utf-8',
);
this.templateU120Text = readFileSync(
path.resolve(__dirname, `../../templates/template_U_120.txt`),
'utf-8',
);
this.templateU120NoParentHtml = readFileSync(
path.resolve(
__dirname,
`../../templates/template_U_120_no_parent.html`,
),
'utf-8',
);
this.templateU120NoParentText = readFileSync(
path.resolve(__dirname, `../../templates/template_U_120_no_parent.txt`),
'utf-8',
);
this.templateU121Html = readFileSync(
path.resolve(__dirname, `../../templates/template_U_121.html`),
'utf-8',
@ -1188,6 +1211,72 @@ export class SendGridService {
}
}
/**
* U-120使
* @param context
* @param customerAdminMails (primary/secondary)
* @param customerAccountName
* @param dealerAccountName
* @param tire1AdminMails (primary/secondary)
* @returns mail with u119
*/
async sendMailWithU120(
context: Context,
customerAdminMails: string[],
customerAccountName: string,
dealerAccountName: string | null,
requestTime: string,
fileName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`,
);
try {
const subject = 'ユーザー一括登録 受付通知 [U-120]';
let html: string;
let text: string;
console.log(dealerAccountName);
if (!dealerAccountName) {
html = this.templateU120NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU120NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
} else {
html = this.templateU120Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU120Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
}
// メールを送信する
await this.sendMail(
context,
customerAdminMails,
[],
this.mailFrom,
subject,
text,
html,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`,
);
}
}
/**
* U-121使
* @param context

View File

@ -12,7 +12,6 @@ export const FILE_NAME = '$FILE_NAME$';
export const TYPIST_NAME = '$TYPIST_NAME$';
export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$';
export const REQUEST_TIME = '$REQUEST_TIME$';
export const EMAIL_DUPLICATION = `$EMAIL_DUPLICATION$`;
export const AUTHOR_ID_DUPLICATION = `$AUTHOR_ID_DUPLICATION$`;
export const UNEXPECTED_ERROR = `$UNEXPECTED_ERROR$`;

View File

@ -0,0 +1,80 @@
<html>
<head>
<title>ユーザー一括登録 受付通知 [U-120]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</p>
<p>
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$
にお問い合わせください。
</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, please do not reply.
</p>
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</p>
<p>
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$
にお問い合わせください。
</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, please do not reply.
</p>
</div>
<div>
<h3>&lt;Français&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</p>
<p>
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$
にお問い合わせください。
</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, please do not reply.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,57 @@
<English>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.
<Deutsch>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.
<Français>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.

View File

@ -0,0 +1,68 @@
<html>
<head>
<title>ユーザー一括登録 受付通知 [U-120]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</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, please do not reply.
</p>
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</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, please do not reply.
</p>
</div>
<div>
<h3>&lt;Français&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>ODMS Cloudをご利用いただきありがとうございます。</p>
<p>
CSVファイルによるユーザー一括登録を受け付けました。<br />
- リクエスト日時:$REQUEST_TIME$<br />
- SCVファイル名$FILE_NAME$
</p>
<p>
・登録完了には時間がかかる場合がありますので少々お待ちください。<br />
・登録完了通知は別途お送りします。<br />
・CSVファイルの内容に間違いがある場合は登録を完了できません。
</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, please do not reply.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,51 @@
<English>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.
<Deutsch>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.
<Français>
Dear $CUSTOMER_NAME$,
ODMS Cloudをご利用いただきありがとうございます。
CSVファイルによるユーザー一括登録を受け付けました。
- リクエスト日時:$REQUEST_TIME$
- SCVファイル名$FILE_NAME$
・登録完了には時間がかかる場合がありますので少々お待ちください。
・登録完了通知は別途お送りします。
・CSVファイルの内容に間違いがある場合は登録を完了できません。
If you received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail, please do not reply.