Merged PR 101: API実装(ライセンス注文登録)

## 概要
[Task1685: API実装(ライセンス注文登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1685)

タスク 1685: API実装(ライセンス注文登録)
ライセンス注文APIを追加

## レビューポイント
登録時のDB処理方法に問題がないか。
処理、エラーハンドリングに過不足がないか。

## UIの変更
なし

## 動作確認状況
ローカルでユニットテストを実施。
ローカルでAPIを実行し、DBに登録できること・poNumberの重複チェックが想定通りに動作していることを確認。

## 補足
なし
This commit is contained in:
maruyama.t 2023-05-30 07:17:43 +00:00 committed by oura.a
parent 3191e22ab6
commit 2c935c8b52
14 changed files with 542 additions and 18 deletions

View File

@ -19,6 +19,7 @@ import { AccountsRepositoryModule } from './repositories/accounts/accounts.repos
import { TypeOrmModule } from '@nestjs/typeorm';
import { SendGridModule } from './gateways/sendgrid/sendgrid.module';
import { UsersRepositoryModule } from './repositories/users/users.repository.module';
import { LicensesRepositoryModule } from './repositories/licenses/licenses.repository.module';
import { AudioFilesRepositoryModule } from './repositories/audio_files/audio_files.repository.module';
import { AudioOptionItemsRepositoryModule } from './repositories/audio_option_items/audio_option_items.repository.module';
import { TasksRepositoryModule } from './repositories/tasks/tasks.repository.module';
@ -55,8 +56,10 @@ import { LicensesController } from './features/licenses/licenses.controller';
TasksModule,
UsersModule,
SendGridModule,
LicensesModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
AudioFilesRepositoryModule,
AudioOptionItemsRepositoryModule,
TasksRepositoryModule,
@ -77,7 +80,6 @@ import { LicensesController } from './features/licenses/licenses.controller';
NotificationModule,
NotificationhubModule,
BlobstorageModule,
LicensesModule,
AuthGuardsModule,
],
controllers: [

View File

@ -26,6 +26,9 @@ export const ErrorCodes = [
'E010201', // 未認証ユーザエラー
'E010202', // 認証済ユーザエラー
'E010203', // 管理ユーザ権限エラー
'E010204', // ユーザ不在エラー
'E010301', // メールアドレス登録済みエラー
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
'E010501', // アカウント不在エラー
] as const;

View File

@ -15,6 +15,9 @@ export const errors: Errors = {
E010201: 'Email not verified user Error.',
E010202: 'Email already verified user Error.',
E010203: 'Administrator Permissions Error.',
E010204: 'User not Found Error.',
E010301: 'This email user already created Error',
E010302: 'This AuthorId already used Error',
E010401: 'This PoNumber already used Error',
E010501: 'Account not Found Error.',
};

View File

@ -88,6 +88,18 @@ export const BLOB_STORAGE_REGION_EU = [
*/
export const ROLE_NONE = 'None';
/**
*
* @const {string}
*/
export const LICENSE_STATUS_ISSUE_REQUESTING = 'Issue Requesting';
/**
*
* @const {string}
*/
export const LICENSE_STATUS_ISSUED = 'Issued';
/**
*
* @const {string}

View File

@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LicensesController } from './licenses.controller';
import { LicensesService } from './licenses.service';
import { CryptoService } from '../../gateways/crypto/crypto.service';
import { ConfigModule } from '@nestjs/config';
describe('LicensesController', () => {
@ -16,7 +17,7 @@ describe('LicensesController', () => {
}),
],
controllers: [LicensesController],
providers: [LicensesService],
providers: [LicensesService, CryptoService],
})
.overrideProvider(LicensesService)
.useValue(mockLicensesService)

View File

@ -5,6 +5,7 @@ import {
Post,
Req,
UseGuards,
HttpException,
} from '@nestjs/common';
import {
ApiResponse,
@ -16,12 +17,21 @@ import { ErrorResponse } from '../../common/error/types/types';
import { LicensesService } from './licenses.service';
import { CreateOrdersResponse, CreateOrdersRequest } from './types/types';
import { Request } from 'express';
import { CryptoService } from '../../gateways/crypto/crypto.service';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { confirmPermission } from '../../common/auth/auth';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { isVerifyError, verify } from '../../common/jwt';
import { AccessToken } from '../../common/token';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ApiTags('licenses')
@Controller('licenses')
export class LicensesController {
constructor(private readonly licensesService: LicensesService) {}
constructor(
private readonly licensesService: LicensesService,
private readonly cryptoService: CryptoService,
) {}
@ApiResponse({
status: HttpStatus.OK,
@ -45,6 +55,8 @@ export class LicensesController {
})
@ApiOperation({ operationId: 'createOrders' })
@ApiBearerAuth()
// @UseGuards(AuthGuard)
// @UseGuards(RoleGuard.requireds({ roles: ['admin', 'author'] }))
@Post('/orders')
async createOrders(
@Req() req: Request,
@ -52,6 +64,41 @@ export class LicensesController {
): Promise<CreateOrdersResponse> {
console.log(req.header('Authorization'));
console.log(body);
// アクセストークンにより権限を確認する
const pubKey = await this.cryptoService.getPublicKey();
const accessToken = retrieveAuthorizationToken(req);
//アクセストークンが存在しない場合のエラー
if (accessToken == undefined) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const payload = verify<AccessToken>(accessToken, pubKey);
//アクセストークン形式エラー
if (isVerifyError(payload)) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
//アクセストークンの権限不足エラー
if (!confirmPermission(payload.role)) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
// ライセンス注文処理
await this.licensesService.licenseOrders(
payload,
body.poNumber,
body.orderCount,
);
return {};
}
}

View File

@ -1,8 +1,18 @@
import { Module } from '@nestjs/common';
import { LicensesController } from './licenses.controller';
import { LicensesService } from './licenses.service';
import { CryptoModule } from '../../gateways/crypto/crypto.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
@Module({
imports: [
CryptoModule,
UsersRepositoryModule,
AccountsRepositoryModule,
LicensesRepositoryModule,
],
controllers: [LicensesController],
providers: [LicensesService],
})

View File

@ -1,18 +1,113 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LicensesService } from './licenses.service';
import { AccessToken } from 'src/common/token';
import { CreateOrdersRequest } from './types/types';
import {
makeDefaultAccountsRepositoryMockValue,
makeDefaultLicensesRepositoryMockValue,
makeDefaultUsersRepositoryMockValue,
makeLicensesServiceMock,
} from './test/liscense.service.mock';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { HttpException, HttpStatus } from '@nestjs/common';
describe('LicensesService', () => {
let service: LicensesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LicensesService],
}).compile();
service = module.get<LicensesService>(LicensesService);
it('ライセンス注文が完了する', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '' };
body.orderCount = 1000;
body.poNumber = '1';
expect(
await service.licenseOrders(token, body.poNumber, body.orderCount),
).toEqual(undefined);
});
it('should be defined', () => {
expect(service).toBeDefined();
it('ユーザID取得できなかった場合、エラーとなる', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error(
'User not Found Error.',
);
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '', role: '' };
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('親ユーザID取得できなかった場合、エラーとなる', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error(
'Account not Found Error.',
);
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '' };
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('POナンバー重複時、エラーとなる', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
lisencesRepositoryMockValue.order = new Error(
'Email already verified user Error.',
);
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '' };
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});

View File

@ -1,4 +1,108 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { request } from 'http';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { CryptoService } from '../../gateways/crypto/crypto.service';
import { AccessToken } from 'src/common/token';
import { User as EntityUser } from '../../repositories/users/entity/user.entity';
import {
UsersRepositoryService,
UserNotFoundError,
} from '../../repositories/users/users.repository.service';
import {
AccountsRepositoryService,
AccountNotFoundError,
} from '../../repositories/accounts/accounts.repository.service';
import {
LicensesRepositoryService,
PoNumberAlreadyExistError,
} from '../../repositories/licenses/licenses.repository.service';
import { CreateOrdersRequest } from './types/types';
import { DataSource } from 'typeorm';
@Injectable()
export class LicensesService {}
export class LicensesService {
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly accountsRepository: AccountsRepositoryService,
private readonly licensesRepository: LicensesRepositoryService,
) {}
private readonly logger = new Logger(LicensesService.name);
/**
* license Orders
* @param token
* @param body
*/
async licenseOrders(
accessToken: AccessToken,
poNumber: string,
orderCount: number,
): Promise<void> {
//アクセストークンからユーザーIDを取得する
this.logger.log(`[IN] ${this.licenseOrders.name}`);
const userId = accessToken.userId;
let myAccountId: number;
let parentAccountId: number;
// ユーザIDからアカウントIDを取得する
try {
myAccountId = (await this.usersRepository.findUserByExternalId(userId))
.account_id;
} catch (e) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
// 親アカウントIDを取得
try {
parentAccountId = (
await this.accountsRepository.findAccountById(myAccountId)
).parent_account_id;
} catch (e) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
try {
await this.licensesRepository.order(
poNumber,
myAccountId,
parentAccountId,
orderCount,
);
} catch (e) {
switch (e.constructor) {
case PoNumberAlreadyExistError:
throw new HttpException(
makeErrorResponse('E010401'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
}

View File

@ -0,0 +1,119 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LicensesService } from '../licenses.service';
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
export type LicensesRepositoryMockValue = {
order: undefined | Error;
};
export type AccountsRepositoryMockValue = {
findAccountById: Account | Error;
};
export type UsersRepositoryMockValue = {
findUserByExternalId: User | Error;
};
export const makeLicensesServiceMock = async (
licensesRepositoryMockValue: LicensesRepositoryMockValue,
usersRepositoryMockValue: UsersRepositoryMockValue,
accountsRepositoryMockValue: AccountsRepositoryMockValue,
): Promise<LicensesService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [LicensesService],
})
.useMocker((token) => {
switch (token) {
case LicensesRepositoryService:
return makeLicensesRepositoryMock(licensesRepositoryMockValue);
case UsersRepositoryService:
return makeUsersRepositoryMock(usersRepositoryMockValue);
case AccountsRepositoryService:
return makeAccountsRepositoryMock(accountsRepositoryMockValue);
}
})
.compile();
return module.get<LicensesService>(LicensesService);
};
export const makeLicensesRepositoryMock = (
value: LicensesRepositoryMockValue,
) => {
const { order } = value;
return {
order:
order instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(order)
: jest.fn<Promise<void>, []>().mockResolvedValue(order),
};
};
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
const { findUserByExternalId } = value;
return {
findUserByExternalId:
findUserByExternalId instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(findUserByExternalId)
: jest.fn<Promise<User>, []>().mockResolvedValue(findUserByExternalId),
};
};
export const makeAccountsRepositoryMock = (
value: AccountsRepositoryMockValue,
) => {
const { findAccountById } = value;
return {
findAccountById:
findAccountById instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(findAccountById)
: jest.fn<Promise<Account>, []>().mockResolvedValue(findAccountById),
};
};
export const makeDefaultLicensesRepositoryMockValue =
(): LicensesRepositoryMockValue => {
return {
order: undefined,
};
};
export const makeDefaultUsersRepositoryMockValue =
(): UsersRepositoryMockValue => {
const user1 = new User();
user1.id = 2;
user1.external_id = 'ede66c43-9b9d-4222-93ed-5f11c96e08e2';
user1.account_id = 1234567890123456;
user1.role = 'none';
user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
user1.accepted_terms_version = '1.0';
user1.email_verified = true;
user1.auto_renew = false;
user1.license_alert = false;
user1.notification = false;
user1.deleted_at = undefined;
user1.created_by = 'test';
user1.created_at = new Date();
user1.updated_by = undefined;
user1.updated_at = undefined;
return {
findUserByExternalId: user1,
};
};
export const makeDefaultAccountsRepositoryMockValue =
(): AccountsRepositoryMockValue => {
const account1 = new Account();
account1.id = 2;
account1.parent_account_id = 987654321098765;
return {
findAccountById: account1,
};
};

View File

@ -3,6 +3,8 @@ import { DataSource, UpdateResult } from 'typeorm';
import { User } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
export class AccountNotFoundError extends Error {}
@Injectable()
export class AccountsRepositoryService {
constructor(private dataSource: DataSource) {}
@ -112,4 +114,22 @@ export class AccountsRepositoryService {
return { newAccount: persistedAccount, adminUser: persistedUser };
});
}
/**
* IDからアカウント情報を取得する
* @param id
* @returns account
*/
async findAccountById(id: number): Promise<Account> {
const account = await this.dataSource.getRepository(Account).findOne({
where: {
id: id,
},
});
if (!account) {
throw new AccountNotFoundError();
}
return account;
}
}

View File

@ -0,0 +1,36 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
} from 'typeorm';
@Entity({ name: 'license_orders' })
export class LicenseOrder {
@PrimaryGeneratedColumn()
id: number;
@Column()
po_number: string;
@Column()
from_account_id: number;
@Column()
to_account_id: number;
@CreateDateColumn()
ordered_at: Date;
@Column('timestamp', { nullable: true })
issued_at?: Date;
@Column()
quantity: number;
@Column()
status: string;
@Column('timestamp', { nullable: true })
canceled_at?: Date;
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LicenseOrder } from './entity/license.entity';
import { LicensesRepositoryService } from './licenses.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([LicenseOrder])],
providers: [LicensesRepositoryService],
exports: [LicensesRepositoryService],
})
export class LicensesRepositoryModule {}

View File

@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { LicenseOrder } from './entity/license.entity';
import {
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_STATUS_ISSUED,
} from '../../constants';
export class PoNumberAlreadyExistError extends Error {}
@Injectable()
export class LicensesRepositoryService {
constructor(private dataSource: DataSource) {}
async order(
poNumber: string,
fromAccountId: number,
toAccountId: number,
quantity: number,
): Promise<LicenseOrder> {
const licenseOrder = new LicenseOrder();
licenseOrder.po_number = poNumber;
licenseOrder.from_account_id = fromAccountId;
licenseOrder.to_account_id = toAccountId;
licenseOrder.quantity = quantity;
licenseOrder.status = LICENSE_STATUS_ISSUE_REQUESTING;
// ライセンス注文テーブルに登録する
const createdEntity = await this.dataSource.transaction(
async (entityManager) => {
//poNumberの重複チェックを行う
const isPoNumberDuplicated = await entityManager
.getRepository(LicenseOrder)
.findOne({
where: [
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUED,
},
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUE_REQUESTING,
},
],
});
// 重複があった場合はエラーを返却する
if (isPoNumberDuplicated) {
throw new PoNumberAlreadyExistError();
}
const repo = entityManager.getRepository(LicenseOrder);
const newLicenseOrder = repo.create(licenseOrder);
const persisted = await repo.save(newLicenseOrder);
return persisted;
},
);
return createdEntity;
}
}