diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 84f6dd6..6cdd041 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -86,4 +86,5 @@ export const ErrorCodes = [ 'E017002', // 親アカウント変更不可エラー(階層関係が不正) 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) 'E017004', // 親アカウント変更不可エラー(国が同一でない) + 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 2e22585..6d3855d 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -76,4 +76,5 @@ export const errors: Errors = { E017002: 'Parent account switch failed Error: hierarchy mismatch', E017003: 'Parent account switch failed Error: region mismatch', E017004: 'Parent account switch failed Error: country mismatch', + E018001: 'Partner account delete failed Error: not satisfied conditions', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 0e56a73..0f47c2e 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2471,8 +2471,11 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO:service層を呼び出す。本実装時に以下は削除する。 - // await this.accountService.deletePartnerAccount(context, userId, targetAccountId); + await this.accountService.deletePartnerAccount( + context, + userId, + targetAccountId, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 7b43645..7cd6f4a 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -49,6 +49,7 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, + MANUAL_RECOVERY_REQUIRED, OPTION_ITEM_VALUE_TYPE, STORAGE_SIZE_PER_LICENSE, TASK_STATUS, @@ -90,10 +91,19 @@ import { } from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; -import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; +import { + createTask, + getCheckoutPermissions, + getTasks, +} from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; import { Account } from '../../repositories/accounts/entity/account.entity'; +import { + createTemplateFile, + getTemplateFiles, +} from '../templates/test/utility'; +import { createUserGroup } from '../users/test/utility'; describe('createAccount', () => { let source: DataSource | null = null; @@ -8312,3 +8322,1128 @@ describe('switchParent', () => { } }); }); + +describe('deletePartnerAccount', () => { + 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 service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + } + }); + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('パートナーアカウントが親が子アカウントを持つ場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + // 第5階層のアカウント作成 + await makeTestAccount(source, { + parent_account_id: tier4Account.id, + tier: 5, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }, + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }), + deleteUsers: () => { + throw new Error('deleteUsers failed'); + }, + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('Blobコンテナの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: () => { + throw new Error('deleteContainer failed'); + }, + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('メール送信失敗時でも処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('sendMail failed'); + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + 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', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index d6af114..b4a943d 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -54,6 +54,7 @@ import { DealerAccountNotFoundError, HierarchyMismatchError, RegionMismatchError, + PartnerAccountDeletionError, } from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { @@ -2833,4 +2834,151 @@ export class AccountsService { throw new Error(`Invalid country. country=${country}`); } } + /** + * 指定したアカウントIDのパートナーアカウントを削除する + * @param context + * @param externalId + * @param targetAccountId 削除対象パートナーのアカウントID + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deletePartnerAccount.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: parentAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (parentAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + + // 削除対象のパートナーアカウントを取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + if (targetAccount === null) { + throw new AccountNotFoundError( + `Account not found. targetAccountId: ${targetAccountId}`, + ); + } + + // メール送信に必要な情報を取得する + if (!targetAccount.primary_admin_user_id) { + throw new Error( + `primary_admin_user_id not found. accountId: ${targetAccountId}`, + ); + } + const primaryAdminUser = await this.usersRepository.findUserById( + context, + targetAccount.primary_admin_user_id, + ); + const adb2cAdmin = await this.adB2cService.getUser( + context, + primaryAdminUser.external_id, + ); + const { + displayName: targetPrimaryAdminName, + emailAddress: targetPrimaryAdminEmail, + } = getUserNameAndMailAddress(adb2cAdmin); + + if (!targetPrimaryAdminEmail) { + throw new Error( + `adb2c user mail not found. externalId: ${primaryAdminUser.external_id}`, + ); + } + + // アカウント削除処理(DB) + const targetUsers = await this.accountRepository.deletePartnerAccount( + context, + parentAccount.id, + targetAccountId, + ); + + // アカウント削除処理(Azure AD B2C) + try { + // 削除対象アカウント内のADB2Cユーザーをすべて削除する + await this.adB2cService.deleteUsers( + targetUsers.map((x) => x.external_id), + context, + ); + this.logger.log( + `[${context.getTrackingId()}] delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } catch (e) { + // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } + + // アカウント削除処理(Blob Storage) + await this.deleteBlobContainer( + targetAccountId, + targetAccount.country, + context, + ); + + // メール送信処理 + try { + const { companyName: parentCompanyName, adminEmails: parentEmails } = + await this.getAccountInformation(context, parentAccount.id); + + await this.sendgridService.sendMailWithU123( + context, + targetAccount.company_name, + targetPrimaryAdminName, + targetPrimaryAdminEmail, + 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 PartnerAccountDeletionError: + throw new HttpException( + makeErrorResponse('E018001'), + 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.deletePartnerAccount.name}`, + ); + } + } } diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index b21e914..2309c23 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -87,6 +87,8 @@ export class SendGridService { private readonly templateU122Text: string; private readonly templateU122NoParentHtml: string; private readonly templateU122NoParentText: string; + private readonly templateU123Html: string; + private readonly templateU123Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -328,6 +330,14 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_122_no_parent.txt`), 'utf-8', ); + this.templateU123Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.html`), + 'utf-8', + ); + this.templateU123Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.txt`), + 'utf-8', + ); } } @@ -1428,6 +1438,56 @@ export class SendGridService { } } + /** + * U-123のテンプレートを使用したメールを送信する + * @param context + * @param partnerAccountName + * @param partnerPrimaryName + * @param partnerPrimaryMail + * @param dealerAccountName + * @param dealerEmails + * @returns mail with u123 + */ + async sendMailWithU123( + context: Context, + partnerAccountName: string, + partnerPrimaryName: string, + partnerPrimaryMail: string, + dealerAccountName: string, + dealerEmails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + try { + const subject = 'パートナーアカウント情報消去完了通知 [U-123]'; + + const html = this.templateU123Html + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + const text = this.templateU123Text + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + + // メールを送信する + await this.sendMail( + context, + [partnerPrimaryMail], + dealerEmails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index c063325..ab49646 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -35,6 +35,7 @@ import { AccountNotFoundError, AdminUserNotFoundError, DealerAccountNotFoundError, + PartnerAccountDeletionError, } from './errors/types'; import { AlreadyLicenseAllocatedError, @@ -1547,4 +1548,224 @@ export class AccountsRepositoryService { ); }); } + + /** + * 指定したパートナーアカウントを削除する + * @param context + * @param parentAccountId + * @param targetAccountId + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + parentAccountId: number, + targetAccountId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のユーザーを取得 + const userRepo = entityManager.getRepository(User); + const users = await userRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const accountRepo = entityManager.getRepository(Account); + + // 対象アカウントが存在するかチェック + 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 (targetAccount.parent_account_id !== parentAccountId) { + throw new PartnerAccountDeletionError( + `Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`, + ); + } + + // 対象アカウントに子アカウントが存在する場合はエラー + const childrenAccounts = await accountRepo.find({ + where: { parent_account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // 子アカウントが存在する場合はエラー + if (childrenAccounts.length > 0) { + throw new PartnerAccountDeletionError( + `Target account has children account. targetAccountId: ${targetAccountId}`, + ); + } + + // ユーザテーブルのレコードを削除する + await deleteEntity( + userRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ソート条件のテーブルのレコードを削除する + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + await deleteEntity( + sortCriteriaRepo, + { user_id: In(users.map((user) => user.id)) }, + this.isCommentOut, + context, + ); + + // アカウントを削除 + await deleteEntity( + accountRepo, + { id: targetAccountId }, + this.isCommentOut, + context, + ); + // ライセンス系(card_license_issue以外)のテーブルのレコードを削除する + const orderRepo = entityManager.getRepository(LicenseOrder); + await deleteEntity( + orderRepo, + { from_account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseRepo = entityManager.getRepository(License); + const targetLicenses = await licenseRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const cardLicenseRepo = entityManager.getRepository(CardLicense); + await deleteEntity( + cardLicenseRepo, + { license_id: In(targetLicenses.map((license) => license.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + licenseRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + await deleteEntity( + licenseAllocationHistoryRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ユーザーグループ系のテーブルのレコードを削除する + const userGroupRepo = entityManager.getRepository(UserGroup); + const targetUserGroup = await userGroupRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + await deleteEntity( + userGroupMemberRepo, + { + user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + userGroupRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ワークタイプ系のテーブルのレコードを削除する + const worktypeRepo = entityManager.getRepository(Worktype); + const taggerWorktypes = await worktypeRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const optionItemRepo = entityManager.getRepository(OptionItem); + await deleteEntity( + optionItemRepo, + { worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + worktypeRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // テンプレートファイルテーブルのレコードを削除する + const templateFileRepo = entityManager.getRepository(TemplateFile); + await deleteEntity( + templateFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // オーディオファイル系のテーブルのレコードを削除する + const audioFileRepo = entityManager.getRepository(AudioFile); + const targetaudioFiles = await audioFileRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem); + await deleteEntity( + audioOptionItemsRepo, + { + audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + audioFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // タスク系のテーブルのレコードを削除する + const taskRepo = entityManager.getRepository(Task); + const targetTasks = await taskRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + await deleteEntity( + checkoutPermissionRepo, + { task_id: In(targetTasks.map((task) => task.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + taskRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + return users; + }); + } } diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 4cb5779..4b59a1b 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -27,6 +27,13 @@ export class AccountLockedError extends Error { } } +// パートナーアカウント削除不可エラー +export class PartnerAccountDeletionError extends Error { + constructor(message: string) { + super(message); + this.name = 'PartnerAccountDeletionError'; + } +} /** * 階層構造関係不適切エラー */ diff --git a/dictation_server/src/templates/template_U_123.html b/dictation_server/src/templates/template_U_123.html new file mode 100644 index 0000000..2e46fc4 --- /dev/null +++ b/dictation_server/src/templates/template_U_123.html @@ -0,0 +1,65 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
+

<English>

+

Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

+

ODMS Cloudをご利用いただきありがとうございます。

+

+ お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

+

+ 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

+

+ 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$, -> $PRIMARY_ADMIN_NAME$

+

ODMS Cloudをご利用いただきありがとうございます。

+

+ お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

+

+ 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

+

+ 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$, -> $PRIMARY_ADMIN_NAME$

+

ODMS Cloudをご利用いただきありがとうございます。

+

+ お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

+

+ 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

+

+ 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. +

+
+ + diff --git a/dictation_server/src/templates/template_U_123.txt b/dictation_server/src/templates/template_U_123.txt new file mode 100644 index 0000000..3a98e16 --- /dev/null +++ b/dictation_server/src/templates/template_U_123.txt @@ -0,0 +1,38 @@ + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +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. + + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +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. + + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +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. \ No newline at end of file