Merged PR 228: Revert "Merged PR 218: 画面実装(カードライセンス取り込みPU)

Revert "Merged PR 218: 画面実装(カードライセンス取り込みPU)

## 概要
[Task2161: 画面実装(カードライセンス取り込みPU)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2161)

タスク 2161: 画面実装(カードライセンス取り込みPU)
カードライセンス取り込みポップアップを実装しました。
以下の項目については別タスクへ切り出しての対応とし、本タスクでは対象外とさせてください。
・テキストボックスへの4文字区切りの入力
・テキストボックスの文字数制限
・改行コード入力時の挙動(バーコードリーダー対応)

## レビューポイント
なし

## UIの変更
https://ndstokyo.sharepoint.com/:i:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2161/%E3%82%AB%E3%83%BC%E3%83%89%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BFPU.PNG?csf=1&web=1&e=cRtMX3

## 動作確認状況
ローカルで動作確認済み

## 補足
なし"

Reverted commit `9a66ca02`.
This commit is contained in:
oura.a 2023-07-11 05:50:51 +00:00
parent 9a66ca027a
commit 3584a65682
32 changed files with 616 additions and 366 deletions

View File

@ -7,7 +7,6 @@ import ui from "features/ui/uiSlice";
import user from "features/user/userSlice";
import license from "features/license/licenseOrder/licenseSlice";
import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssueSlice";
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
import dictation from "features/dictation/dictationSlice";
@ -21,7 +20,6 @@ export const store = configureStore({
user,
license,
licenseCardIssue,
licenseCardActivate,
licenseSummary,
dictation,
},

View File

@ -1,4 +0,0 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./licenseCardActivateSlice";

View File

@ -1,31 +0,0 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = {
apps: {
keyLicense: "",
isLoading: false,
},
};
export const licenseCardActivateSlice = createSlice({
name: "licenseCardActivate",
initialState,
reducers: {
changeKeyLicense: (
state,
action: PayloadAction<{ keyLicense: string }>
) => {
const { keyLicense } = action.payload;
state.apps.keyLicense = keyLicense.toUpperCase();
},
cleanupApps: (state) => {
state.apps = initialState.apps;
},
},
});
export const { changeKeyLicense, cleanupApps } =
licenseCardActivateSlice.actions;
export default licenseCardActivateSlice.reducer;

View File

@ -1,64 +0,0 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { openSnackbar } from "../../ui/uiSlice";
import type { RootState } from "../../../app/store";
import { getTranslationID } from "../../../translation";
import { LicensesApi } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const activateCardLicenseAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
cardLicenseKey: string;
},
{
// rejectした時の返却値の型s
rejectValue: {
error: ErrorObject;
};
}
>("licenses/activateCardLicenseAsync", async (args, thunkApi) => {
const { cardLicenseKey } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
await licensesApi.activateCardLicenses(
{
cardLicenseKey,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,18 +0,0 @@
import { RootState } from "../../../app/store";
export const selectInputValidationErrors = (state: RootState) => {
const { keyLicense } = state.licenseCardActivate.apps;
const hasErrorIncorrectKeyNumber = checkErrorIncorrectKeyNumber(keyLicense);
return {
hasErrorIncorrectKeyNumber,
};
};
export const checkErrorIncorrectKeyNumber = (keyLicense: string): boolean =>
// // 20+4(20文字space4個以外の場合はエラー
keyLicense.length !== 24;
export const selectKeyLicense = (state: RootState) =>
state.licenseCardActivate.apps.keyLicense;
export const selectIsLoading = (state: RootState) =>
state.licenseCardActivate.apps.isLoading;

View File

@ -1,8 +0,0 @@
export interface LicenseCardActivateState {
apps: Apps;
}
export interface Apps {
keyLicense: string;
isLoading: boolean;
}

View File

@ -1,20 +1,6 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import React, { useCallback } from "react";
import styles from "styles/app.module.scss";
import close from "../../assets/images/close.svg";
import {
activateCardLicenseAsync,
selectKeyLicense,
cleanupApps,
selectIsLoading,
changeKeyLicense,
selectInputValidationErrors,
} from "../../features/license/licenseCardActivate/index";
import progress_activit from "../../assets/images/progress_activit.svg";
interface CardLicenseActivatePopupProps {
onClose: () => void;
@ -24,92 +10,18 @@ export const CardLicenseActivatePopup: React.FC<
CardLicenseActivatePopupProps
> = (props) => {
const { onClose } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const cardLicenseKey = useSelector(selectKeyLicense);
const [keyNumber, setKeyNumber] = useState<string>(cardLicenseKey);
const isLoading = useSelector(selectIsLoading);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
onClose();
}, [isLoading, onClose]);
// ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
// isLoadingがtrueの場合は確認ダイアログを表示する
if (isLoading) {
e.preventDefault();
// ChromeではreturnValueが必要
e.returnValue = "";
}
};
// コンポーネントがマウントされた時にイベントハンドラを登録する
useEffect(() => {
window.addEventListener("beforeunload", handleBeforeUnload);
// コンポーネントがアンマウントされるときにイベントハンドラを解除する
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
});
useEffect(
() => () => {
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
dispatch(cleanupApps());
},
[dispatch]
);
const [isPushActivateButton, setIsPushActivateButton] =
useState<boolean>(false);
// エラー宣言
const { hasErrorIncorrectKeyNumber } = useSelector(
selectInputValidationErrors
);
// activateボタン押下時
const onActivateLicense = useCallback(async () => {
setIsPushActivateButton(true);
if (keyNumber.length !== 24) {
const inputBox = document.getElementById("inputBox");
// カーソルをテキストボックスに戻す
if (inputBox) {
inputBox.focus();
}
return;
}
// activateAPIの呼び出し
const cardLicenseKeyWithoutSpaces = keyNumber.replace(/\s/g, "");
const { meta } = await dispatch(
activateCardLicenseAsync({ cardLicenseKey: cardLicenseKeyWithoutSpaces })
);
setIsPushActivateButton(false);
// カーソルをテキストボックスに戻す
const inputBox = document.getElementById("inputBox");
if (inputBox) {
inputBox.focus();
}
if (meta.requestStatus === "fulfilled") {
dispatch(cleanupApps());
setKeyNumber("");
}
}, [keyNumber, dispatch]);
}, [onClose]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("cardLicenseActivatePopupPage.label.title"))}
Activate License Key
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
@ -118,93 +30,24 @@ export const CardLicenseActivatePopup: React.FC<
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>
<label htmlFor="inputBox">
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumber"
)
)}
</label>
</dt>
<dt>Key number</dt>
<dd className="">
<input
id="inputBox"
type="text"
size={48}
size={40}
name=""
value={keyNumber}
maxLength={24} // 20+4(20文字space4個
value=""
maxLength={20}
className={styles.formInput}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(e) => {
let input = e.target.value.toUpperCase();
input = input.replace(/[^A-Z0-9]/g, "");
// _.chunk関数で、配列の要素を一定の要素数ごとに分ける
input = _.chunk(input, 4)
.map((a) => a.join(""))
.join(" ");
setKeyNumber(input);
if (input.includes("\n")) {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
onActivateLicense();
}
}}
onBlur={(e) => {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.target.value.toUpperCase();
setKeyNumber(input);
const button = document.getElementById("button");
if (button) {
button.focus();
button.click();
}
}
}}
/>
{isPushActivateButton && hasErrorIncorrectKeyNumber && (
<span className={styles.formError}>
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumberIncorrectError"
)
)}
</span>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
id="button"
type="button"
name="submit"
value={t(
getTranslationID(
"cardLicenseActivatePopupPage.label.activateButton"
)
)}
onClick={onActivateLicense}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
value="Activate"
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={closePopup}
/>
</dd>
</dl>

View File

@ -238,17 +238,5 @@
"label": {
"cardLicenseButton": "(de)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(de)Activate License Key",
"keyNumber": "(de)Key number",
"activateButton": "(de)activate",
"keyNumberIncorrectError": "(de)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(de)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(de)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,17 +238,5 @@
"label": {
"cardLicenseButton": "License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "Activate License Key",
"keyNumber": "Key number",
"activateButton": "activate",
"keyNumberIncorrectError": "Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,17 +238,5 @@
"label": {
"cardLicenseButton": "(es)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(es)Activate License Key",
"keyNumber": "(es)Key number",
"activateButton": "(es)activate",
"keyNumberIncorrectError": "(es)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(es)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(es)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,17 +238,5 @@
"label": {
"cardLicenseButton": "(fr)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(fr)Activate License Key",
"keyNumber": "(fr)Key number",
"activateButton": "(fr)activate",
"keyNumberIncorrectError": "(fr)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(fr)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(fr)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -13,3 +13,4 @@ TENANT_NAME=adb2codmsdev
SIGNIN_FLOW_NAME=b2c_1_signin_dev
EMAIL_CONFIRM_LIFETIME=86400000
APP_DOMAIN=https://10.1.0.10:4443/
STORAGE_TOKEN_EXPIRE_TIME=2

View File

@ -16,7 +16,6 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
NOTIFICATION_HUB_NAME=ntf-odms-shared
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
APP_DOMAIN=http://localhost:8081/
STORAGE_TOKEN_EXPIRE_TIME=30
STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev

View File

@ -36,4 +36,8 @@ export const ErrorCodes = [
'E010501', // アカウント不在エラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー
'E010701', // Blobファイル不在エラー
'E010801', // ライセンス不在エラー
'E010802', // ライセンス取り込み済みエラー
] as const;

View File

@ -25,4 +25,8 @@ export const errors: Errors = {
E010501: 'Account not Found Error.',
E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',
E010701: 'File not found in Blob Storage Error.',
E010801: 'License not exist Error',
E010802: 'License already activated Error',
};

View File

@ -177,16 +177,24 @@ export class FilesController {
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
)
async downloadLocation(
@Headers() headers,
@Req() req: Request,
@Query() body: AudioDownloadLocationRequest,
): Promise<AudioDownloadLocationResponse> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { audioFileId } = body;
// コンテナ作成処理の前にアクセストークンの認証を行う
//
return { url: '' };
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const url = await this.filesService.publishAudioFileDownloadSas(
accessToken.userId,
audioFileId,
);
return { url };
}
@Get('template/download-location')

View File

@ -6,8 +6,16 @@ import {
makeDefaultUsersRepositoryMockValue,
makeFilesServiceMock,
} from './test/files.service.mock';
import { DataSource } from 'typeorm';
import {
createAccount,
createTask,
createUser,
makeTestingModuleWithBlob,
} from './test/utility';
import { FilesService } from './files.service';
describe('FilesService', () => {
describe('音声ファイルアップロードURL取得', () => {
it('アップロードSASトークンが乗っているURLを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue();
@ -95,7 +103,9 @@ describe('FilesService', () => {
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
);
});
});
describe('タスク作成', () => {
it('文字起こしタスクを作成できる', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue();

View File

@ -28,6 +28,7 @@ export class FilesService {
private readonly logger = new Logger(FilesService.name);
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly tasksRepository: TasksRepositoryService,
private readonly tasksRepositoryService: TasksRepositoryService,
private readonly blobStorageService: BlobstorageService,
) {}
@ -127,13 +128,19 @@ export class FilesService {
}
try {
// URLにSASトークンがついている場合は取り除く
const urlObj = new URL(url);
urlObj.search = '';
const fileUrl = urlObj.toString();
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
// 追加時に末尾のJOBナンバーにインクリメントする
const task = await this.tasksRepositoryService.create(
user.account_id,
user.id,
priority,
url,
fileUrl,
fileName,
authorId,
workType,

View File

@ -9,7 +9,9 @@ import { Task } from '../../../repositories/tasks/entity/task.entity';
export type BlobstorageServiceMockValue = {
createContainer: void | Error;
containerExists: boolean | Error;
fileExists: boolean | Error;
publishUploadSas: string | Error;
publishDownloadSas: string | Error;
};
export type UsersRepositoryMockValue = {
@ -47,13 +49,23 @@ export const makeFilesServiceMock = async (
export const makeBlobstorageServiceMock = (
value: BlobstorageServiceMockValue,
) => {
const { containerExists, createContainer, publishUploadSas } = value;
const {
containerExists,
fileExists,
createContainer,
publishUploadSas,
publishDownloadSas,
} = value;
return {
containerExists:
containerExists instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
fileExists:
fileExists instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(fileExists)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(fileExists),
createContainer:
createContainer instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = (
publishUploadSas instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
: jest.fn<Promise<string>, []>().mockResolvedValue(publishUploadSas),
publishDownloadSas:
publishDownloadSas instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(publishDownloadSas)
: jest.fn<Promise<string>, []>().mockResolvedValue(publishDownloadSas),
};
};
@ -80,7 +96,9 @@ export const makeBlobstorageServiceMockValue =
(): BlobstorageServiceMockValue => {
return {
containerExists: true,
fileExists: true,
publishUploadSas: 'https://blob-storage?sas-token',
publishDownloadSas: 'https://blob-storage?sas-token',
createContainer: undefined,
};
};

View File

@ -28,7 +28,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES } from '../../constants';
import { ADMIN_ROLES, TIER_1, TIER_5 } from '../../constants';
import jwt from 'jsonwebtoken';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ -112,7 +112,7 @@ export class LicensesController {
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
// 第一階層以外は401を返す後々UseGuardsで弾く
if (payload.tier != 1) {
if (payload.tier != TIER_1) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
@ -159,6 +159,22 @@ export class LicensesController {
console.log(req.header('Authorization'));
console.log(body);
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
// TODO 第五階層以外は401を返す後々UseGuardsで弾く
if (payload.tier != TIER_5) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
await this.licensesService.activateCardLicenseKey(
payload.userId,
body.cardLicenseKey,
);
return {};
}
}

View File

@ -3,6 +3,7 @@ import {
CreateOrdersRequest,
IssueCardLicensesRequest,
IssueCardLicensesResponse,
ActivateCardLicensesRequest,
} from './types/types';
import {
makeDefaultAccountsRepositoryMockValue,
@ -12,14 +13,23 @@ import {
} from './test/liscense.service.mock';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { HttpException, HttpStatus } from '@nestjs/common';
import { PoNumberAlreadyExistError } from '../../repositories/licenses/errors/types';
import {
PoNumberAlreadyExistError,
LicenseKeyAlreadyActivatedError,
LicenseNotExistError,
} from '../../repositories/licenses/errors/types';
import { LicensesService } from './licenses.service';
import { makeTestingModule } from '../../common/test/modules';
import { DataSource } from 'typeorm';
import {
createAccount,
createUser,
createCardLicense,
createLicense,
createCardLicenseIssue,
selectCardLicensesCount,
selectCardLicense,
selectLicense,
} from './test/utility';
describe('LicensesService', () => {
@ -177,6 +187,92 @@ describe('LicensesService', () => {
),
);
});
it('カードライセンス取り込みが完了する', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
expect(
await service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
).toEqual(undefined);
});
it('カードライセンス取り込みに失敗した場合、エラーになるDBエラー', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
lisencesRepositoryMockValue.activateCardLicense = new Error('DB failed');
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('カードライセンス取り込みに失敗した場合、エラーになる(ライセンスが存在しないエラー)', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
lisencesRepositoryMockValue.activateCardLicense =
new LicenseNotExistError();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010801'), HttpStatus.BAD_REQUEST),
);
});
it('カードライセンス取り込みに失敗した場合、エラーになる(ライセンスが既に取り込まれているエラー)', async () => {
const lisencesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
lisencesRepositoryMockValue.activateCardLicense =
new LicenseKeyAlreadyActivatedError();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const accountsRepositoryMockValue =
makeDefaultAccountsRepositoryMockValue();
const service = await makeLicensesServiceMock(
lisencesRepositoryMockValue,
usersRepositoryMockValue,
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010802'), HttpStatus.BAD_REQUEST),
);
});
});
describe('DBテスト', () => {
@ -214,4 +310,38 @@ describe('DBテスト', () => {
const dbSelectResult = await selectCardLicensesCount(source);
expect(dbSelectResult.count).toEqual(issueCount);
});
it('カードライセンス取り込みが完了する', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'userId',
'admin',
);
const cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
const defaultAccountId = 150;
const licenseId = 50;
const issueId = 100;
await createLicense(source, licenseId, defaultAccountId);
await createCardLicense(source, licenseId, issueId, cardLicenseKey);
await createCardLicenseIssue(source, issueId);
const service = module.get<LicensesService>(LicensesService);
await service.activateCardLicenseKey(externalId, cardLicenseKey);
const dbSelectResultFromCardLicense = await selectCardLicense(
source,
cardLicenseKey,
);
const dbSelectResultFromLicense = await selectLicense(source, licenseId);
expect(
dbSelectResultFromCardLicense.cardLicense.activated_at,
).toBeDefined();
expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId);
});
});

View File

@ -4,7 +4,11 @@ import { AccessToken } from '../../common/token';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { PoNumberAlreadyExistError } from '../../repositories/licenses/errors/types';
import {
PoNumberAlreadyExistError,
LicenseNotExistError,
LicenseKeyAlreadyActivatedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { IssueCardLicensesResponse } from './types/types';
@ -140,4 +144,71 @@ export class LicensesService {
);
}
}
/**
* card license activate
* @param externalId
* @param cardLicenseKey
*/
async activateCardLicenseKey(
externalId: string,
cardLicenseKey: string,
): Promise<void> {
this.logger.log(
`[IN] ${this.activateCardLicenseKey.name}, argCardLicenseKey: ${cardLicenseKey}`,
);
let myAccountId: number;
// ユーザIDからアカウントIDを取得する
try {
myAccountId = (
await this.usersRepository.findUserByExternalId(externalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
// カードライセンスを取り込む
try {
await this.licensesRepository.activateCardLicense(
myAccountId,
cardLicenseKey,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('cardLicenseKey activate failed');
switch (e.constructor) {
case LicenseNotExistError:
throw new HttpException(
makeErrorResponse('E010801'),
HttpStatus.BAD_REQUEST,
);
case LicenseKeyAlreadyActivatedError:
throw new HttpException(
makeErrorResponse('E010802'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`);
return;
}
}

View File

@ -9,6 +9,7 @@ import { AccountsRepositoryService } from '../../../repositories/accounts/accoun
export type LicensesRepositoryMockValue = {
order: undefined | Error;
createCardLicenses: string[] | Error;
activateCardLicense: undefined | Error;
};
export type AccountsRepositoryMockValue = {
@ -45,7 +46,7 @@ export const makeLicensesServiceMock = async (
export const makeLicensesRepositoryMock = (
value: LicensesRepositoryMockValue,
) => {
const { order, createCardLicenses } = value;
const { order, createCardLicenses, activateCardLicense } = value;
return {
order:
order instanceof Error
@ -57,6 +58,10 @@ export const makeLicensesRepositoryMock = (
: jest
.fn<Promise<string[]>, []>()
.mockResolvedValue(createCardLicenses),
activateCardLicense:
activateCardLicense instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(activateCardLicense)
: jest.fn<Promise<void>, []>().mockResolvedValue(activateCardLicense),
};
};
@ -100,6 +105,7 @@ export const makeDefaultLicensesRepositoryMockValue =
'XYLEWNY2LR6Q657CZE41',
'AEJWRFFSWRQYQQJ6WVLV',
],
activateCardLicense: undefined,
};
};
export const makeDefaultUsersRepositoryMockValue =

View File

@ -1,7 +1,11 @@
import { DataSource } from 'typeorm';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { CardLicense } from '../../../repositories/licenses/entity/license.entity';
import {
License,
CardLicense,
CardLicenseIssue,
} from '../../../repositories/licenses/entity/license.entity';
export const createAccount = async (
datasource: DataSource,
@ -49,9 +53,92 @@ export const createUser = async (
return { userId: user.id, externalId: external_id };
};
export const createLicense = async (
datasource: DataSource,
licenseId: number,
accountId: number,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
expiry_date: null,
account_id: accountId,
type: 'card',
status: 'Unallocated',
allocated_user_id: null,
order_id: null,
deleted_at: null,
delete_order_id: null,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as License;
};
export const createCardLicense = async (
datasource: DataSource,
licenseId: number,
issueId: number,
cardLicenseKey: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(CardLicense).insert({
license_id: licenseId,
issue_id: issueId,
card_license_key: cardLicenseKey,
activated_at: null,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as CardLicense;
};
export const createCardLicenseIssue = async (
datasource: DataSource,
issueId: number,
): Promise<void> => {
const { identifiers } = await datasource
.getRepository(CardLicenseIssue)
.insert({
id: issueId,
issued_at: new Date(),
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as CardLicenseIssue;
};
export const selectCardLicensesCount = async (
datasource: DataSource,
): Promise<{ count: number }> => {
const count = await datasource.getRepository(CardLicense).count();
return { count: count };
};
export const selectCardLicense = async (
datasource: DataSource,
cardLicenseKey: string,
): Promise<{ cardLicense: CardLicense }> => {
const cardLicense = await datasource.getRepository(CardLicense).findOne({
where: {
card_license_key: cardLicenseKey,
},
});
return { cardLicense };
};
export const selectLicense = async (
datasource: DataSource,
id: number,
): Promise<{ license: License }> => {
const license = await datasource.getRepository(License).findOne({
where: {
id: id,
},
});
return { license };
};

View File

@ -19,7 +19,6 @@ import {
} from './test/utility';
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
import { makeTestingModule } from '../../common/test/modules';
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
describe('TasksService', () => {
it('タスク一覧を取得できるadmin', async () => {

View File

@ -4,7 +4,9 @@ import {
StorageSharedKeyCredential,
ContainerClient,
ContainerSASPermissions,
BlobSASPermissions,
generateBlobSASQueryParameters,
BlobClient,
} from '@azure/storage-blob';
import { ConfigService } from '@nestjs/config';
import {
@ -83,6 +85,27 @@ export class BlobstorageService {
return exists;
}
/**
* Files exists
* @param accountId
* @param country
* @param path
* @param fileName
* @param containerUrl
* @returns exists
*/
async fileExists(
accountId: number,
country: string,
filePath: string,
): Promise<boolean> {
const containerClient = this.getContainerClient(accountId, country);
const blob = containerClient.getBlobClient(`${filePath}`);
const exists = await blob.exists();
return exists;
}
/**
* SASトークン付きのBlobStorageアップロードURLを生成し返却します
* @param accountId
@ -109,10 +132,9 @@ export class BlobstorageService {
}
//SASの有効期限を設定
//TODO 有効期限は仮で30分
const expiryDate = new Date();
expiryDate.setMinutes(
expiryDate.getMinutes() +
expiryDate.setHours(
expiryDate.getHours() +
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
@ -137,6 +159,63 @@ export class BlobstorageService {
return url.toString();
}
/**
* SASトークン付きのBlobStorageダウンロードURLを生成し返却します
* @param accountId
* @param country
* @param path
* @param fileName
* @returns download sas
*/
async publishDownloadSas(
accountId: number,
country: string,
filePath: string,
): Promise<string> {
this.logger.log(`[IN] ${this.publishDownloadSas.name}`);
let containerClient: ContainerClient;
let blobClient: BlobClient;
let sharedKeyCredential: StorageSharedKeyCredential;
try {
// コンテナ名を指定してClientを取得
containerClient = this.getContainerClient(accountId, country);
// コンテナ内のBlobパス名を指定してClientを取得
blobClient = containerClient.getBlobClient(`${filePath}`);
// 国に対応したリージョンの接続情報を取得する
sharedKeyCredential = this.getSharedKeyCredential(country);
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
}
//SASの有効期限を設定
const expiryDate = new Date();
expiryDate.setHours(
expiryDate.getHours() +
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
//SASの権限を設定ダウンロードのため読み取り許可
const permissions = new BlobSASPermissions();
permissions.read = true;
//SASを発行
const sasToken = generateBlobSASQueryParameters(
{
containerName: containerClient.containerName,
blobName: blobClient.name,
permissions: permissions,
startsOn: new Date(),
expiresOn: expiryDate,
},
sharedKeyCredential,
);
const url = new URL(blobClient.url);
url.search = `${sasToken}`;
return url.toString();
}
/**
* Gets container client
* @param companyName

View File

@ -3,6 +3,7 @@ import {
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'license_orders' })
@ -63,6 +64,18 @@ export class License {
@Column({ nullable: true })
delete_order_id: number;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@UpdateDateColumn()
updated_at: Date;
}
@Entity({ name: 'licenses_history' })
export class LicenseHistory {
@ -92,6 +105,18 @@ export class CardLicenseIssue {
@Column()
issued_at: Date;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@UpdateDateColumn()
updated_at: Date;
}
@Entity({ name: 'card_licenses' })
@ -107,4 +132,16 @@ export class CardLicense {
@Column({ nullable: true })
activated_at: Date;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@UpdateDateColumn({})
updated_at: Date;
}

View File

@ -1,2 +1,8 @@
// POナンバーがすでに存在するエラー
export class PoNumberAlreadyExistError extends Error {}
// 取り込むカードライセンスが存在しないエラー
export class LicenseNotExistError extends Error {}
// 取り込むライセンスが既に取り込み済みのエラー
export class LicenseKeyAlreadyActivatedError extends Error {}

View File

@ -1,10 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CardLicense, CardLicenseIssue, License, LicenseOrder } from './entity/license.entity';
import {
CardLicense,
CardLicenseIssue,
License,
LicenseOrder,
} from './entity/license.entity';
import { LicensesRepositoryService } from './licenses.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([LicenseOrder,License,CardLicense,CardLicenseIssue])],
imports: [
TypeOrmModule.forFeature([
LicenseOrder,
License,
CardLicense,
CardLicenseIssue,
]),
],
providers: [LicensesRepositoryService],
exports: [LicensesRepositoryService],
})

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import {
LicenseOrder,
@ -13,11 +13,16 @@ import {
LICENSE_STATUS_ISSUED,
LICENSE_TYPE,
} from '../../constants';
import { PoNumberAlreadyExistError } from './errors/types';
import {
PoNumberAlreadyExistError,
LicenseNotExistError,
LicenseKeyAlreadyActivatedError,
} from './errors/types';
@Injectable()
export class LicensesRepositoryService {
constructor(private dataSource: DataSource) {}
private readonly logger = new Logger(LicensesRepositoryService.name);
async order(
poNumber: string,
@ -185,4 +190,69 @@ export class LicensesRepositoryService {
return licenseKeys;
}
/**
*
* @param accountId
* @param licenseKey
* @returns void
*/
async activateCardLicense(
accountId: number,
licenseKey: string,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const cardLicenseRepo = entityManager.getRepository(CardLicense);
// カードライセンステーブルを検索
const targetCardLicense = await cardLicenseRepo.findOne({
where: {
card_license_key: licenseKey,
},
});
// カードライセンスが存在しなければエラー
if (!targetCardLicense) {
this.logger.error(
`card license key not exist. card_licence_key: ${licenseKey}`,
);
throw new LicenseNotExistError();
}
// 既に取り込み済みならエラー
if (targetCardLicense.activated_at) {
this.logger.error(
`card license already activated. card_licence_key: ${licenseKey}`,
);
throw new LicenseKeyAlreadyActivatedError();
}
const licensesRepo = entityManager.getRepository(License);
// ライセンステーブルを検索
const targetLicense = await licensesRepo.findOne({
where: {
id: targetCardLicense.license_id,
},
});
// ライセンスが存在しなければエラー
if (!targetLicense) {
this.logger.error(
`license not exist. licence_id: ${targetCardLicense.license_id}`,
);
throw new LicenseNotExistError();
}
// ライセンステーブルを更新する
targetLicense.account_id = accountId;
await licensesRepo.save(targetLicense);
// カードライセンステーブルを更新する
targetCardLicense.activated_at = new Date();
await cardLicenseRepo.save(targetCardLicense);
this.logger.log(
`activate success. licence_id: ${targetCardLicense.license_id}`,
);
});
return;
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TemplateFilesRepositoryService } from './template_files.repository.service';
import { TemplateFile } from './entity/template_file.entity';
@Module({
imports: [TypeOrmModule.forFeature([TemplateFile])],
providers: [TemplateFilesRepositoryService],
exports: [TemplateFilesRepositoryService],
})
export class TemplateFilesRepositoryModule {}

View File

@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class TemplateFilesRepositoryService {
constructor(private dataSource: DataSource) {}
}