Merge branch 'develop'
This commit is contained in:
commit
fa1b3aa5a0
@ -16,6 +16,8 @@ import UserListPage from "pages/UserListPage";
|
||||
import LicensePage from "pages/LicensePage";
|
||||
import DictationPage from "pages/DictationPage";
|
||||
import PartnerPage from "pages/PartnerPage";
|
||||
import WorkflowPage from "pages/WorkflowPage";
|
||||
import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<Routes>
|
||||
@ -58,7 +60,11 @@ const AppRouter: React.FC = () => (
|
||||
/>
|
||||
<Route
|
||||
path="/workflow"
|
||||
element={<RouteAuthGuard component={<SamplePage />} />}
|
||||
element={<RouteAuthGuard component={<WorkflowPage />} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workflow/typist-group"
|
||||
element={<RouteAuthGuard component={<TypistGroupSettingPage />} />}
|
||||
/>
|
||||
<Route
|
||||
path="/partners"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@ import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice"
|
||||
import dictation from "features/dictation/dictationSlice";
|
||||
import partner from "features/partner/partnerSlice";
|
||||
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
|
||||
import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -30,6 +31,7 @@ export const store = configureStore({
|
||||
partnerLicense,
|
||||
dictation,
|
||||
partner,
|
||||
typistGroup,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
21
dictation_client/src/assets/images/group_add.svg
Normal file
21
dictation_client/src/assets/images/group_add.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#282828;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M25.7,23.7c2.5,0.6,4.8,0.2,6.7-1.2s2.9-3.4,2.9-6.1s-1-4.8-2.9-6.1c-1.9-1.3-4.1-1.7-6.7-1.1
|
||||
c0.9,1.1,1.5,2.2,1.9,3.3s0.6,2.5,0.6,4s-0.2,2.8-0.6,3.9S26.6,22.6,25.7,23.7z"/>
|
||||
<path class="st0" d="M17.8,24c2.2,0,4-0.7,5.4-2.1s2.1-3.2,2.1-5.4s-0.7-4-2.1-5.4C21.8,9.6,20,9,17.8,9s-4,0.7-5.4,2.1
|
||||
s-2.1,3.2-2.1,5.4s0.7,4,2.1,5.4C13.8,23.2,15.5,24,17.8,24z M14.5,13.2c0.8-0.8,1.9-1.3,3.2-1.3s2.4,0.4,3.2,1.3
|
||||
c0.9,0.9,1.3,1.9,1.3,3.2s-0.4,2.4-1.3,3.2C20.1,20.5,19,21,17.8,21s-2.4-0.4-3.2-1.3c-0.9-0.8-1.3-1.9-1.3-3.2
|
||||
C13.2,15.1,13.7,14.1,14.5,13.2z"/>
|
||||
<path class="st0" d="M5,37v-1.7c0-0.5,0.1-1,0.4-1.5s0.7-0.8,1.2-1.1c2.4-1.1,4.3-1.8,5.9-2.2c1.6-0.4,3.3-0.5,5.2-0.5
|
||||
s3.7,0.2,5.2,0.5c0.4,0.1,0.9,0.2,1.4,0.4c0.7-0.8,1.5-1.6,2.4-2.3c-1.1-0.4-2.2-0.7-3.1-1c-1.9-0.5-3.8-0.7-5.9-0.7
|
||||
s-4,0.2-5.9,0.7s-4,1.2-6.4,2.3c-1,0.5-1.9,1.2-2.5,2.1C2.3,33,2,34,2,35.2V40h19.1c0-1,0.1-2,0.3-3H5z"/>
|
||||
<polygon class="st0" points="33.4,43.9 33.4,37.4 26.9,37.4 26.9,33.5 33.4,33.5 33.4,27 37.3,27 37.3,33.5 43.8,33.5 43.8,37.4
|
||||
37.3,37.4 37.3,43.9 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10
dictation_client/src/common/convertLocalToUTCDate.ts
Normal file
10
dictation_client/src/common/convertLocalToUTCDate.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// タイムゾーンを考慮し、ローカルからUTCとなるよう日付を変換する関数
|
||||
export const convertLocalToUTCDate = (date: Date) => {
|
||||
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;
|
||||
|
||||
const timezoneOffsetMinutes = date.getTimezoneOffset();
|
||||
const timezoneOffsetMilliseconds =
|
||||
timezoneOffsetMinutes * MILLISECONDS_IN_A_MINUTE;
|
||||
|
||||
return new Date(date.getTime() + timezoneOffsetMilliseconds);
|
||||
};
|
||||
@ -42,4 +42,8 @@ export const errorCodes = [
|
||||
"E010701", // Blobファイル不在エラー
|
||||
"E010801", // ライセンス不在エラー
|
||||
"E010802", // ライセンス取り込み済みエラー
|
||||
"E010803", // ライセンス発行済みエラー
|
||||
"E010804", // ライセンス数不足エラー
|
||||
"E010805", // ライセンス有効期限切れエラー
|
||||
"E010806", // ライセンス割り当て不可エラー
|
||||
] as const;
|
||||
|
||||
@ -26,7 +26,9 @@ export const HEADER_NAME = "ODMS Cloud";
|
||||
export const ADMIN_ONLY_TABS = [
|
||||
HEADER_MENUS_LICENSE,
|
||||
HEADER_MENUS_USER,
|
||||
HEADER_MENUS_WORKFLOW,
|
||||
HEADER_MENUS_PARTNER,
|
||||
HEADER_MENUS_WORKFLOW,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -11,7 +11,15 @@ interface HeaderProps {
|
||||
const Header: React.FC<HeaderProps> = (props) => {
|
||||
const { userName } = props;
|
||||
const location = useLocation();
|
||||
return getHeader(location.pathname, userName);
|
||||
|
||||
const splitPaths = location.pathname.split("/");
|
||||
|
||||
let path = location.pathname;
|
||||
if (splitPaths.length >= 2) {
|
||||
path = `/${splitPaths[1]}`;
|
||||
}
|
||||
|
||||
return getHeader(path, userName);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { LicenseOrderHistoryState } from "./state";
|
||||
import { getLicenseOrderHistoriesAsync } from "./operations";
|
||||
import { getLicenseOrderHistoriesAsync, issueLicenseAsync } from "./operations";
|
||||
import { LIMIT_ORDER_HISORY_NUM } from "./constants";
|
||||
|
||||
const initialState: LicenseOrderHistoryState = {
|
||||
@ -52,6 +52,15 @@ export const licenseOrderHistorySlice = createSlice({
|
||||
builder.addCase(getLicenseOrderHistoriesAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(issueLicenseAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(issueLicenseAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(issueLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -74,3 +74,70 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const issueLicenseAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
// パラメータ
|
||||
orderedAccountId: number;
|
||||
poNumber: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/issueLicenseAsync", async (args, thunkApi) => {
|
||||
const { orderedAccountId, poNumber } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
await accountsApi.issueLicense(
|
||||
{
|
||||
orderedAccountId,
|
||||
poNumber,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
if (error.code === "E010803") {
|
||||
errorMessage = getTranslationID(
|
||||
"orderHistoriesPage.message.alreadyIssueLicense"
|
||||
);
|
||||
} else if (error.code === "E010804") {
|
||||
errorMessage = getTranslationID(
|
||||
"orderHistoriesPage.message.notEnoughOfNumberOfLicense"
|
||||
);
|
||||
}
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,7 +3,11 @@ import type { RootState } from "app/store";
|
||||
import { ErrorObject, createErrorObject } from "common/errors";
|
||||
import { getTranslationID } from "translation";
|
||||
import { closeSnackbar, openSnackbar } from "features/ui/uiSlice";
|
||||
import { AccountsApi, CreateAccountRequest } from "../../api/api";
|
||||
import {
|
||||
AccountsApi,
|
||||
CreateAccountRequest,
|
||||
GetDealersResponse,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
|
||||
export const signupAsync = createAsyncThunk<
|
||||
@ -56,3 +60,36 @@ export const signupAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getDealersAsync = createAsyncThunk<
|
||||
GetDealersResponse,
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("login/getDealersAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const res = await accountApi.getDealers();
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Dealer } from "api/api";
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectInputValidationErrors = (state: RootState) => {
|
||||
@ -56,3 +57,18 @@ export const selectPassword = (state: RootState) => state.signup.apps.password;
|
||||
|
||||
export const selectPageState = (state: RootState) =>
|
||||
state.signup.apps.pageState;
|
||||
|
||||
export const selectAllDealers = (state: RootState) =>
|
||||
state.signup.domain.dealers;
|
||||
|
||||
export const selectSameCountryDealers = (state: RootState) => {
|
||||
const { dealers } = state.signup.domain;
|
||||
const { country } = state.signup.apps;
|
||||
return dealers.filter((x: Dealer) => x.country === country);
|
||||
};
|
||||
|
||||
export const selectSelectedDealer = (state: RootState) => {
|
||||
const { dealers } = state.signup.domain;
|
||||
const { dealer } = state.signup.apps;
|
||||
return dealers.find((x: Dealer) => x.id === dealer);
|
||||
};
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { SignupState } from "./state";
|
||||
import { signupAsync } from "./operations";
|
||||
import { getDealersAsync, signupAsync } from "./operations";
|
||||
|
||||
const initialState: SignupState = {
|
||||
apps: {
|
||||
pageState: "input",
|
||||
company: "",
|
||||
country: "",
|
||||
dealer: "",
|
||||
dealer: undefined,
|
||||
adminName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
dealers: [],
|
||||
},
|
||||
domain: {
|
||||
dealers: [],
|
||||
},
|
||||
};
|
||||
|
||||
@ -32,10 +36,11 @@ export const signupSlice = createSlice({
|
||||
changeCountry: (state, action: PayloadAction<{ country: string }>) => {
|
||||
const { country } = action.payload;
|
||||
state.apps.country = country;
|
||||
state.apps.dealer = undefined;
|
||||
},
|
||||
changeDealer: (state, action: PayloadAction<{ dealer: string }>) => {
|
||||
changeDealer: (state, action: PayloadAction<{ dealer: number }>) => {
|
||||
const { dealer } = action.payload;
|
||||
state.apps.dealer = dealer;
|
||||
state.apps.dealer = Number.isNaN(dealer) ? undefined : dealer;
|
||||
},
|
||||
changeAdminName: (state, action: PayloadAction<{ adminName: string }>) => {
|
||||
const { adminName } = action.payload;
|
||||
@ -60,6 +65,15 @@ export const signupSlice = createSlice({
|
||||
builder.addCase(signupAsync.rejected, () => {
|
||||
//
|
||||
});
|
||||
builder.addCase(getDealersAsync.pending, () => {
|
||||
//
|
||||
});
|
||||
builder.addCase(getDealersAsync.fulfilled, (state, action) => {
|
||||
state.domain.dealers = action.payload.dealers;
|
||||
});
|
||||
builder.addCase(getDealersAsync.rejected, () => {
|
||||
//
|
||||
});
|
||||
},
|
||||
});
|
||||
export const {
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
import { Dealer } from "api/api";
|
||||
|
||||
export interface SignupState {
|
||||
apps: Apps;
|
||||
domain: Domain;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
pageState: "input" | "confirm" | "complete";
|
||||
company: string;
|
||||
country: string;
|
||||
dealer: string;
|
||||
dealer?: number | undefined;
|
||||
adminName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
dealers: Dealer[];
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
dealers: Dealer[];
|
||||
}
|
||||
|
||||
@ -5,3 +5,9 @@ export const LICENSE_STATUS = {
|
||||
ALERT: "Alert",
|
||||
RENEW: "Renew",
|
||||
} as const;
|
||||
|
||||
// Licenseの割り当て状態
|
||||
export const LICENSE_ALLOCATE_STATUS = {
|
||||
ALLOCATED: "Allocated",
|
||||
NOTALLOCATED: "Not Allocated",
|
||||
} as const;
|
||||
|
||||
@ -3,7 +3,12 @@ import type { RootState } from "app/store";
|
||||
import { USER_ROLES } from "components/auth/constants";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getTranslationID } from "translation";
|
||||
import { GetUsersResponse, UsersApi } from "../../api/api";
|
||||
import {
|
||||
GetUsersResponse,
|
||||
UsersApi,
|
||||
LicensesApi,
|
||||
GetAllocatableLicensesResponse,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../common/errors";
|
||||
|
||||
@ -59,16 +64,17 @@ export const addUserAsync = createAsyncThunk<
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
const { addUser } = state.user.apps;
|
||||
const newUser = { ...addUser };
|
||||
// roleがAUTHOR以外の場合、不要なプロパティをundefinedにする
|
||||
if (addUser.role !== USER_ROLES.AUTHOR) {
|
||||
addUser.authorId = undefined;
|
||||
addUser.encryption = undefined;
|
||||
addUser.prompt = undefined;
|
||||
addUser.encryptionPassword = undefined;
|
||||
if (newUser.role !== USER_ROLES.AUTHOR) {
|
||||
newUser.authorId = undefined;
|
||||
newUser.encryption = undefined;
|
||||
newUser.prompt = undefined;
|
||||
newUser.encryptionPassword = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
await usersApi.signup(addUser, {
|
||||
await usersApi.signup(newUser, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
thunkApi.dispatch(
|
||||
@ -188,3 +194,115 @@ export const updateUserAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getAllocatableLicensesAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetAllocatableLicensesResponse,
|
||||
// 引数
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/getAllocatableLicensesAsync", async (args, thunkApi) => {
|
||||
// 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 {
|
||||
const res = await licensesApi.getAllocatableLicenses({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} 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 });
|
||||
}
|
||||
});
|
||||
|
||||
export const allocateLicenseAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
// 引数
|
||||
{
|
||||
userId: number;
|
||||
newLicenseId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/allocateLicenseAsync", async (args, thunkApi) => {
|
||||
const { userId, newLicenseId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
await usersApi.allocateLicense(
|
||||
{
|
||||
userId,
|
||||
newLicenseId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
if (error.code === "E010805") {
|
||||
errorMessage = getTranslationID(
|
||||
"allocateLicensePopupPage.message.licenseAllocationFailure"
|
||||
);
|
||||
} else if (error.code === "E010806") {
|
||||
errorMessage = getTranslationID(
|
||||
"allocateLicensePopupPage.message.licenseAllocationFailure"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { RootState } from "app/store";
|
||||
import { USER_ROLES } from "components/auth/constants";
|
||||
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
|
||||
import {
|
||||
AddUser,
|
||||
RoleType,
|
||||
@ -7,7 +8,7 @@ import {
|
||||
isLicenseStatusType,
|
||||
isRoleType,
|
||||
} from "./types";
|
||||
import { LICENSE_STATUS } from "./constants";
|
||||
import { LICENSE_STATUS, LICENSE_ALLOCATE_STATUS } from "./constants";
|
||||
|
||||
export const selectInputValidationErrors = (state: RootState) => {
|
||||
const { name, email, role, authorId, encryption, encryptionPassword } =
|
||||
@ -63,11 +64,8 @@ export const selectUpdateValidationErrors = (state: RootState) => {
|
||||
);
|
||||
|
||||
if (passwordError) {
|
||||
// 最初にEncryptionがfasleで、Encryptionがtrueに変更された場合、EncryptionPasswordが必須
|
||||
if (!initEncryption) {
|
||||
hasErrorIncorrectEncryptionPassword = true;
|
||||
// Encryptionがある状態で変更がある場合、EncryptionPasswordが空でもエラーにしない
|
||||
} else if (!encryptionPassword || encryptionPassword === "") {
|
||||
// 最初にEncryptionがtrueで、EncryptionPassword変更されていない場合はエラーとしない
|
||||
if (initEncryption && encryptionPassword === undefined) {
|
||||
hasErrorIncorrectEncryptionPassword = false;
|
||||
} else {
|
||||
hasErrorIncorrectEncryptionPassword = true;
|
||||
@ -227,3 +225,105 @@ export const selectUpdateUser = (state: RootState) =>
|
||||
|
||||
export const selectHasPasswordMask = (state: RootState) =>
|
||||
state.user.apps.hasPasswordMask;
|
||||
|
||||
export const selectLicenseAllocateUserId = (state: RootState) =>
|
||||
state.user.apps.licenseAllocateUser.id;
|
||||
|
||||
export const selectLicenseAllocateUserEmail = (state: RootState) =>
|
||||
state.user.apps.licenseAllocateUser.email;
|
||||
|
||||
export const selectLicenseAllocateUserName = (state: RootState) =>
|
||||
state.user.apps.licenseAllocateUser.name;
|
||||
|
||||
export const selectLicenseAllocateUserAuthorId = (state: RootState) =>
|
||||
state.user.apps.licenseAllocateUser.authorId;
|
||||
|
||||
export const selectLicenseAllocateUserStatus = (state: RootState) =>
|
||||
// ライセンスが割り当てられてるかどうかのステータスを返却する。NORMAL,ALERT,RENEWはすべてライセンス割り当て扱い
|
||||
state.user.apps.licenseAllocateUser.licenseStatus === LICENSE_STATUS.NOLICENSE
|
||||
? LICENSE_ALLOCATE_STATUS.NOTALLOCATED
|
||||
: LICENSE_ALLOCATE_STATUS.ALLOCATED;
|
||||
|
||||
export const selectLicenseAllocateUserExpirationDate = (state: RootState) => {
|
||||
const { licenseStatus, remaining, expiration } =
|
||||
state.user.apps.licenseAllocateUser;
|
||||
|
||||
// ライセンスが割当たっていない場合は-、割当たってる場合はremaining(expiration)の形式で返却
|
||||
if (licenseStatus === LICENSE_STATUS.NOLICENSE) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return `${expiration}(${remaining})`;
|
||||
};
|
||||
|
||||
export const selectSelectedlicenseId = (state: RootState) =>
|
||||
state.user.apps.selectedlicenseId;
|
||||
|
||||
export const selectAllocatableLicenses = (state: RootState) => {
|
||||
const { allocatableLicenses } = state.user.domain;
|
||||
|
||||
// licenseIdはそのまま返却、expiryDateは「nullならundifined」「null以外ならyyyy/mm/dd(現在との差分日数)」を返却
|
||||
const transformedLicenses = allocatableLicenses.map((license) => ({
|
||||
licenseId: license.licenseId,
|
||||
expiryDate: license.expiryDate
|
||||
? calculateExpiryDate(license.expiryDate)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return transformedLicenses;
|
||||
};
|
||||
|
||||
export const selectInputValidationErrorsForLicenseAcclocation = (
|
||||
state: RootState
|
||||
) => {
|
||||
// 必須項目のチェック(License Acclocation画面用)
|
||||
|
||||
// License available選択チェック
|
||||
// 初期値である0と、「Select a license」選択時のNaNの場合エラーとする
|
||||
const hasErrorEmptyLicense =
|
||||
state.user.apps.selectedlicenseId === 0 ||
|
||||
Number.isNaN(state.user.apps.selectedlicenseId);
|
||||
|
||||
return {
|
||||
hasErrorEmptyLicense,
|
||||
};
|
||||
};
|
||||
|
||||
// 日付の差分を計算するサブ関数
|
||||
const calculateExpiryDate = (expiryDate: string) => {
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; // 1日のミリ秒数
|
||||
|
||||
const currentDate = new Date();
|
||||
const expirationDate = new Date(expiryDate);
|
||||
|
||||
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
|
||||
const currentDateLocal = convertLocalToUTCDate(currentDate);
|
||||
const expirationDateLocal = convertLocalToUTCDate(expirationDate);
|
||||
|
||||
// ライセンスは時刻を考慮しないので時分秒を意識しない日付を取得する
|
||||
const currentDateWithoutTime = new Date(
|
||||
currentDateLocal.getFullYear(),
|
||||
currentDateLocal.getMonth(),
|
||||
currentDateLocal.getDate()
|
||||
);
|
||||
|
||||
const expirationDateWithoutTime = new Date(
|
||||
expirationDateLocal.getFullYear(),
|
||||
expirationDateLocal.getMonth(),
|
||||
expirationDateLocal.getDate()
|
||||
);
|
||||
|
||||
// 差分日数を取得
|
||||
const timeDifference =
|
||||
expirationDateWithoutTime.getTime() - currentDateWithoutTime.getTime();
|
||||
const daysDifference = Math.ceil(timeDifference / MILLISECONDS_IN_A_DAY);
|
||||
|
||||
// yyyy/mm/dd形式の年月日を取得
|
||||
const expirationYear = expirationDateWithoutTime.getFullYear();
|
||||
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
|
||||
const expirationDay = expirationDateWithoutTime.getDate();
|
||||
|
||||
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay}`;
|
||||
|
||||
return `${formattedExpirationDate} (${daysDifference})`;
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { User } from "../../api/api";
|
||||
import { AddUser, UpdateUser } from "./types";
|
||||
import { User, AllocatableLicenseInfo } from "../../api/api";
|
||||
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
|
||||
|
||||
export interface UsersState {
|
||||
domain: Domain;
|
||||
@ -8,12 +8,15 @@ export interface UsersState {
|
||||
|
||||
export interface Domain {
|
||||
users: User[];
|
||||
allocatableLicenses: AllocatableLicenseInfo[];
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
addUser: AddUser;
|
||||
selectedUser: UpdateUser;
|
||||
updateUser: UpdateUser;
|
||||
licenseAllocateUser: LicenseAllocateUser;
|
||||
selectedlicenseId: number;
|
||||
hasPasswordMask: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
@ -50,6 +50,16 @@ export interface UpdateUser {
|
||||
notification: boolean;
|
||||
}
|
||||
|
||||
export interface LicenseAllocateUser {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
authorId: string;
|
||||
licenseStatus: LicenseStatusType | string;
|
||||
expiration: string;
|
||||
remaining: number | string;
|
||||
}
|
||||
|
||||
export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES];
|
||||
|
||||
// 受け取った値がUSER_ROLESの型であるかどうかを判定する
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { USER_ROLES } from "components/auth/constants";
|
||||
import { UsersState } from "./state";
|
||||
import { addUserAsync, listUsersAsync, updateUserAsync } from "./operations";
|
||||
import { RoleType } from "./types";
|
||||
import {
|
||||
addUserAsync,
|
||||
listUsersAsync,
|
||||
updateUserAsync,
|
||||
getAllocatableLicensesAsync,
|
||||
} from "./operations";
|
||||
import { RoleType, UserView } from "./types";
|
||||
|
||||
const initialState: UsersState = {
|
||||
domain: { users: [] },
|
||||
domain: { users: [], allocatableLicenses: [] },
|
||||
apps: {
|
||||
updateUser: {
|
||||
id: 0,
|
||||
@ -45,6 +50,16 @@ const initialState: UsersState = {
|
||||
prompt: false,
|
||||
encryptionPassword: "",
|
||||
},
|
||||
licenseAllocateUser: {
|
||||
id: 0,
|
||||
name: "",
|
||||
email: "",
|
||||
authorId: "",
|
||||
licenseStatus: "",
|
||||
expiration: "",
|
||||
remaining: "",
|
||||
},
|
||||
selectedlicenseId: 0,
|
||||
hasPasswordMask: false,
|
||||
isLoading: false,
|
||||
},
|
||||
@ -90,6 +105,9 @@ export const userSlice = createSlice({
|
||||
) => {
|
||||
const { encryption } = action.payload;
|
||||
state.apps.addUser.encryption = encryption;
|
||||
if (!encryption) {
|
||||
state.apps.addUser.encryptionPassword = undefined;
|
||||
}
|
||||
},
|
||||
changePrompt: (state, action: PayloadAction<{ prompt: boolean }>) => {
|
||||
const { prompt } = action.payload;
|
||||
@ -170,14 +188,16 @@ export const userSlice = createSlice({
|
||||
if (initEncryption && encryption && !password) {
|
||||
state.apps.hasPasswordMask = true;
|
||||
}
|
||||
if (!encryption) {
|
||||
state.apps.updateUser.encryptionPassword = undefined;
|
||||
}
|
||||
},
|
||||
changeUpdateEncryptionPassword: (
|
||||
state,
|
||||
action: PayloadAction<{ encryptionPassword: string }>
|
||||
) => {
|
||||
const { encryptionPassword } = action.payload;
|
||||
state.apps.updateUser.encryptionPassword =
|
||||
encryptionPassword === "" ? undefined : encryptionPassword;
|
||||
state.apps.updateUser.encryptionPassword = encryptionPassword;
|
||||
},
|
||||
changeUpdatePrompt: (state, action: PayloadAction<{ prompt: boolean }>) => {
|
||||
const { prompt } = action.payload;
|
||||
@ -214,6 +234,30 @@ export const userSlice = createSlice({
|
||||
cleanupUpdateUser: (state) => {
|
||||
state.apps.updateUser = initialState.apps.updateUser;
|
||||
},
|
||||
changeLicenseAllocateUser: (
|
||||
state,
|
||||
action: PayloadAction<{ selectedUser: UserView }>
|
||||
) => {
|
||||
const { selectedUser } = action.payload;
|
||||
state.apps.licenseAllocateUser.id = selectedUser.id;
|
||||
state.apps.licenseAllocateUser.name = selectedUser.name;
|
||||
state.apps.licenseAllocateUser.email = selectedUser.email;
|
||||
state.apps.licenseAllocateUser.authorId = selectedUser.authorId;
|
||||
state.apps.licenseAllocateUser.licenseStatus = selectedUser.licenseStatus;
|
||||
state.apps.licenseAllocateUser.expiration = selectedUser.expiration;
|
||||
state.apps.licenseAllocateUser.remaining = selectedUser.remaining;
|
||||
},
|
||||
changeSelectedlicenseId: (
|
||||
state,
|
||||
action: PayloadAction<{ selectedlicenseId: number }>
|
||||
) => {
|
||||
const { selectedlicenseId } = action.payload;
|
||||
state.apps.selectedlicenseId = selectedlicenseId;
|
||||
},
|
||||
cleanupLicenseAllocateInfo: (state) => {
|
||||
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
|
||||
state.apps.selectedlicenseId = initialState.apps.selectedlicenseId;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listUsersAsync.pending, (state) => {
|
||||
@ -244,6 +288,16 @@ export const userSlice = createSlice({
|
||||
builder.addCase(updateUserAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getAllocatableLicensesAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(getAllocatableLicensesAsync.fulfilled, (state, action) => {
|
||||
state.domain.allocatableLicenses = action.payload.allocatableLicenses;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getAllocatableLicensesAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -270,6 +324,9 @@ export const {
|
||||
changePrompt,
|
||||
changeEncryptionPassword,
|
||||
changeHasPasswordMask,
|
||||
changeLicenseAllocateUser,
|
||||
changeSelectedlicenseId,
|
||||
cleanupLicenseAllocateInfo,
|
||||
} = userSlice.actions;
|
||||
|
||||
export default userSlice.reducer;
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./typistGroupSlice";
|
||||
export * from "./state";
|
||||
export * from "./selectors";
|
||||
export * from "./operations";
|
||||
@ -0,0 +1,43 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getTranslationID } from "translation";
|
||||
import { AccountsApi, GetTypistGroupsResponse } from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
|
||||
export const listTypistGroupsAsync = createAsyncThunk<
|
||||
GetTypistGroupsResponse,
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/listTypistGroupsAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const typistGroup = await accountsApi.getTypistGroups({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
return typistGroup.data;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectTypistGroups = (state: RootState) =>
|
||||
state.typistGroup.domain.typistGroups;
|
||||
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.typistGroup.apps.isLoading;
|
||||
14
dictation_client/src/features/workflow/typistGroup/state.ts
Normal file
14
dictation_client/src/features/workflow/typistGroup/state.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { TypistGroup } from "../../../api/api";
|
||||
|
||||
export interface TypistGroupState {
|
||||
apps: Apps;
|
||||
domain: Domain;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
typistGroups: TypistGroup[];
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { TypistGroupState } from "./state";
|
||||
import { listTypistGroupsAsync } from "./operations";
|
||||
|
||||
const initialState: TypistGroupState = {
|
||||
apps: {
|
||||
isLoading: false,
|
||||
},
|
||||
domain: {
|
||||
typistGroups: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const typistGroupSlice = createSlice({
|
||||
name: "typistGroup",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listTypistGroupsAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(listTypistGroupsAsync.fulfilled, (state, action) => {
|
||||
state.domain.typistGroups = action.payload.typistGroups;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(listTypistGroupsAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default typistGroupSlice.reducer;
|
||||
@ -17,6 +17,7 @@ import {
|
||||
selectOrderHisory,
|
||||
selectTotal,
|
||||
selectTotalPage,
|
||||
issueLicenseAsync,
|
||||
selectOffset,
|
||||
savePageInfo,
|
||||
selectCompanyName,
|
||||
@ -50,6 +51,40 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
onReturn();
|
||||
}, [isLoading, onReturn]);
|
||||
|
||||
// issue,issueCancel,orderCancelボタンの処理終了時の情報更新用
|
||||
const UpdateHistoriesList = () => {
|
||||
dispatch(
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// issueボタンを押下時の処理
|
||||
const issueLicense = useCallback(
|
||||
async (poNumber: string) => {
|
||||
// ダイアログ確認
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(t(getTranslationID("common.message.dialogConfirm")))) {
|
||||
// 注文APIの呼び出し
|
||||
if (selectedRow) {
|
||||
const { meta } = await dispatch(
|
||||
issueLicenseAsync({
|
||||
orderedAccountId: selectedRow.accountId,
|
||||
poNumber,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
UpdateHistoriesList();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// ページネーションのボタンクリック時のアクション
|
||||
const movePage = (targetOffset: number) => {
|
||||
dispatch(
|
||||
@ -130,7 +165,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
{t(getTranslationID("orderHistoriesPage.label.status"))}
|
||||
</th>
|
||||
<th className={styles.noLine} />
|
||||
<th className={styles.noLine} />
|
||||
</tr>
|
||||
{!isLoading &&
|
||||
licenseOrderHistory.length !== 0 &&
|
||||
@ -168,63 +202,68 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
})()}
|
||||
</td>
|
||||
<td>
|
||||
{selectedRow === undefined &&
|
||||
x.status === STATUS.ISSUE_REQESTING && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href=""
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.orderCancel"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow !== undefined &&
|
||||
x.status === STATUS.ISSUE_REQESTING && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href=""
|
||||
className={`${styles.colorLink} ${styles.isActive}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.issue"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow !== undefined &&
|
||||
x.status === STATUS.ISSUED && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href=""
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.issueCancel"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{!selectedRow && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className={`${styles.menuLink} ${
|
||||
x.status === STATUS.ISSUE_REQESTING
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.orderCancel"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.colorLink} ${
|
||||
x.status === STATUS.ISSUE_REQESTING
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
issueLicense(x.poNumber);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.issue"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className={`${styles.menuLink} ${
|
||||
x.status === STATUS.ISSUED
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"orderHistoriesPage.label.issueCancel"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
selectAdminName,
|
||||
selectEmail,
|
||||
selectPassword,
|
||||
selectSelectedDealer,
|
||||
} from "../../features/signup/selectors";
|
||||
import { signupAsync } from "../../features/signup/operations";
|
||||
|
||||
@ -25,13 +26,14 @@ const SignupConfirm: React.FC = (): JSX.Element => {
|
||||
const adminName = useSelector(selectAdminName);
|
||||
const adminMail = useSelector(selectEmail);
|
||||
const adminPassword = useSelector(selectPassword);
|
||||
const dealer = useSelector(selectSelectedDealer);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
dispatch(
|
||||
signupAsync({
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId: 0,
|
||||
dealerAccountId: dealer?.id ?? undefined,
|
||||
adminName,
|
||||
adminMail,
|
||||
adminPassword,
|
||||
@ -39,7 +41,15 @@ const SignupConfirm: React.FC = (): JSX.Element => {
|
||||
token: "",
|
||||
})
|
||||
);
|
||||
}, [dispatch, companyName, country, adminName, adminMail, adminPassword]);
|
||||
}, [
|
||||
dispatch,
|
||||
companyName,
|
||||
country,
|
||||
dealer,
|
||||
adminName,
|
||||
adminMail,
|
||||
adminPassword,
|
||||
]);
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
@ -71,7 +81,9 @@ const SignupConfirm: React.FC = (): JSX.Element => {
|
||||
<dt className={styles.marginBtm1}>
|
||||
{t(getTranslationID("signupConfirmPage.label.dealer"))}
|
||||
</dt>
|
||||
<dd />
|
||||
<dd>
|
||||
<p className={styles.formConfirm}>{dealer?.name} </p>
|
||||
</dd>
|
||||
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("signupConfirmPage.text.adminInfoTitle"))}
|
||||
|
||||
@ -1,30 +1,38 @@
|
||||
import { AppDispatch } from "app/store";
|
||||
import {
|
||||
selectAdminName,
|
||||
selectAllDealers,
|
||||
selectCompany,
|
||||
selectCountry,
|
||||
selectDealer,
|
||||
selectEmail,
|
||||
selectInputValidationErrors,
|
||||
selectSameCountryDealers,
|
||||
} from "features/signup/selectors";
|
||||
import {
|
||||
changeAdminName,
|
||||
changeCompany,
|
||||
changeCountry,
|
||||
changeDealer,
|
||||
changeEmail,
|
||||
changePageState,
|
||||
changePassword,
|
||||
} from "features/signup/signupSlice";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { getTranslationID } from "translation";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { getDealersAsync } from "features/signup/operations";
|
||||
import { LANGUAGE_LIST } from "features/top/constants";
|
||||
import { openSnackbar } from "features/ui";
|
||||
import { COUNTRY_LIST } from "./constants";
|
||||
|
||||
const SignupInput: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [t, i18n] = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
|
||||
const [isOpenPolicy, setIsOpenPolicy] = useState<boolean>(false);
|
||||
@ -39,6 +47,7 @@ const SignupInput: React.FC = (): JSX.Element => {
|
||||
hasErrorIncorrectEmail,
|
||||
hasErrorIncorrectPassword,
|
||||
} = useSelector(selectInputValidationErrors);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (
|
||||
hasErrorEmptyAdminName ||
|
||||
@ -63,11 +72,65 @@ const SignupInput: React.FC = (): JSX.Element => {
|
||||
hasErrorIncorrectPassword,
|
||||
]);
|
||||
|
||||
const allDealers = useSelector(selectAllDealers);
|
||||
const dealers = useSelector(selectSameCountryDealers);
|
||||
|
||||
const company = useSelector(selectCompany);
|
||||
const country = useSelector(selectCountry);
|
||||
const dealer = useSelector(selectDealer);
|
||||
const adminName = useSelector(selectAdminName);
|
||||
const email = useSelector(selectEmail);
|
||||
|
||||
// 入力画面の初期化時の処理
|
||||
useEffect(() => {
|
||||
dispatch(getDealersAsync());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// 外部のWebサイトからの遷移時にURLのパラメータを取得
|
||||
// 以下のようなURLで遷移してきた場合に、Dealerと言語を変更する
|
||||
// https://xxx/signup?dealer=1&language=en
|
||||
// dealer={account_id(第四階層のアカウントID)} でDealerを指定
|
||||
// language={language(en/de/fr/es)} で言語を指定
|
||||
const query = new URLSearchParams(search);
|
||||
const paramDealer = query.get("dealer");
|
||||
const language = query.get("language");
|
||||
|
||||
// URLで言語が指定されていたら言語を変更
|
||||
if (language && LANGUAGE_LIST.map((x) => x.value).includes(language)) {
|
||||
i18n.changeLanguage(language);
|
||||
// 既にcookieに選択言語があれば削除
|
||||
document.cookie = "language=; max-age=0";
|
||||
// cookieの期限は1年
|
||||
document.cookie = `language=${language}; max-age=31536000`;
|
||||
}
|
||||
|
||||
// Dealerが取得できていない場合は何もしない
|
||||
if (allDealers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// URLでDealerが指定されていたら、そのDealerを選択(国も選択したDealerの国に変更)
|
||||
const urlDealer = allDealers.find(
|
||||
(x) => x.id === parseInt(paramDealer ?? "", 10)
|
||||
);
|
||||
if (urlDealer) {
|
||||
dispatch(changeCountry({ country: urlDealer.country }));
|
||||
dispatch(changeDealer({ dealer: urlDealer.id }));
|
||||
} else if (paramDealer) {
|
||||
// URLでDealerが指定されていたが、存在しない場合はメッセージを表示
|
||||
dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: t(
|
||||
getTranslationID("signupPage.message.dealerNotFoundError")
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [i18n, dispatch, search, allDealers]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<header />
|
||||
@ -145,9 +208,29 @@ const SignupInput: React.FC = (): JSX.Element => {
|
||||
</dd>
|
||||
<dt> {t(getTranslationID("signupPage.label.dealer"))}</dt>
|
||||
<dd>
|
||||
<select className={styles.formInput}>
|
||||
<option>Select dealer</option>
|
||||
<option value="Tokyo">Tokyo</option>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(event) => {
|
||||
dispatch(
|
||||
changeDealer({
|
||||
dealer: parseInt(event.target.value, 10),
|
||||
})
|
||||
);
|
||||
}}
|
||||
value={dealers.find((x) => x.id === dealer)?.id ?? NaN}
|
||||
>
|
||||
{[
|
||||
{
|
||||
id: NaN,
|
||||
country: "",
|
||||
name: "Select dealer",
|
||||
},
|
||||
...dealers,
|
||||
].map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{x.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className={styles.formComment}>
|
||||
{t(getTranslationID("signupPage.text.dealerExplanation"))}
|
||||
|
||||
118
dictation_client/src/pages/TypistGroupSettingPage/index.tsx
Normal file
118
dictation_client/src/pages/TypistGroupSettingPage/index.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import progress_activit from "assets/images/progress_activit.svg";
|
||||
import undo from "assets/images/undo.svg";
|
||||
import group_add from "assets/images/group_add.svg";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectTypistGroups,
|
||||
selectIsLoading,
|
||||
listTypistGroupsAsync,
|
||||
} from "features/workflow/typistGroup";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
|
||||
const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const typistGroup = useSelector(selectTypistGroups);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(listTypistGroupsAsync());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Header userName="XXXXXX" />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>
|
||||
{t(getTranslationID("workflowPage.label.title"))}
|
||||
</h1>
|
||||
<p className={styles.pageTx}>{` ${t(
|
||||
getTranslationID("typistGroupSetting.label.title")
|
||||
)}`}</p>
|
||||
</div>
|
||||
|
||||
<section className={styles.workflow}>
|
||||
<div>
|
||||
<ul className={styles.menuAction}>
|
||||
<li>
|
||||
<a
|
||||
href="/workflow"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<img src={undo} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("typistGroupSetting.label.return"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a className={`${styles.menuLink} ${styles.isActive}`}>
|
||||
<img src={group_add} alt="" className={styles.menuIcon} />
|
||||
|
||||
{t(getTranslationID("typistGroupSetting.label.addGroup"))}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<table className={`${styles.table} ${styles.group}`}>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th className={styles.noLine}>
|
||||
{t(getTranslationID("typistGroupSetting.label.groupName"))}
|
||||
</th>
|
||||
<th>{/** empty th */}</th>
|
||||
</tr>
|
||||
{!isLoading && typistGroup.length === 0 ? (
|
||||
<p style={{ margin: "10px", textAlign: "center" }}>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
) : (
|
||||
typistGroup.map((group) => (
|
||||
<tr key={group.id}>
|
||||
<td>{group.name}</td>
|
||||
<td>
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"typistGroupSetting.label.edit"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</table>
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypistGroupSettingPage;
|
||||
281
dictation_client/src/pages/UserListPage/allocateLicensePopup.tsx
Normal file
281
dictation_client/src/pages/UserListPage/allocateLicensePopup.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import { AppDispatch } from "app/store";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
selectLicenseAllocateUserId,
|
||||
selectLicenseAllocateUserEmail,
|
||||
selectLicenseAllocateUserName,
|
||||
selectLicenseAllocateUserAuthorId,
|
||||
selectLicenseAllocateUserStatus,
|
||||
selectLicenseAllocateUserExpirationDate,
|
||||
selectAllocatableLicenses,
|
||||
getAllocatableLicensesAsync,
|
||||
changeSelectedlicenseId,
|
||||
selectSelectedlicenseId,
|
||||
selectIsLoading,
|
||||
listUsersAsync,
|
||||
allocateLicenseAsync,
|
||||
cleanupLicenseAllocateInfo,
|
||||
selectInputValidationErrorsForLicenseAcclocation,
|
||||
LICENSE_ALLOCATE_STATUS,
|
||||
} from "features/user";
|
||||
import { getTranslationID } from "translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface AllocateLicensePopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { hasErrorEmptyLicense } = useSelector(
|
||||
selectInputValidationErrorsForLicenseAcclocation
|
||||
);
|
||||
|
||||
const id = useSelector(selectLicenseAllocateUserId);
|
||||
const email = useSelector(selectLicenseAllocateUserEmail);
|
||||
const name = useSelector(selectLicenseAllocateUserName);
|
||||
const authorId = useSelector(selectLicenseAllocateUserAuthorId);
|
||||
const status = useSelector(selectLicenseAllocateUserStatus);
|
||||
const expirationDate = useSelector(selectLicenseAllocateUserExpirationDate);
|
||||
const allocatableLicenses = useSelector(selectAllocatableLicenses);
|
||||
const selectedlicenseId = useSelector(selectSelectedlicenseId);
|
||||
|
||||
const [isPushAllocateButton, setIsPushAllocateButton] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// 画面起動時、有効なライセンスを取得する
|
||||
dispatch(getAllocatableLicensesAsync());
|
||||
}
|
||||
}, [isOpen, dispatch]);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setIsPushAllocateButton(false);
|
||||
dispatch(cleanupLicenseAllocateInfo());
|
||||
onClose();
|
||||
}, [isLoading, onClose, dispatch]);
|
||||
|
||||
const onAllocateLicense = useCallback(async () => {
|
||||
setIsPushAllocateButton(true);
|
||||
|
||||
// 入力不正時は何もしない
|
||||
if (hasErrorEmptyLicense) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(
|
||||
allocateLicenseAsync({ userId: id, newLicenseId: selectedlicenseId })
|
||||
);
|
||||
setIsPushAllocateButton(false);
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [dispatch, closePopup, id, selectedlicenseId, hasErrorEmptyLicense]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("allocateLicensePopupPage.label.title"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form action="" name="" method="" className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.personalInformation"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dt>
|
||||
{t(getTranslationID("allocateLicensePopupPage.label.email"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={email}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("allocateLicensePopupPage.label.name"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={name}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("allocateLicensePopupPage.label.authorID"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={authorId}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("allocateLicensePopupPage.label.status"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={(() => {
|
||||
switch (status) {
|
||||
case LICENSE_ALLOCATE_STATUS.ALLOCATED:
|
||||
return t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.allocated"
|
||||
)
|
||||
);
|
||||
case LICENSE_ALLOCATE_STATUS.NOTALLOCATED:
|
||||
return t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.notAllocated"
|
||||
)
|
||||
);
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
})()}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.expirationDate"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={expirationDate}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.licenseInformation"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.licenseAvailable"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>
|
||||
<select
|
||||
name=""
|
||||
className={`${styles.formInput} ${
|
||||
isPushAllocateButton && hasErrorEmptyLicense && styles.isError
|
||||
}`}
|
||||
onChange={(event) => {
|
||||
dispatch(
|
||||
changeSelectedlicenseId({
|
||||
selectedlicenseId: parseInt(event.target.value, 10),
|
||||
})
|
||||
);
|
||||
}}
|
||||
value={selectedlicenseId ?? ""}
|
||||
>
|
||||
<option value="">
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.dropDownHeading"
|
||||
)
|
||||
)}
|
||||
</option>
|
||||
{allocatableLicenses.map((x) => (
|
||||
<option key={x.licenseId} value={x.licenseId}>
|
||||
{/* one yearの場合はそれに相当する内容、それ以外の場合は値をそのまま表示 */}
|
||||
{x.expiryDate
|
||||
? x.expiryDate
|
||||
: t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.oneYear"
|
||||
)
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isPushAllocateButton && hasErrorEmptyLicense && (
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.message.inputEmptyError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.allocateLicense"
|
||||
)
|
||||
)}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
|
||||
onClick={onAllocateLicense}
|
||||
/>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -12,17 +12,21 @@ import {
|
||||
} from "features/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import { isLicenseStatusType } from "features/user/types";
|
||||
import { isLicenseStatusType, UserView } from "features/user/types";
|
||||
import { LICENSE_STATUS } from "features/user/constants";
|
||||
import { isApproveTier } from "features/auth/utils";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import { changeUpdateUser } from "features/user/userSlice";
|
||||
import {
|
||||
changeUpdateUser,
|
||||
changeLicenseAllocateUser,
|
||||
} from "features/user/userSlice";
|
||||
import personAdd from "../../assets/images/person_add.svg";
|
||||
import checkFill from "../../assets/images/check_fill.svg";
|
||||
import checkOutline from "../../assets/images/check_outline.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import { UserAddPopup } from "./popup";
|
||||
import { UserUpdatePopup } from "./updatePopup";
|
||||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||||
|
||||
const UserListPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -30,6 +34,8 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
|
||||
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
||||
useState(false);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsPopupOpen(true);
|
||||
@ -43,6 +49,14 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
[setIsUpdatePopupOpen, dispatch]
|
||||
);
|
||||
|
||||
const onAllocateLicensePopupOpen = useCallback(
|
||||
(selectedUser: UserView) => {
|
||||
setIsAllocateLicensePopupOpen(true);
|
||||
dispatch(changeLicenseAllocateUser({ selectedUser }));
|
||||
},
|
||||
[setIsAllocateLicensePopupOpen, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// ユーザ一覧取得処理を呼び出す
|
||||
dispatch(listUsersAsync());
|
||||
@ -59,14 +73,18 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
isOpen={isUpdatePopupOpen}
|
||||
onClose={() => {
|
||||
setIsUpdatePopupOpen(false);
|
||||
dispatch(listUsersAsync());
|
||||
}}
|
||||
/>
|
||||
<UserAddPopup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={() => {
|
||||
setIsPopupOpen(false);
|
||||
dispatch(listUsersAsync());
|
||||
}}
|
||||
/>
|
||||
<AllocateLicensePopup
|
||||
isOpen={isAllocateLicensePopupOpen}
|
||||
onClose={() => {
|
||||
setIsAllocateLicensePopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.wrap}>
|
||||
@ -164,7 +182,12 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
{isTier5 && (
|
||||
<>
|
||||
<li>
|
||||
<a href="">
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onAllocateLicensePopupOpen(user);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.label.licenseAllocation"
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
changePrompt,
|
||||
changeEncryption,
|
||||
changeEncryptionPassword,
|
||||
listUsersAsync,
|
||||
} from "features/user";
|
||||
import { USER_ROLES } from "components/auth/constants";
|
||||
import close from "../../assets/images/close.svg";
|
||||
@ -75,6 +76,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
hasErrorEmptyName,
|
||||
@ -146,9 +148,9 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.role"))}</dt>
|
||||
<dd>
|
||||
<label htmlFor={USER_ROLES.AUTHOR}>
|
||||
<label htmlFor={`add_${USER_ROLES.AUTHOR}`}>
|
||||
<input
|
||||
id={USER_ROLES.AUTHOR}
|
||||
id={`add_${USER_ROLES.AUTHOR}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -159,9 +161,9 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.author"))}
|
||||
</label>
|
||||
<label htmlFor={USER_ROLES.TYPIST}>
|
||||
<label htmlFor={`add_${USER_ROLES.TYPIST}`}>
|
||||
<input
|
||||
id={USER_ROLES.TYPIST}
|
||||
id={`add_${USER_ROLES.TYPIST}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -172,9 +174,9 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.transcriptionist"))}
|
||||
</label>
|
||||
<label htmlFor={USER_ROLES.NONE}>
|
||||
<label htmlFor={`add_${USER_ROLES.NONE}`}>
|
||||
<input
|
||||
id={USER_ROLES.NONE}
|
||||
id={`add_${USER_ROLES.NONE}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -303,9 +305,10 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
<dt>{t(getTranslationID("userListPage.label.setting"))}</dt>
|
||||
<dd className={styles.last}>
|
||||
<p>
|
||||
<label htmlFor="Auto renew">
|
||||
<label htmlFor="add_AutoRenew">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="add_AutoRenew"
|
||||
className={styles.formCheck}
|
||||
checked={addUser.autoRenew}
|
||||
onChange={(e) => {
|
||||
@ -318,9 +321,10 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="License Alert">
|
||||
<label htmlFor="add_LicenseAlert">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="add_LicenseAlert"
|
||||
checked={addUser.licenseAlert}
|
||||
className={styles.formCheck}
|
||||
onChange={(e) => {
|
||||
@ -333,9 +337,10 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="Notification">
|
||||
<label htmlFor="add_Notification">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="add_Notification"
|
||||
checked={addUser.notification}
|
||||
className={styles.formCheck}
|
||||
onChange={(e) => {
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
selectUpdateUser,
|
||||
selectUpdateValidationErrors,
|
||||
updateUserAsync,
|
||||
listUsersAsync,
|
||||
} from "features/user";
|
||||
import { getTranslationID } from "translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
@ -79,6 +80,7 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
@ -124,9 +126,9 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.role"))}</dt>
|
||||
<dd>
|
||||
<label htmlFor={USER_ROLES.AUTHOR}>
|
||||
<label htmlFor={`edit_${USER_ROLES.AUTHOR}`}>
|
||||
<input
|
||||
id={USER_ROLES.AUTHOR}
|
||||
id={`edit_${USER_ROLES.AUTHOR}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -138,9 +140,9 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.author"))}
|
||||
</label>
|
||||
<label htmlFor={USER_ROLES.TYPIST}>
|
||||
<label htmlFor={`edit_${USER_ROLES.TYPIST}`}>
|
||||
<input
|
||||
id={USER_ROLES.TYPIST}
|
||||
id={`edit_${USER_ROLES.TYPIST}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -152,9 +154,9 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.transcriptionist"))}
|
||||
</label>
|
||||
<label htmlFor={USER_ROLES.NONE}>
|
||||
<label htmlFor={`edit_${USER_ROLES.NONE}`}>
|
||||
<input
|
||||
id={USER_ROLES.NONE}
|
||||
id={`edit_${USER_ROLES.NONE}`}
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
@ -315,9 +317,10 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
<dt>{t(getTranslationID("userListPage.label.setting"))}</dt>
|
||||
<dd className="last">
|
||||
<p>
|
||||
<label htmlFor="Auto renew">
|
||||
<label htmlFor="edit_AutoRenew">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit_AutoRenew"
|
||||
className={styles.formCheck}
|
||||
checked={user.autoRenew}
|
||||
onChange={(e) => {
|
||||
@ -332,9 +335,10 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="License Alert">
|
||||
<label htmlFor="edit_LicenseAlert">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit_LicenseAlert"
|
||||
className={styles.formCheck}
|
||||
checked={user.licenseAlert}
|
||||
onChange={(e) => {
|
||||
@ -349,9 +353,10 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="Notification">
|
||||
<label htmlFor="edit_Notification">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit_Notification"
|
||||
className={styles.formCheck}
|
||||
checked={user.notification}
|
||||
onChange={(e) => {
|
||||
|
||||
24
dictation_client/src/pages/WorkflowPage/index.tsx
Normal file
24
dictation_client/src/pages/WorkflowPage/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
|
||||
const WorkflowPage: React.FC = (): JSX.Element => (
|
||||
<div className={styles.wrap}>
|
||||
<Header userName="XXXXXX" />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<span>
|
||||
<a style={{ margin: 20 }} href="/workflow/typist-group">
|
||||
Transcriptionist Group Setting
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WorkflowPage;
|
||||
@ -935,13 +935,75 @@ h3 + .brCrumb .tlIcon {
|
||||
.modal .formInput {
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
.modal .formInput[type="file"] {
|
||||
border: none;
|
||||
background: inherit;
|
||||
}
|
||||
.modal .formInput[type="file"].isHide {
|
||||
display: none;
|
||||
}
|
||||
.modal .form label {
|
||||
padding: 0.5rem 0 0.2rem;
|
||||
}
|
||||
.modal .form label.formFileButton {
|
||||
padding: 0.5rem 0.8rem;
|
||||
border: 1px #999999 solid;
|
||||
background: #ffffff;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.01rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
border-radius: 0.3rem;
|
||||
position: relative;
|
||||
-moz-transition: all 0.3s ease-out;
|
||||
-ms-transition: all 0.3s ease-out;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
.modal .form label.formFileButton:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.modal .formFileName {
|
||||
display: inline-block;
|
||||
width: 350px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border: 1px #e6e6e6 solid;
|
||||
background: #e6e6e6;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
}
|
||||
.modal .form .icLoading {
|
||||
bottom: 1.5rem;
|
||||
left: calc(50% - 25px);
|
||||
}
|
||||
.modal .form .tableWrap {
|
||||
max-height: 60vh;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.modal .form .table {
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.modal .form .table .tableHeader th {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: #282828;
|
||||
}
|
||||
.modal .form .table td {
|
||||
padding: 0.6rem 0.4rem;
|
||||
}
|
||||
.modal .form .table .formInput {
|
||||
width: inherit;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.modal .encryptionPass {
|
||||
display: none;
|
||||
}
|
||||
@ -2199,12 +2261,38 @@ tr.isSelected .menuInTable li a {
|
||||
.workflow .table.workflow td.txWsline {
|
||||
white-space: pre;
|
||||
}
|
||||
.workflow .table.group {
|
||||
.workflow .table.group,
|
||||
.workflow .table.template {
|
||||
width: 600px;
|
||||
}
|
||||
.workflow .table.group td:last-child {
|
||||
.workflow .table.group td:last-child,
|
||||
.workflow .table.template td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
.workflow .table.worktype {
|
||||
width: 1000px;
|
||||
}
|
||||
.workflow .table.worktype td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
.workflow .table .menuLink {
|
||||
min-width: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.workflow .menuAction.worktype {
|
||||
width: 1000px;
|
||||
}
|
||||
.workflow .menuAction.worktype .selectMenu {
|
||||
padding-top: 0.5rem;
|
||||
float: right;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.workflow .menuAction.worktype .formInput {
|
||||
width: inherit;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.formList dd.formChange {
|
||||
display: flex;
|
||||
|
||||
15
dictation_client/src/styles/app.module.scss.d.ts
vendored
15
dictation_client/src/styles/app.module.scss.d.ts
vendored
@ -60,6 +60,12 @@ declare const classNames: {
|
||||
readonly modalTitleIcon: "modalTitleIcon";
|
||||
readonly last: "last";
|
||||
readonly slideSet: "slideSet";
|
||||
readonly isHide: "isHide";
|
||||
readonly formFileButton: "formFileButton";
|
||||
readonly formFileName: "formFileName";
|
||||
readonly tableWrap: "tableWrap";
|
||||
readonly table: "table";
|
||||
readonly tableHeader: "tableHeader";
|
||||
readonly encryptionPass: "encryptionPass";
|
||||
readonly pageHeader: "pageHeader";
|
||||
readonly pageTitle: "pageTitle";
|
||||
@ -75,8 +81,6 @@ declare const classNames: {
|
||||
readonly snackbarMessage: "snackbarMessage";
|
||||
readonly snackbarIcon: "snackbarIcon";
|
||||
readonly snackbarIconClose: "snackbarIconClose";
|
||||
readonly table: "table";
|
||||
readonly tableHeader: "tableHeader";
|
||||
readonly hasSort: "hasSort";
|
||||
readonly isActiveAz: "isActiveAz";
|
||||
readonly isActiveZa: "isActiveZa";
|
||||
@ -100,8 +104,8 @@ declare const classNames: {
|
||||
readonly menuAction: "menuAction";
|
||||
readonly inTable: "inTable";
|
||||
readonly menuLink: "menuLink";
|
||||
readonly colorLink: "colorLink";
|
||||
readonly menuIcon: "menuIcon";
|
||||
readonly colorLink: "colorLink";
|
||||
readonly isDisable: "isDisable";
|
||||
readonly icCheckCircle: "icCheckCircle";
|
||||
readonly icInTable: "icInTable";
|
||||
@ -112,7 +116,6 @@ declare const classNames: {
|
||||
readonly displayOptions: "displayOptions";
|
||||
readonly tableFilter: "tableFilter";
|
||||
readonly tableFilter2: "tableFilter2";
|
||||
readonly tableWrap: "tableWrap";
|
||||
readonly txWsline: "txWsline";
|
||||
readonly hidePri: "hidePri";
|
||||
readonly opPri: "opPri";
|
||||
@ -178,6 +181,9 @@ declare const classNames: {
|
||||
readonly changeTitle: "changeTitle";
|
||||
readonly role4: "role4";
|
||||
readonly group: "group";
|
||||
readonly template: "template";
|
||||
readonly worktype: "worktype";
|
||||
readonly selectMenu: "selectMenu";
|
||||
readonly alignCenter: "alignCenter";
|
||||
readonly alignLeft: "alignLeft";
|
||||
readonly alignRight: "alignRight";
|
||||
@ -203,6 +209,5 @@ declare const classNames: {
|
||||
readonly txNormal: "txNormal";
|
||||
readonly txIcon: "txIcon";
|
||||
readonly txWswrap: "txWswrap";
|
||||
readonly widthMid: "widthMid";
|
||||
};
|
||||
export = classNames;
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
"message": {
|
||||
"inputEmptyError": "(de)この項目の入力は必須です。入力してください。",
|
||||
"passwordIncorrectError": "(de)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
|
||||
"emailIncorrectError": "(de)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
|
||||
"emailIncorrectError": "(de)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。",
|
||||
"dealerNotFoundError": "(de)指定されたディーラーが見つかりませんでした。直接ディーラーを指定してください。"
|
||||
},
|
||||
"text": {
|
||||
"title": "(de)Create your account",
|
||||
@ -324,6 +325,46 @@
|
||||
"issueCancel": "(de)Issue Cancel",
|
||||
"orderCancel": "(de)Order Cancel",
|
||||
"histories": "(de)histories"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "(de)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
|
||||
"alreadyIssueLicense": "(de)すでに発行済みの注文です。画面を更新してください。"
|
||||
}
|
||||
},
|
||||
"allocateLicensePopupPage": {
|
||||
"label": {
|
||||
"title": "(de)License Allocation",
|
||||
"personalInformation": "(de)Personal Information",
|
||||
"email": "(de)Email",
|
||||
"name": "(de)Name",
|
||||
"authorID": "(de)Author ID",
|
||||
"status": "(de)Status",
|
||||
"allocated": "(de)Allocated",
|
||||
"notAllocated": "(de)Not Allocated",
|
||||
"expirationDate": "(de)Expiration date",
|
||||
"licenseInformation": "(de)License Information",
|
||||
"licenseAvailable": "(de)License available",
|
||||
"dropDownHeading": "(de)Select a license.",
|
||||
"oneYear": "(de)One Year",
|
||||
"allocateLicense": "(de)OK"
|
||||
},
|
||||
"message": {
|
||||
"inputEmptyError": "(de)この項目の入力は必須です。入力してください。",
|
||||
"licenseAllocationFailure": "(de)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
|
||||
}
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "(de)Workflow"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
"label": {
|
||||
"title": "(de)Transctiprionist Group",
|
||||
"return": "(de)Return",
|
||||
"addGroup": "(de)Add Group",
|
||||
"groupName": "(de)Group Name",
|
||||
"edit": "(de)Edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,8 @@
|
||||
"message": {
|
||||
"inputEmptyError": "この項目の入力は必須です。入力してください。",
|
||||
"passwordIncorrectError": "入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
|
||||
"emailIncorrectError": "メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
|
||||
"emailIncorrectError": "メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。",
|
||||
"dealerNotFoundError": "指定されたディーラーが見つかりませんでした。直接ディーラーを指定してください。"
|
||||
},
|
||||
"text": {
|
||||
"title": "Create your account",
|
||||
@ -324,6 +325,46 @@
|
||||
"issueCancel": "Issue Cancel",
|
||||
"orderCancel": "Order Cancel",
|
||||
"histories": "histories"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
|
||||
"alreadyIssueLicense": "すでに発行済みの注文です。画面を更新してください。"
|
||||
}
|
||||
},
|
||||
"allocateLicensePopupPage": {
|
||||
"label": {
|
||||
"title": "License Allocation",
|
||||
"personalInformation": "Personal Information",
|
||||
"email": "Email",
|
||||
"name": "Name",
|
||||
"authorID": "Author ID",
|
||||
"status": "Status",
|
||||
"allocated": "Allocated",
|
||||
"notAllocated": "Not Allocated",
|
||||
"expirationDate": "Expiration date",
|
||||
"licenseInformation": "License Information",
|
||||
"licenseAvailable": "License available",
|
||||
"dropDownHeading": "Select a license.",
|
||||
"oneYear": "One Year",
|
||||
"allocateLicense": "OK"
|
||||
},
|
||||
"message": {
|
||||
"inputEmptyError": "この項目の入力は必須です。入力してください。",
|
||||
"licenseAllocationFailure": "ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
|
||||
}
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "Workflow"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
"label": {
|
||||
"title": "Transctiprionist Group",
|
||||
"return": "Return",
|
||||
"addGroup": "Add Group",
|
||||
"groupName": "Group Name",
|
||||
"edit": "Edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,8 @@
|
||||
"message": {
|
||||
"inputEmptyError": "(es)この項目の入力は必須です。入力してください。",
|
||||
"passwordIncorrectError": "(es)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
|
||||
"emailIncorrectError": "(es)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
|
||||
"emailIncorrectError": "(es)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。",
|
||||
"dealerNotFoundError": "(es)指定されたディーラーが見つかりませんでした。直接ディーラーを指定してください。"
|
||||
},
|
||||
"text": {
|
||||
"title": "(es)Create your account",
|
||||
@ -324,6 +325,46 @@
|
||||
"issueCancel": "(es)Issue Cancel",
|
||||
"orderCancel": "(es)Order Cancel",
|
||||
"histories": "(es)histories"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "(es)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
|
||||
"alreadyIssueLicense": "(es)すでに発行済みの注文です。画面を更新してください。"
|
||||
}
|
||||
},
|
||||
"allocateLicensePopupPage": {
|
||||
"label": {
|
||||
"title": "(es)License Allocation",
|
||||
"personalInformation": "(es)Personal Information",
|
||||
"email": "(es)Email",
|
||||
"name": "(es)Name",
|
||||
"authorID": "(es)Author ID",
|
||||
"status": "(es)Status",
|
||||
"allocated": "(es)Allocated",
|
||||
"notAllocated": "(es)Not Allocated",
|
||||
"expirationDate": "(es)Expiration date",
|
||||
"licenseInformation": "(es)License Information",
|
||||
"licenseAvailable": "(es)License available",
|
||||
"dropDownHeading": "(es)Select a license.",
|
||||
"oneYear": "(es)One Year",
|
||||
"allocateLicense": "(es)OK"
|
||||
},
|
||||
"message": {
|
||||
"inputEmptyError": "(es)この項目の入力は必須です。入力してください。",
|
||||
"licenseAllocationFailure": "(es)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
|
||||
}
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "(es)Workflow"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
"label": {
|
||||
"title": "(es)Transctiprionist Group",
|
||||
"return": "(es)Return",
|
||||
"addGroup": "(es)Add Group",
|
||||
"groupName": "(es)Group Name",
|
||||
"edit": "(es)Edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,8 @@
|
||||
"message": {
|
||||
"inputEmptyError": "(fr)この項目の入力は必須です。入力してください。",
|
||||
"passwordIncorrectError": "(fr)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
|
||||
"emailIncorrectError": "(fr)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
|
||||
"emailIncorrectError": "(fr)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。",
|
||||
"dealerNotFoundError": "(fr)指定されたディーラーが見つかりませんでした。直接ディーラーを指定してください。"
|
||||
},
|
||||
"text": {
|
||||
"title": "(fr)Create your account",
|
||||
@ -324,6 +325,46 @@
|
||||
"issueCancel": "(fr)Issue Cancel",
|
||||
"orderCancel": "(fr)Order Cancel",
|
||||
"histories": "(fr)histories"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "(fr)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
|
||||
"alreadyIssueLicense": "(fr)すでに発行済みの注文です。画面を更新してください。"
|
||||
}
|
||||
},
|
||||
"allocateLicensePopupPage": {
|
||||
"label": {
|
||||
"title": "(fr)License Allocation",
|
||||
"personalInformation": "(fr)Personal Information",
|
||||
"email": "(fr)Email",
|
||||
"name": "(fr)Name",
|
||||
"authorID": "(fr)Author ID",
|
||||
"status": "(fr)Status",
|
||||
"allocated": "(fr)Allocated",
|
||||
"notAllocated": "(fr)Not Allocated",
|
||||
"expirationDate": "(fr)Expiration date",
|
||||
"licenseInformation": "(fr)License Information",
|
||||
"licenseAvailable": "(fr)License available",
|
||||
"dropDownHeading": "(fr)Select a license.",
|
||||
"oneYear": "(fr)One Year",
|
||||
"allocateLicense": "(fr)OK"
|
||||
},
|
||||
"message": {
|
||||
"inputEmptyError": "(fr)この項目の入力は必須です。入力してください。",
|
||||
"licenseAllocationFailure": "(fr)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
|
||||
}
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "(fr)Workflow"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
"label": {
|
||||
"title": "(fr)Transctiprionist Group",
|
||||
"return": "(fr)Return",
|
||||
"addGroup": "(fr)Add Group",
|
||||
"groupName": "(fr)Group Name",
|
||||
"edit": "(fr)Edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
-- +migrate Up
|
||||
CREATE TABLE IF NOT EXISTS `lisence_allocation_history` (
|
||||
`user_id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ユーザーID',
|
||||
`license_id` BIGINT UNSIGNED NOT NULL COMMENT 'ライセンスID',
|
||||
`allocate_type` VARCHAR(255) NOT NULL COMMENT '割り当て種別(割当解除/割当)',
|
||||
`executed_at` TIMESTAMP NOT NULL COMMENT '実施日時',
|
||||
`switch_from_type` VARCHAR(255) NOT NULL COMMENT '切り替え元種別(特になし/カード/トライアル)',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `lisence_allocation_history`;
|
||||
@ -0,0 +1,19 @@
|
||||
-- +migrate Up
|
||||
DROP TABLE IF EXISTS `lisence_allocation_history`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `license_allocation_history` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT '割り当て履歴ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT 'ユーザーID',
|
||||
`license_id` BIGINT UNSIGNED NOT NULL COMMENT 'ライセンスID',
|
||||
`is_allocated` BOOLEAN NOT NULL DEFAULT 0 COMMENT '割り当て済みか',
|
||||
`executed_at` TIMESTAMP NOT NULL COMMENT '実施日時',
|
||||
`switch_from_type` VARCHAR(255) NOT NULL COMMENT '切り替え元種別(特になし/カード/トライアル)',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `license_allocation_history`;
|
||||
@ -298,6 +298,60 @@
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createTypistGroup",
|
||||
"summary": "",
|
||||
"description": "ログインしているユーザーのアカウント配下にタイピストグループを追加します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTypistGroupRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTypistGroupResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "グループ名が空の場合/ユーザーが存在しない場合",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/partner": {
|
||||
@ -882,6 +936,118 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/license/allocate": {
|
||||
"post": {
|
||||
"operationId": "allocateLicense",
|
||||
"summary": "",
|
||||
"description": "ライセンスを割り当てます",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AllocateLicenseRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AllocateLicenseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "割り当て失敗時",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/license/deallocate": {
|
||||
"post": {
|
||||
"operationId": "deallocateLicense",
|
||||
"summary": "",
|
||||
"description": "ライセンス割り当てを解除します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeallocateLicenseRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeallocateLicenseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "すでにライセンスが割り当て解除されている時",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/files/audio/upload-finished": {
|
||||
"post": {
|
||||
"operationId": "uploadFinished",
|
||||
@ -1816,6 +1982,44 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/licenses/allocatable": {
|
||||
"get": {
|
||||
"operationId": "getAllocatableLicenses",
|
||||
"summary": "",
|
||||
"description": "割り当て可能なライセンスを取得します",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAllocatableLicensesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["licenses"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/notification/register": {
|
||||
"post": {
|
||||
"operationId": "register",
|
||||
@ -1923,7 +2127,7 @@
|
||||
"minLength": 2,
|
||||
"maxLength": 2
|
||||
},
|
||||
"dealerAccountId": { "type": "number", "nullable": true },
|
||||
"dealerAccountId": { "type": "number" },
|
||||
"adminName": { "type": "string" },
|
||||
"adminMail": { "type": "string" },
|
||||
"adminPassword": { "type": "string" },
|
||||
@ -1936,7 +2140,6 @@
|
||||
"required": [
|
||||
"companyName",
|
||||
"country",
|
||||
"dealerAccountId",
|
||||
"adminName",
|
||||
"adminMail",
|
||||
"adminPassword",
|
||||
@ -2028,6 +2231,23 @@
|
||||
},
|
||||
"required": ["typistGroups"]
|
||||
},
|
||||
"CreateTypistGroupRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"typistGroupName": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
"typistIds": {
|
||||
"minItems": 1,
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["typistGroupName", "typistIds"]
|
||||
},
|
||||
"CreateTypistGroupResponse": { "type": "object", "properties": {} },
|
||||
"CreatePartnerAccountRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -2373,6 +2593,26 @@
|
||||
"required": ["id", "role", "autoRenew", "licenseAlart", "notification"]
|
||||
},
|
||||
"PostUpdateUserResponse": { "type": "object", "properties": {} },
|
||||
"AllocateLicenseRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": { "type": "number", "description": "ユーザーID" },
|
||||
"newLicenseId": {
|
||||
"type": "number",
|
||||
"description": "割り当てるライセンスのID"
|
||||
}
|
||||
},
|
||||
"required": ["userId", "newLicenseId"]
|
||||
},
|
||||
"AllocateLicenseResponse": { "type": "object", "properties": {} },
|
||||
"DeallocateLicenseRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": { "type": "number", "description": "ユーザーID" }
|
||||
},
|
||||
"required": ["userId"]
|
||||
},
|
||||
"DeallocateLicenseResponse": { "type": "object", "properties": {} },
|
||||
"AudioOptionItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -2671,6 +2911,24 @@
|
||||
"required": ["cardLicenseKey"]
|
||||
},
|
||||
"ActivateCardLicensesResponse": { "type": "object", "properties": {} },
|
||||
"AllocatableLicenseInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"licenseId": { "type": "number" },
|
||||
"expiryDate": { "format": "date-time", "type": "string" }
|
||||
},
|
||||
"required": ["licenseId", "expiryDate"]
|
||||
},
|
||||
"GetAllocatableLicensesResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allocatableLicenses": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/AllocatableLicenseInfo" }
|
||||
}
|
||||
},
|
||||
"required": ["allocatableLicenses"]
|
||||
},
|
||||
"RegisterRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -42,4 +42,8 @@ export const ErrorCodes = [
|
||||
'E010701', // Blobファイル不在エラー
|
||||
'E010801', // ライセンス不在エラー
|
||||
'E010802', // ライセンス取り込み済みエラー
|
||||
'E010803', // ライセンス発行済みエラー
|
||||
'E010804', // ライセンス不足エラー
|
||||
'E010805', // ライセンス有効期限切れエラー
|
||||
'E010806', // ライセンス割り当て不可エラー
|
||||
] as const;
|
||||
|
||||
@ -31,4 +31,8 @@ export const errors: Errors = {
|
||||
E010701: 'File not found in Blob Storage Error.',
|
||||
E010801: 'License not exist Error',
|
||||
E010802: 'License already activated Error',
|
||||
E010803: 'License already issued Error',
|
||||
E010804: 'License shortage Error',
|
||||
E010805: 'License is expired Error',
|
||||
E010806: 'License is unavailable Error',
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Context } from './types';
|
||||
|
||||
export const makeContext = (externaiId: string): Context => {
|
||||
export const makeContext = (externalId: string): Context => {
|
||||
return {
|
||||
trackingId: externaiId,
|
||||
trackingId: externalId,
|
||||
};
|
||||
};
|
||||
|
||||
218
dictation_server/src/common/test/overrides.ts
Normal file
218
dictation_server/src/common/test/overrides.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import { Context } from '../log';
|
||||
import {
|
||||
AdB2cService,
|
||||
ConflictError,
|
||||
} from '../../gateways/adb2c/adb2c.service';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { User, newUser } from '../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
|
||||
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
|
||||
|
||||
/**
|
||||
* adB2cServiceのモックを作成して、TServiceが依存するサービス(adB2cService)の参照を上書きする
|
||||
* ※ serviceに指定するオブジェクトは`adB2cService: AdB2cService`メンバ変数を持つ必要がある
|
||||
* @param service 上書きしたいTService
|
||||
* @param overrides adB2cServiceの各種メソッドのモックが返す値(省略した場合は既定のダミーの値)
|
||||
*/
|
||||
export const overrideAdB2cService = <TService>(
|
||||
service: TService,
|
||||
overrides: {
|
||||
createUser?: (
|
||||
context: Context,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
) => Promise<{ sub: string } | ConflictError>;
|
||||
deleteUser?: (externalId: string, context: Context) => Promise<void>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
const obj = (service as any).adB2cService as AdB2cService;
|
||||
if (overrides.createUser) {
|
||||
Object.defineProperty(obj, obj.createUser.name, {
|
||||
value: overrides.createUser,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteUser) {
|
||||
Object.defineProperty(obj, obj.deleteUser.name, {
|
||||
value: overrides.deleteUser,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* sendgridServiceのモックを作成して、TServiceが依存するサービス(sendgridService)の参照を上書きする
|
||||
* ※ serviceに指定するオブジェクトは`sendgridService: SendgridService`メンバ変数を持つ必要がある
|
||||
* @param service 上書きしたいTService
|
||||
* @param overrides sendgridServiceの各種メソッドのモックが返す値(省略した場合は既定のダミーの値)
|
||||
*/
|
||||
export const overrideSendgridService = <TService>(
|
||||
service: TService,
|
||||
overrides: {
|
||||
createMailContentFromEmailConfirm?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
userId: number,
|
||||
email: string,
|
||||
) => Promise<{ subject: string; text: string; html: string }>;
|
||||
createMailContentFromEmailConfirmForNormalUser?: (
|
||||
accountId: number,
|
||||
userId: number,
|
||||
email: string,
|
||||
) => Promise<{ subject: string; text: string; html: string }>;
|
||||
sendMail?: (
|
||||
context: Context,
|
||||
to: string,
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => Promise<void>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
const obj = (service as any).sendgridService as SendGridService;
|
||||
if (overrides.sendMail) {
|
||||
Object.defineProperty(obj, obj.sendMail.name, {
|
||||
value: overrides.sendMail,
|
||||
writable: true,
|
||||
});
|
||||
} else {
|
||||
// [重要]
|
||||
// sendMailだけは外部に対する送信を行ってしまう & 失敗によりメールアドレス自体の信頼度が変動してしまうため、
|
||||
// overrideした場合には"必ず"偽物の呼び出しになるようにしておく
|
||||
Object.defineProperty(obj, obj.sendMail.name, {
|
||||
value: async () => {
|
||||
return;
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (overrides.createMailContentFromEmailConfirm) {
|
||||
Object.defineProperty(obj, obj.createMailContentFromEmailConfirm.name, {
|
||||
value: overrides.createMailContentFromEmailConfirm,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (overrides.createMailContentFromEmailConfirmForNormalUser) {
|
||||
Object.defineProperty(
|
||||
obj,
|
||||
obj.createMailContentFromEmailConfirmForNormalUser.name,
|
||||
{
|
||||
value: overrides.createMailContentFromEmailConfirmForNormalUser,
|
||||
writable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* usersRepositoryのモックを作成して、TServiceが依存するサービス(usersRepositoryService)の参照を上書きする
|
||||
* ※ serviceに指定するオブジェクトは`usersRepository: UsersRepositoryService`メンバ変数を持つ必要がある
|
||||
* @param service 上書きしたいTService
|
||||
* @param overrides usersRepositoryの各種メソッドのモックが返す値(省略した場合は既定のダミーの値)
|
||||
*/
|
||||
export const overrideUsersRepositoryService = <TService>(
|
||||
service: TService,
|
||||
overrides: {
|
||||
createNormalUser?: (user: newUser) => Promise<User>;
|
||||
deleteNormalUser?: (userId: number) => Promise<void>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
const obj = (service as any).usersRepository as UsersRepositoryService;
|
||||
if (overrides.createNormalUser) {
|
||||
Object.defineProperty(obj, obj.createNormalUser.name, {
|
||||
value: overrides.createNormalUser,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteNormalUser) {
|
||||
Object.defineProperty(obj, obj.deleteNormalUser.name, {
|
||||
value: overrides.deleteNormalUser,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* blobStorageServiceのモックを作成して、TServiceが依存するサービス(blobStorageService)の参照を上書きする
|
||||
* ※ serviceに指定するオブジェクトは`blobstorageService: blobStorageService`メンバ変数を持つ必要がある
|
||||
* @param service 上書きしたいTService
|
||||
* @param overrides blobStorageServiceの各種メソッドのモックが返す値(省略した場合は既定のダミーの値)
|
||||
*/
|
||||
export const overrideBlobstorageService = <TService>(
|
||||
service: TService,
|
||||
overrides: {
|
||||
createContainer?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<void>;
|
||||
deleteContainer?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<void>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
const obj = (service as any).blobStorageService as BlobstorageService;
|
||||
if (overrides.createContainer) {
|
||||
Object.defineProperty(obj, obj.createContainer.name, {
|
||||
value: overrides.createContainer,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteContainer) {
|
||||
Object.defineProperty(obj, obj.deleteContainer.name, {
|
||||
value: overrides.deleteContainer,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* accountsRepositoryのモックを作成して、TServiceが依存するサービス(AccountsRepositoryService)の参照を上書きする
|
||||
* ※ serviceに指定するオブジェクトは`accountsRepository: AccountsRepositoryService`メンバ変数を持つ必要がある
|
||||
* @param service 上書きしたいTService
|
||||
* @param overrides accountsRepositoryの各種メソッドのモックが返す値(省略した場合は本物のメソッドが呼ばれる)
|
||||
*/
|
||||
export const overrideAccountsRepositoryService = <TService>(
|
||||
service: TService,
|
||||
overrides: {
|
||||
createAccount?: (
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | undefined,
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
adminUserAcceptedTermsVersion: string,
|
||||
) => Promise<{ newAccount: Account; adminUser: User }>;
|
||||
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
const obj = (service as any).accountRepository as AccountsRepositoryService;
|
||||
if (overrides.deleteAccount) {
|
||||
Object.defineProperty(obj, obj.deleteAccount.name, {
|
||||
value: overrides.deleteAccount,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.createAccount) {
|
||||
Object.defineProperty(obj, obj.createAccount.name, {
|
||||
value: overrides.createAccount,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
31
dictation_server/src/common/validators/admin.validator.ts
Normal file
31
dictation_server/src/common/validators/admin.validator.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { registerDecorator, ValidationOptions } from 'class-validator';
|
||||
|
||||
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
|
||||
return (object: any, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: 'IsAdminPasswordvalid',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate: (value: string) => {
|
||||
// 8文字~64文字でなければ早期に不合格
|
||||
const minLength = 8;
|
||||
const maxLength = 64;
|
||||
if (value.length < minLength || value.length > maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
|
||||
return new RegExp(charaTypePattern).test(value);
|
||||
},
|
||||
defaultMessage: () => {
|
||||
return 'Admin password rule not satisfied';
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -119,6 +119,15 @@ export const LICENSE_ALLOCATED_STATUS = {
|
||||
REUSABLE: 'Reusable',
|
||||
DELETED: 'Deleted',
|
||||
} as const;
|
||||
/**
|
||||
* 切り替え元種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const SWITCH_FROM_TYPE = {
|
||||
NONE: 'NONE',
|
||||
CARD: 'CARD',
|
||||
TRIAL: 'TRIAL',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ライセンスの期限切れが近いと見なす日数のしきい値
|
||||
@ -126,6 +135,12 @@ export const LICENSE_ALLOCATED_STATUS = {
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_THRESHOLD_DAYS = 14;
|
||||
|
||||
/**
|
||||
* ライセンスの有効期間
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_DAYS = 365;
|
||||
|
||||
/**
|
||||
* カードライセンスの桁数
|
||||
* @const {number}
|
||||
@ -198,3 +213,15 @@ export const USER_LICENSE_STATUS = {
|
||||
ALERT: 'Alert',
|
||||
RENEW: 'Renew',
|
||||
};
|
||||
|
||||
/**
|
||||
*トライアルライセンスの有効期限(日数)
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_EXPIRATION_DAYS = 30;
|
||||
|
||||
/**
|
||||
* ライセンスの発行数
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_ISSUE_NUM = 100;
|
||||
|
||||
@ -33,6 +33,8 @@ import {
|
||||
IssueLicenseRequest,
|
||||
IssueLicenseResponse,
|
||||
GetDealersResponse,
|
||||
CreateTypistGroupResponse,
|
||||
CreateTypistGroupRequest,
|
||||
} from './types/types';
|
||||
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
@ -40,7 +42,8 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { retrieveAuthorizationToken } from '../../common/http/helper';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Context } from '../../common/log';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ApiTags('accounts')
|
||||
@Controller('accounts')
|
||||
@ -80,7 +83,10 @@ export class AccountsController {
|
||||
} = body;
|
||||
const role = USER_ROLES.NONE;
|
||||
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
await this.accountService.createAccount(
|
||||
context,
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
@ -239,6 +245,46 @@ export class AccountsController {
|
||||
return { typistGroups };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: CreateTypistGroupResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'グループ名が空の場合/ユーザーが存在しない場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'createTypistGroup',
|
||||
description:
|
||||
'ログインしているユーザーのアカウント配下にタイピストグループを追加します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@Post('typist-groups')
|
||||
async createTypistGroup(
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateTypistGroupRequest,
|
||||
): Promise<CreateTypistGroupResponse> {
|
||||
// アクセストークン取得
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@Post('partner')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
@ -277,7 +323,10 @@ export class AccountsController {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(payload.userId);
|
||||
|
||||
await this.accountService.createPartnerAccount(
|
||||
context,
|
||||
companyName,
|
||||
country,
|
||||
email,
|
||||
@ -401,12 +450,17 @@ export class AccountsController {
|
||||
console.log(body);
|
||||
const { orderedAccountId, poNumber } = body;
|
||||
|
||||
/*await this.licensesService.issueLicense(
|
||||
orderedAccountId
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(accessToken.userId);
|
||||
await this.accountService.issueLicense(
|
||||
context,
|
||||
orderedAccountId,
|
||||
accessToken.userId,
|
||||
accessToken.tier,
|
||||
poNumber,
|
||||
);
|
||||
*/
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { AccountsController } from './accounts.controller';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -16,6 +17,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
|
||||
UserGroupsRepositoryModule,
|
||||
SendGridModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
controllers: [AccountsController],
|
||||
providers: [AccountsService],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,7 @@ import {
|
||||
} from '../../gateways/adb2c/adb2c.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import {
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
TIERS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
import { TIERS, USER_ROLES } from '../../constants';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
TypistGroup,
|
||||
@ -26,7 +22,10 @@ import {
|
||||
Dealer,
|
||||
GetMyAccountResponse,
|
||||
} from './types/types';
|
||||
import { DateWithZeroTime } from '../licenses/types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
ExpirationThresholdDate,
|
||||
} from '../licenses/types/types';
|
||||
import { GetLicenseSummaryResponse, Typist } from './types/types';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { UserNotFoundError } from '../../repositories/users/errors/types';
|
||||
@ -34,6 +33,14 @@ import { UserGroupsRepositoryService } from '../../repositories/user_groups/user
|
||||
import { makePassword } from '../../common/password';
|
||||
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
|
||||
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
|
||||
import { Context } from '../../common/log';
|
||||
import {
|
||||
LicensesShortageError,
|
||||
AlreadyIssuedError,
|
||||
OrderNotFoundError,
|
||||
} from '../../repositories/licenses/errors/types';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {
|
||||
constructor(
|
||||
@ -43,6 +50,7 @@ export class AccountsService {
|
||||
private readonly userGroupsRepository: UserGroupsRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
private readonly blobStorageService: BlobstorageService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
private readonly logger = new Logger(AccountsService.name);
|
||||
@ -58,13 +66,9 @@ export class AccountsService {
|
||||
|
||||
try {
|
||||
const currentDate = new DateWithZeroTime();
|
||||
|
||||
const expiringSoonDate = new Date(currentDate.getTime());
|
||||
expiringSoonDate.setDate(
|
||||
currentDate.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
const expiringSoonDate = new ExpirationThresholdDate(
|
||||
currentDate.getTime(),
|
||||
);
|
||||
// システム上有効期限日付の23時59分59秒999ミリ秒までライセンスは有効なため、各値最大値をセット
|
||||
expiringSoonDate.setHours(23, 59, 59, 999);
|
||||
|
||||
const { licenseSummary, isStorageAvailable } =
|
||||
await this.accountRepository.getLicenseSummaryInfo(
|
||||
@ -118,100 +122,221 @@ export class AccountsService {
|
||||
* @returns account
|
||||
*/
|
||||
async createAccount(
|
||||
context: Context,
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | null,
|
||||
dealerAccountId: number | undefined,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedTermsVersion: string,
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createAccount.name} | params: { ` +
|
||||
`country: ${country}, ` +
|
||||
`dealerAccountId: ${dealerAccountId}, ` +
|
||||
`role: ${role}, ` +
|
||||
`acceptedTermsVersion: ${acceptedTermsVersion} };`,
|
||||
);
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log('create externalUser failed');
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010301'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
TIERS.TIER5,
|
||||
externalUser.sub,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
} catch (e) {
|
||||
console.log('create account failed');
|
||||
console.log(
|
||||
`[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// メールの送信元を取得
|
||||
const from = this.configService.get<string>('MAIL_FROM') ?? '';
|
||||
|
||||
// メールの内容を構成
|
||||
const { subject, text, html } =
|
||||
await this.sendgridService.createMailContentFromEmailConfirm(
|
||||
account.id,
|
||||
user.id,
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create externalUser failed');
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
this.logger.error(`email conflict. externalUser: ${externalUser}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010301'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
TIERS.TIER5,
|
||||
externalUser.sub,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] adminUser.external_id: ${user.external_id}`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create account failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 新規作成アカウント用のBlobコンテナを作成
|
||||
try {
|
||||
await this.blobStorageService.createContainer(
|
||||
context,
|
||||
account.id,
|
||||
country,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create container failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
// DBのアカウントを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// メールの送信元を取得
|
||||
const from = this.configService.get<string>('MAIL_FROM') ?? '';
|
||||
|
||||
// メールの内容を構成
|
||||
const { subject, text, html } =
|
||||
await this.sendgridService.createMailContentFromEmailConfirm(
|
||||
context,
|
||||
account.id,
|
||||
user.id,
|
||||
email,
|
||||
);
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(
|
||||
context,
|
||||
email,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('send E-mail failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
// DBのアカウントを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
// Blobコンテナを削除
|
||||
await this.deleteBlobContainer(account.id, country, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
userId: user.id,
|
||||
externalUserId: user.external_id,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log('create user failed');
|
||||
console.log(`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`);
|
||||
console.log(
|
||||
`[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
|
||||
);
|
||||
console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.createAccount.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
userId: user.id,
|
||||
externalUserId: user.external_id,
|
||||
};
|
||||
// AdB2cのユーザーを削除
|
||||
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteAdB2cUser(
|
||||
externalUserId: string,
|
||||
context: Context,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBのアカウントを削除
|
||||
private async deleteAccount(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
context: Context,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.accountRepository.deleteAccount(accountId, userId);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete account: ${accountId}, user: ${userId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Blobコンテナを削除
|
||||
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteBlobContainer(
|
||||
accountId: number,
|
||||
country: string,
|
||||
context: Context,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.blobStorageService.deleteContainer(
|
||||
context,
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,97 +460,168 @@ export class AccountsService {
|
||||
|
||||
/**
|
||||
* パートナーを追加する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param email
|
||||
* @param adminName
|
||||
* @param userId
|
||||
* @param tier
|
||||
* @param companyName パートナーの会社名
|
||||
* @param country パートナーの所属する国
|
||||
* @param email パートナーの管理者のメールアドレス
|
||||
* @param adminName パートナーの管理者の名前
|
||||
* @param creatorUserId パートナーを作成する上位階層アカウントのユーザーID
|
||||
* @param creatorAccountTier パートナーを作成する上位階層アカウントの階層
|
||||
*/
|
||||
async createPartnerAccount(
|
||||
context: Context,
|
||||
companyName: string,
|
||||
country: string,
|
||||
email: string,
|
||||
adminName: string,
|
||||
userId: string,
|
||||
tier: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.createPartnerAccount.name}`);
|
||||
|
||||
let myAccountId: number;
|
||||
creatorUserId: string,
|
||||
creatorAccountTier: number,
|
||||
): Promise<{ accountId: number }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createPartnerAccount.name} | params: { creatorUserId: ${creatorUserId}, creatorAccountTier: ${creatorAccountTier} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// アクセストークンからユーザーIDを取得する
|
||||
myAccountId = (await this.usersRepository.findUserByExternalId(userId))
|
||||
.account_id;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof UserNotFoundError) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
let myAccountId: number;
|
||||
|
||||
try {
|
||||
// アクセストークンからユーザーIDを取得する
|
||||
myAccountId = (
|
||||
await this.usersRepository.findUserByExternalId(creatorUserId)
|
||||
).account_id;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof UserNotFoundError) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
} else {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ramdomPassword = makePassword();
|
||||
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
|
||||
try {
|
||||
// 管理者ユーザを作成し、AzureADB2C IDを取得する
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
ramdomPassword,
|
||||
adminName,
|
||||
);
|
||||
} else {
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
// メールアドレスが重複していた場合はエラーを返す
|
||||
if (isConflictError(externalUser)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010301'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const ramdomPassword = makePassword();
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
companyName,
|
||||
country,
|
||||
myAccountId,
|
||||
creatorAccountTier + 1,
|
||||
externalUser.sub,
|
||||
USER_ROLES.NONE,
|
||||
null,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create partner account failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 管理者ユーザを作成し、AzureADB2C IDを取得する
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
email,
|
||||
ramdomPassword,
|
||||
adminName,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
// メールアドレスが重複していた場合はエラーを返す
|
||||
if (isConflictError(externalUser)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010301'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
companyName,
|
||||
try {
|
||||
// 新規作成アカウント用のBlobコンテナを作成
|
||||
await this.blobStorageService.createContainer(
|
||||
context,
|
||||
account.id,
|
||||
country,
|
||||
myAccountId,
|
||||
tier + 1,
|
||||
externalUser.sub,
|
||||
USER_ROLES.NONE,
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create partner container failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
const from = this.configService.get<string>('MAIL_FROM') || '';
|
||||
const { subject, text, html } =
|
||||
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
|
||||
newAccount.id,
|
||||
adminUser.id,
|
||||
email,
|
||||
// DBのアカウントとユーザーを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
}
|
||||
|
||||
try {
|
||||
const from = this.configService.get<string>('MAIL_FROM') || '';
|
||||
const { subject, text, html } =
|
||||
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
|
||||
account.id,
|
||||
user.id,
|
||||
email,
|
||||
);
|
||||
await this.sendgridService.sendMail(
|
||||
context,
|
||||
email,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
return { accountId: account.id };
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create partner account send mail failed');
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
// DBのアカウントを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
// Blobコンテナを削除
|
||||
await this.deleteBlobContainer(account.id, country, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create partner account failed');
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.createPartnerAccount.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -446,11 +642,17 @@ export class AccountsService {
|
||||
|
||||
try {
|
||||
const currentDate = new DateWithZeroTime();
|
||||
// 第五階層のshortage算出に使用する日付情報
|
||||
// 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
|
||||
const expiringSoonDate = new ExpirationThresholdDate(
|
||||
currentDate.getTime(),
|
||||
);
|
||||
|
||||
const getPartnerLicenseResult =
|
||||
await this.accountRepository.getPartnerLicense(
|
||||
accountId,
|
||||
currentDate,
|
||||
expiringSoonDate,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
@ -471,27 +673,14 @@ export class AccountsService {
|
||||
},
|
||||
);
|
||||
|
||||
// 第五階層のshortage算出に使用する日付情報
|
||||
// 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
|
||||
const expiringSoonDate = new Date(currentDate.getTime());
|
||||
expiringSoonDate.setDate(
|
||||
currentDate.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
);
|
||||
expiringSoonDate.setHours(23, 59, 59, 999);
|
||||
|
||||
// 各子アカウントのShortageを算出してreturn用の変数にマージする
|
||||
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
|
||||
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
|
||||
let childShortage;
|
||||
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
|
||||
// 第五階層の場合計算式が異なるため、別途値を取得する
|
||||
const { expiringSoonLicense, allocatableLicenseWithMargin } =
|
||||
await this.accountRepository.getLicenseCountForShortage(
|
||||
childPartnerLicenseFromRepository.accountId,
|
||||
currentDate,
|
||||
expiringSoonDate,
|
||||
);
|
||||
childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
|
||||
childShortage =
|
||||
childPartnerLicenseFromRepository.allocatableLicenseWithMargin -
|
||||
childPartnerLicenseFromRepository.expiringSoonLicense;
|
||||
} else {
|
||||
childShortage =
|
||||
childPartnerLicenseFromRepository.stockLicense -
|
||||
@ -587,6 +776,72 @@ export class AccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 対象の注文を発行する
|
||||
* @param context
|
||||
* @param orderedAccountId
|
||||
* @param userId
|
||||
* @param tier
|
||||
* @param poNumber
|
||||
*/
|
||||
async issueLicense(
|
||||
context: Context,
|
||||
orderedAccountId: number,
|
||||
userId: string,
|
||||
tier: number,
|
||||
poNumber: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.issueLicense.name} | params: { ` +
|
||||
`orderedAccountId: ${orderedAccountId}, ` +
|
||||
`userId: ${userId}, ` +
|
||||
`tier: ${tier}, ` +
|
||||
`poNumber: ${poNumber} };`,
|
||||
);
|
||||
try {
|
||||
// アクセストークンからユーザーIDを取得する
|
||||
const myAccountId = (
|
||||
await this.usersRepository.findUserByExternalId(userId)
|
||||
).account_id;
|
||||
await this.licensesRepository.issueLicense(
|
||||
orderedAccountId,
|
||||
myAccountId,
|
||||
tier,
|
||||
poNumber,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case OrderNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010801'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case AlreadyIssuedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010803'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case LicensesShortageError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010804'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.issueLicense.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// dealersのアカウント情報を取得する
|
||||
async getDealers(): Promise<GetDealersResponse> {
|
||||
this.logger.log(`[IN] ${this.getDealers.name}`);
|
||||
|
||||
@ -14,6 +14,8 @@ import { UserGroup } from '../../../repositories/user_groups/entity/user_group.e
|
||||
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
|
||||
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
|
||||
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
|
||||
import { Context } from '../../../common/log';
|
||||
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
export type LicensesRepositoryMockValue = {
|
||||
getLicenseOrderHistoryInfo:
|
||||
@ -22,6 +24,7 @@ export type LicensesRepositoryMockValue = {
|
||||
orderHistories: LicenseOrder[];
|
||||
}
|
||||
| Error;
|
||||
issueLicense: undefined | Error;
|
||||
};
|
||||
export type UsersRepositoryMockValue = {
|
||||
findUserById: User | Error;
|
||||
@ -58,6 +61,15 @@ export type AccountsRepositoryMockValue = {
|
||||
};
|
||||
createAccount: { newAccount: Account; adminUser: User } | Error;
|
||||
};
|
||||
|
||||
export type BlobStorageServiceMockValue = {
|
||||
createContainer: void | Error;
|
||||
containerExists: boolean | Error;
|
||||
fileExists: boolean | Error;
|
||||
publishUploadSas: string | Error;
|
||||
publishDownloadSas: string | Error;
|
||||
};
|
||||
|
||||
export const makeAccountsServiceMock = async (
|
||||
accountsRepositoryMockValue: AccountsRepositoryMockValue,
|
||||
usersRepositoryMockValue: UsersRepositoryMockValue,
|
||||
@ -65,6 +77,7 @@ export const makeAccountsServiceMock = async (
|
||||
adB2cMockValue: AdB2cMockValue,
|
||||
configMockValue: ConfigMockValue,
|
||||
sendGridMockValue: SendGridMockValue,
|
||||
blobStorageMockValue: BlobStorageServiceMockValue,
|
||||
licensesRepositoryMockValue: LicensesRepositoryMockValue,
|
||||
): Promise<AccountsService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -90,6 +103,8 @@ export const makeAccountsServiceMock = async (
|
||||
return makeConfigMock(configMockValue);
|
||||
case SendGridService:
|
||||
return makeSendGridServiceMock(sendGridMockValue);
|
||||
case BlobstorageService:
|
||||
return makeBlobStorageServiceMock(blobStorageMockValue);
|
||||
case LicensesRepositoryService:
|
||||
return makeLicensesRepositoryMock(licensesRepositoryMockValue);
|
||||
}
|
||||
@ -127,10 +142,10 @@ export const makeAccountsRepositoryMock = (
|
||||
export const makeLicensesRepositoryMock = (
|
||||
value: LicensesRepositoryMockValue,
|
||||
) => {
|
||||
const { getLicenseOrderHistoryInfo } = value;
|
||||
const { getLicenseOrderHistoryInfo, issueLicense } = value;
|
||||
|
||||
return {
|
||||
findUserById:
|
||||
getLicenseOrderHistoryInfo:
|
||||
getLicenseOrderHistoryInfo instanceof Error
|
||||
? jest
|
||||
.fn<Promise<void>, []>()
|
||||
@ -141,6 +156,21 @@ export const makeLicensesRepositoryMock = (
|
||||
[]
|
||||
>()
|
||||
.mockResolvedValue(getLicenseOrderHistoryInfo),
|
||||
issueLicense:
|
||||
issueLicense instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(issueLicense)
|
||||
: jest
|
||||
.fn<
|
||||
Promise<{
|
||||
context: Context;
|
||||
orderedAccountId: number;
|
||||
myAccountId: number;
|
||||
tier: number;
|
||||
poNumber: string;
|
||||
}>,
|
||||
[]
|
||||
>()
|
||||
.mockResolvedValue(issueLicense),
|
||||
};
|
||||
};
|
||||
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
|
||||
@ -228,6 +258,42 @@ export const makeSendGridServiceMock = (value: SendGridMockValue) => {
|
||||
: jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeBlobStorageServiceMock = (
|
||||
value: BlobStorageServiceMockValue,
|
||||
) => {
|
||||
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)
|
||||
: jest.fn<Promise<void>, []>().mockResolvedValue(createContainer),
|
||||
publishUploadSas:
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
|
||||
export const makeDefaultAccountsRepositoryMockValue =
|
||||
(): AccountsRepositoryMockValue => {
|
||||
@ -402,5 +468,17 @@ export const makeDefaultLicensesRepositoryMockValue =
|
||||
},
|
||||
],
|
||||
},
|
||||
issueLicense: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const makeBlobStorageServiceMockValue =
|
||||
(): BlobStorageServiceMockValue => {
|
||||
return {
|
||||
containerExists: true,
|
||||
fileExists: true,
|
||||
publishUploadSas: 'https://blob-storage?sas-token',
|
||||
publishDownloadSas: 'https://blob-storage?sas-token',
|
||||
createContainer: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,9 +1,78 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||
import {
|
||||
License,
|
||||
LicenseOrder,
|
||||
} from '../../../repositories/licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
|
||||
// TODO: [PBI 2379] 他のUtilityからコピペしてきたもの。後日整理される前提。
|
||||
export const createAccountAndAdminUser = async (
|
||||
datasource: DataSource,
|
||||
adminExternalId: string,
|
||||
): Promise<{
|
||||
accountId: number;
|
||||
adminId: number;
|
||||
role: string;
|
||||
tier: number;
|
||||
}> => {
|
||||
const { identifiers: account_idf } = await datasource
|
||||
.getRepository(Account)
|
||||
.insert({
|
||||
tier: 1,
|
||||
country: 'JP',
|
||||
delegation_permission: false,
|
||||
locked: false,
|
||||
company_name: 'test inc.',
|
||||
verified: true,
|
||||
deleted_at: '',
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const account = account_idf.pop() as Account;
|
||||
|
||||
const { identifiers: user_idf } = await datasource
|
||||
.getRepository(User)
|
||||
.insert({
|
||||
account_id: account.id,
|
||||
external_id: adminExternalId,
|
||||
role: 'admin none',
|
||||
accepted_terms_version: '1.0',
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
encryption_password: 'password',
|
||||
prompt: true,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const user = user_idf.pop() as User;
|
||||
|
||||
// Accountの管理者を設定する
|
||||
await datasource.getRepository(Account).update(
|
||||
{ id: user.account_id },
|
||||
{
|
||||
primary_admin_user_id: user.id,
|
||||
},
|
||||
);
|
||||
|
||||
const accountResult = await getAccount(datasource, account.id);
|
||||
const userResult = await getUser(datasource, user.id);
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
adminId: user.id,
|
||||
role: userResult.role,
|
||||
tier: accountResult.tier,
|
||||
};
|
||||
};
|
||||
|
||||
export const createAccount = async (
|
||||
datasource: DataSource,
|
||||
@ -29,6 +98,80 @@ export const createAccount = async (
|
||||
return { accountId: account.id };
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定IDのアカウントを取得する
|
||||
* @param dataSource データソース
|
||||
* @param id アカウントID
|
||||
* @returns 該当アカウント
|
||||
*/
|
||||
export const getAccount = async (dataSource: DataSource, id: number) => {
|
||||
return await dataSource.getRepository(Account).findOne({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのアカウントを取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当アカウント一覧
|
||||
*/
|
||||
export const getAccounts = async (
|
||||
dataSource: DataSource,
|
||||
): Promise<Account[]> => {
|
||||
return await dataSource.getRepository(Account).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定ExternalIdのユーザーを取得する
|
||||
* @param dataSource データソース
|
||||
* @param externalId 外部ID
|
||||
* @returns 該当ユーザー
|
||||
*/
|
||||
export const getUserFromExternalID = async (
|
||||
dataSource: DataSource,
|
||||
externalId: string,
|
||||
) => {
|
||||
return await dataSource.getRepository(User).findOne({
|
||||
where: { external_id: externalId },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定Idのユーザーを取得する
|
||||
* @param dataSource データソース
|
||||
* @param externalId 外部ID
|
||||
* @returns 該当ユーザー
|
||||
*/
|
||||
export const getUser = async (
|
||||
datasource: DataSource,
|
||||
id: number,
|
||||
): Promise<User> => {
|
||||
const user = await datasource.getRepository(User).findOne({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのユーザーを取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当ユーザー一覧
|
||||
*/
|
||||
export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
|
||||
return await dataSource.getRepository(User).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのソート条件を取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当ソート条件一覧
|
||||
*/
|
||||
export const getSortCriteria = async (dataSource: DataSource) => {
|
||||
return await dataSource.getRepository(SortCriteria).find();
|
||||
};
|
||||
|
||||
export const createLicense = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
@ -50,6 +193,30 @@ export const createLicense = async (
|
||||
identifiers.pop() as License;
|
||||
};
|
||||
|
||||
// 有効期限・ステータス付きのライセンスを作成
|
||||
export const createLicenseSetExpiryDateAndStatus = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
expiryDate: Date,
|
||||
status: string,
|
||||
): Promise<void> => {
|
||||
const { identifiers } = await datasource.getRepository(License).insert({
|
||||
expiry_date: expiryDate,
|
||||
account_id: accountId,
|
||||
type: 'NORMAL',
|
||||
status: status,
|
||||
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 createLicenseOrder = async (
|
||||
datasource: DataSource,
|
||||
fromAccountId: number,
|
||||
@ -74,3 +241,31 @@ export const createLicenseOrder = async (
|
||||
});
|
||||
identifiers.pop() as License;
|
||||
};
|
||||
|
||||
export const createUser = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
external_id: string,
|
||||
role: string,
|
||||
author_id?: string | undefined,
|
||||
): Promise<{ userId: number; externalId: string; authorId: string }> => {
|
||||
const { identifiers } = await datasource.getRepository(User).insert({
|
||||
account_id: accountId,
|
||||
external_id: external_id,
|
||||
role: role,
|
||||
accepted_terms_version: '1.0',
|
||||
author_id: author_id,
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
encryption: false,
|
||||
prompt: false,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const user = identifiers.pop() as User;
|
||||
return { userId: user.id, externalId: external_id, authorId: author_id };
|
||||
};
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsInt, IsOptional, Matches, Min } from 'class-validator';
|
||||
import {
|
||||
IsEmail,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
Matches,
|
||||
MaxLength,
|
||||
Min,
|
||||
ArrayMinSize,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator';
|
||||
|
||||
export class CreateAccountRequest {
|
||||
@ApiProperty()
|
||||
@ -10,16 +20,17 @@ export class CreateAccountRequest {
|
||||
maxLength: 2,
|
||||
})
|
||||
country: string;
|
||||
@ApiProperty({ nullable: true })
|
||||
@ApiProperty({ required: false })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
dealerAccountId?: number;
|
||||
dealerAccountId?: number | undefined;
|
||||
@ApiProperty()
|
||||
adminName: string;
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
adminMail: string;
|
||||
@ApiProperty()
|
||||
@IsAdminPasswordvalid()
|
||||
adminPassword: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン' })
|
||||
acceptedTermsVersion: string;
|
||||
@ -120,6 +131,18 @@ export class GetTypistGroupsResponse {
|
||||
typistGroups: TypistGroup[];
|
||||
}
|
||||
|
||||
export class CreateTypistGroupRequest {
|
||||
@ApiProperty({ minLength: 1, maxLength: 50 })
|
||||
@MinLength(1)
|
||||
@MaxLength(50)
|
||||
typistGroupName: string;
|
||||
@ApiProperty({ minItems: 1 })
|
||||
@ArrayMinSize(1)
|
||||
typistIds: number[];
|
||||
}
|
||||
|
||||
export class CreateTypistGroupResponse {}
|
||||
|
||||
export class CreatePartnerAccountRequest {
|
||||
@ApiProperty()
|
||||
companyName: string;
|
||||
@ -189,9 +212,15 @@ export class GetPartnerLicensesResponse {
|
||||
childrenPartnerLicenses: PartnerLicenseInfo[];
|
||||
}
|
||||
|
||||
// 第五階層のshortage算出用
|
||||
export class PartnerLicenseInfoForShortage {
|
||||
expiringSoonLicense?: number;
|
||||
allocatableLicenseWithMargin?: number;
|
||||
}
|
||||
|
||||
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
|
||||
export type PartnerLicenseInfoForRepository = Omit<
|
||||
PartnerLicenseInfo,
|
||||
PartnerLicenseInfo & PartnerLicenseInfoForShortage,
|
||||
'shortage'
|
||||
>;
|
||||
|
||||
|
||||
@ -389,7 +389,7 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
|
||||
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId, userId } = await createUser(
|
||||
const { externalId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'typist-user-external-id',
|
||||
|
||||
@ -232,15 +232,11 @@ export class FilesService {
|
||||
|
||||
try {
|
||||
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
|
||||
const isContainerExist = await this.blobStorageService.containerExists(
|
||||
await this.blobStorageService.containerExists(
|
||||
context,
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
//TODO [Task2241] コンテナが無ければ作成しているが、アカウント登録時に作成するので本処理は削除予定。
|
||||
if (!isContainerExist) {
|
||||
await this.blobStorageService.createContainer(accountId, country);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.log(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
@ -21,6 +22,8 @@ import {
|
||||
IssueCardLicensesRequest,
|
||||
ActivateCardLicensesResponse,
|
||||
ActivateCardLicensesRequest,
|
||||
GetAllocatableLicensesResponse,
|
||||
GetAllocatableLicensesRequest,
|
||||
} from './types/types';
|
||||
import { Request } from 'express';
|
||||
import { retrieveAuthorizationToken } from '../../common/http/helper';
|
||||
@ -29,6 +32,7 @@ import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { makeContext } from '../../common/log';
|
||||
|
||||
@ApiTags('licenses')
|
||||
@Controller('licenses')
|
||||
@ -169,4 +173,47 @@ export class LicensesController {
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetAllocatableLicensesResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'getAllocatableLicenses',
|
||||
description: '割り当て可能なライセンスを取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
)
|
||||
@Get('/allocatable')
|
||||
async getAllocatableLicenses(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@Req() req: Request,
|
||||
): Promise<GetAllocatableLicensesResponse> {
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const payload = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(payload.userId);
|
||||
|
||||
const allocatableLicenses =
|
||||
await this.licensesService.getAllocatableLicenses(
|
||||
context,
|
||||
payload.userId,
|
||||
);
|
||||
|
||||
return allocatableLicenses;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
IssueCardLicensesRequest,
|
||||
IssueCardLicensesResponse,
|
||||
ActivateCardLicensesRequest,
|
||||
NewAllocatedLicenseExpirationDate,
|
||||
} from './types/types';
|
||||
import {
|
||||
makeDefaultAccountsRepositoryMockValue,
|
||||
@ -27,10 +28,16 @@ import {
|
||||
createCardLicense,
|
||||
createLicense,
|
||||
createCardLicenseIssue,
|
||||
createLicenseAllocationHistory,
|
||||
selectCardLicensesCount,
|
||||
selectCardLicense,
|
||||
selectLicense,
|
||||
selectLicenseAllocationHistory,
|
||||
} from './test/utility';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { IsNotIn } from 'class-validator';
|
||||
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
|
||||
|
||||
describe('LicensesService', () => {
|
||||
it('ライセンス注文が完了する', async () => {
|
||||
@ -324,11 +331,19 @@ describe('DBテスト', () => {
|
||||
|
||||
const cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
|
||||
const defaultAccountId = 150;
|
||||
const licenseId = 50;
|
||||
const license_id = 50;
|
||||
const issueId = 100;
|
||||
|
||||
await createLicense(source, licenseId, defaultAccountId);
|
||||
await createCardLicense(source, licenseId, issueId, cardLicenseKey);
|
||||
await createLicense(
|
||||
source,
|
||||
license_id,
|
||||
null,
|
||||
defaultAccountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createCardLicense(source, license_id, issueId, cardLicenseKey);
|
||||
await createCardLicenseIssue(source, issueId);
|
||||
|
||||
const service = module.get<LicensesService>(LicensesService);
|
||||
@ -338,10 +353,494 @@ describe('DBテスト', () => {
|
||||
source,
|
||||
cardLicenseKey,
|
||||
);
|
||||
const dbSelectResultFromLicense = await selectLicense(source, licenseId);
|
||||
const dbSelectResultFromLicense = await selectLicense(source, license_id);
|
||||
expect(
|
||||
dbSelectResultFromCardLicense.cardLicense.activated_at,
|
||||
).toBeDefined();
|
||||
expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId);
|
||||
});
|
||||
|
||||
it('取込可能なライセンスのみが取得できる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const now = new Date();
|
||||
const { accountId } = await createAccount(source);
|
||||
const { externalId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'userId',
|
||||
'admin',
|
||||
);
|
||||
|
||||
// ライセンスを作成する
|
||||
// 1件目
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
new Date(now.getTime() + 60 * 60 * 1000),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 2件目、expiry_dateがnull(OneYear)
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 3件目、1件目と同じ有効期限
|
||||
await createLicense(
|
||||
source,
|
||||
3,
|
||||
new Date(now.getTime() + 60 * 60 * 1000),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 4件目、expiry_dateが一番遠いデータ
|
||||
await createLicense(
|
||||
source,
|
||||
4,
|
||||
new Date(now.getTime() + 60 * 60 * 1000 * 2),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 5件目、expiry_dateがnull(OneYear)
|
||||
await createLicense(
|
||||
source,
|
||||
5,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 6件目、ライセンス状態が割当済
|
||||
await createLicense(
|
||||
source,
|
||||
6,
|
||||
new Date(now.getTime() + 60 * 60 * 1000 * 2),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 7件目、ライセンス状態が削除済
|
||||
await createLicense(
|
||||
source,
|
||||
7,
|
||||
new Date(now.getTime() + 60 * 60 * 1000 * 2),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.DELETED,
|
||||
null,
|
||||
);
|
||||
// 8件目、別アカウントの未割当のライセンス
|
||||
await createLicense(
|
||||
source,
|
||||
8,
|
||||
new Date(now.getTime() + 60 * 60 * 1000),
|
||||
accountId + 1,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
// 9件目、有効期限切れのライセンス
|
||||
await createLicense(
|
||||
source,
|
||||
9,
|
||||
new Date(now.getTime() - 60 * 60 * 1000 * 24),
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
const service = module.get<LicensesService>(LicensesService);
|
||||
const context = makeContext('userId');
|
||||
const response = await service.getAllocatableLicenses(context, externalId);
|
||||
// 対象外のデータは取得していないことを確認する
|
||||
expect(response.allocatableLicenses.length).toBe(5);
|
||||
// ソートして取得されていることを確認する
|
||||
// (expiry_dateがnullを最優先、次に有効期限が遠い順(同じ有効期限の場合はID昇順)
|
||||
expect(response.allocatableLicenses[0].licenseId).toBe(2);
|
||||
expect(response.allocatableLicenses[1].licenseId).toBe(5);
|
||||
expect(response.allocatableLicenses[2].licenseId).toBe(4);
|
||||
expect(response.allocatableLicenses[3].licenseId).toBe(1);
|
||||
expect(response.allocatableLicenses[4].licenseId).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ライセンス割り当て', () => {
|
||||
let source: DataSource = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('未割当のライセンスに対して、ライセンス割り当てが完了する', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
|
||||
const expiry_date = new NewAllocatedLicenseExpirationDate();
|
||||
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 1);
|
||||
const resultLicense = await selectLicense(source, 1);
|
||||
expect(resultLicense.license.allocated_user_id).toBe(userId);
|
||||
expect(resultLicense.license.status).toBe(
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
);
|
||||
expect(resultLicense.license.expiry_date.setMilliseconds(0)).toEqual(
|
||||
expiry_date.setMilliseconds(0),
|
||||
);
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
|
||||
userId,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 1);
|
||||
const result = await selectLicense(source, 1);
|
||||
expect(result.license.allocated_user_id).toBe(userId);
|
||||
expect(result.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
|
||||
expect(result.license.expiry_date).toEqual(date);
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
|
||||
userId,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
userId,
|
||||
);
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
|
||||
const expiry_date = new NewAllocatedLicenseExpirationDate();
|
||||
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 2);
|
||||
|
||||
// もともと割り当てられていたライセンスの状態確認
|
||||
const result1 = await selectLicense(source, 1);
|
||||
expect(result1.license.allocated_user_id).toBe(null);
|
||||
expect(result1.license.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE);
|
||||
expect(result1.license.expiry_date).toEqual(date);
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
|
||||
userId,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
|
||||
1,
|
||||
);
|
||||
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
// 新たに割り当てたライセンスの状態確認
|
||||
const result2 = await selectLicense(source, 2);
|
||||
expect(result2.license.allocated_user_id).toBe(userId);
|
||||
expect(result2.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
|
||||
expect(result2.license.expiry_date.setMilliseconds(0)).toEqual(
|
||||
expiry_date.setMilliseconds(0),
|
||||
);
|
||||
const newlicenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
2,
|
||||
);
|
||||
expect(newlicenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
|
||||
userId,
|
||||
);
|
||||
expect(
|
||||
newlicenseAllocationHistory.licenseAllocationHistory.license_id,
|
||||
).toBe(2);
|
||||
expect(
|
||||
newlicenseAllocationHistory.licenseAllocationHistory.is_allocated,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がNORMALのとき)', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
userId,
|
||||
);
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 2);
|
||||
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
|
||||
).toBe('NONE');
|
||||
});
|
||||
|
||||
it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がCARDのとき)', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
userId,
|
||||
);
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'CARD');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 2);
|
||||
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
|
||||
).toBe('CARD');
|
||||
});
|
||||
|
||||
it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がTRIALのとき)', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.TRIAL,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
userId,
|
||||
);
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.CARD,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(source, 1, userId, 1, 'TRIAL');
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
await service.allocateLicense(makeContext('trackingId'), userId, 2);
|
||||
|
||||
const licenseAllocationHistory = await selectLicenseAllocationHistory(
|
||||
source,
|
||||
userId,
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
|
||||
).toBe('TRIAL');
|
||||
});
|
||||
|
||||
it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
date,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||||
null,
|
||||
);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
|
||||
await expect(
|
||||
service.allocateLicense(makeContext('trackingId'), userId, 1),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('割り当て不可なライセンスを割り当てようとした場合、エラーになる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId } = await createUser(source, accountId, 'userId', 'admin');
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
null,
|
||||
);
|
||||
await createLicense(
|
||||
source,
|
||||
2,
|
||||
null,
|
||||
accountId,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.DELETED,
|
||||
null,
|
||||
);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
|
||||
await expect(
|
||||
service.allocateLicense(makeContext('trackingId'), userId, 1),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010806'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
await expect(
|
||||
service.allocateLicense(makeContext('trackingId'), userId, 2),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010806'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,7 +11,11 @@ import {
|
||||
} 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';
|
||||
import {
|
||||
GetAllocatableLicensesResponse,
|
||||
IssueCardLicensesResponse,
|
||||
} from './types/types';
|
||||
import { Context } from '../../common/log';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesService {
|
||||
@ -211,4 +215,44 @@ export class LicensesService {
|
||||
this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* get allocatable lisences
|
||||
* @param context
|
||||
* @param userId
|
||||
* @@returns AllocatableLicenseInfo[]
|
||||
*/
|
||||
async getAllocatableLicenses(
|
||||
context: Context,
|
||||
userId: string,
|
||||
): Promise<GetAllocatableLicensesResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.getAllocatableLicenses.name} | params: { ` +
|
||||
`userId: ${userId}, `,
|
||||
);
|
||||
// ユーザIDからアカウントIDを取得する
|
||||
try {
|
||||
const myAccountId = (
|
||||
await this.usersRepository.findUserByExternalId(userId)
|
||||
).account_id;
|
||||
// 割り当て可能なライセンスを取得する
|
||||
const allocatableLicenses =
|
||||
await this.licensesRepository.getAllocatableLicenses(myAccountId);
|
||||
|
||||
return {
|
||||
allocatableLicenses,
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('get allocatable lisences failed');
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.getAllocatableLicenses.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
License,
|
||||
CardLicense,
|
||||
CardLicenseIssue,
|
||||
LicenseAllocationHistory,
|
||||
} from '../../../repositories/licenses/entity/license.entity';
|
||||
|
||||
export const createAccount = async (
|
||||
@ -58,15 +59,19 @@ export const createUser = async (
|
||||
export const createLicense = async (
|
||||
datasource: DataSource,
|
||||
licenseId: number,
|
||||
expiry_date: Date,
|
||||
accountId: number,
|
||||
type: string,
|
||||
status: string,
|
||||
allocated_user_id: number,
|
||||
): Promise<void> => {
|
||||
const { identifiers } = await datasource.getRepository(License).insert({
|
||||
id: licenseId,
|
||||
expiry_date: null,
|
||||
expiry_date: expiry_date,
|
||||
account_id: accountId,
|
||||
type: 'card',
|
||||
status: 'Unallocated',
|
||||
allocated_user_id: null,
|
||||
type: type,
|
||||
status: status,
|
||||
allocated_user_id: allocated_user_id,
|
||||
order_id: null,
|
||||
deleted_at: null,
|
||||
delete_order_id: null,
|
||||
@ -114,6 +119,31 @@ export const createCardLicenseIssue = async (
|
||||
identifiers.pop() as CardLicenseIssue;
|
||||
};
|
||||
|
||||
export const createLicenseAllocationHistory = async (
|
||||
datasource: DataSource,
|
||||
historyId: number,
|
||||
userId: number,
|
||||
licenseId: number,
|
||||
type: string,
|
||||
): Promise<void> => {
|
||||
const { identifiers } = await datasource
|
||||
.getRepository(LicenseAllocationHistory)
|
||||
.insert({
|
||||
id: historyId,
|
||||
user_id: userId,
|
||||
license_id: licenseId,
|
||||
is_allocated: true,
|
||||
executed_at: new Date(),
|
||||
switch_from_type: type,
|
||||
deleted_at: null,
|
||||
created_by: null,
|
||||
created_at: new Date(),
|
||||
updated_by: null,
|
||||
updated_at: new Date(),
|
||||
});
|
||||
identifiers.pop() as LicenseAllocationHistory;
|
||||
};
|
||||
|
||||
export const selectCardLicensesCount = async (
|
||||
datasource: DataSource,
|
||||
): Promise<{ count: number }> => {
|
||||
@ -144,3 +174,22 @@ export const selectLicense = async (
|
||||
});
|
||||
return { license };
|
||||
};
|
||||
|
||||
export const selectLicenseAllocationHistory = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
licence_id: number,
|
||||
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory }> => {
|
||||
const licenseAllocationHistory = await datasource
|
||||
.getRepository(LicenseAllocationHistory)
|
||||
.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
license_id: licence_id,
|
||||
},
|
||||
order: {
|
||||
executed_at: 'DESC',
|
||||
},
|
||||
});
|
||||
return { licenseAllocationHistory };
|
||||
};
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsInt, Matches, Max, Min, Length } from 'class-validator';
|
||||
import {
|
||||
LICENSE_EXPIRATION_DAYS,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
TRIAL_LICENSE_EXPIRATION_DAYS,
|
||||
} from '../../../constants';
|
||||
|
||||
export class CreateOrdersRequest {
|
||||
@ApiProperty()
|
||||
@ -37,6 +42,19 @@ export class ActivateCardLicensesRequest {
|
||||
|
||||
export class ActivateCardLicensesResponse {}
|
||||
|
||||
export class GetAllocatableLicensesRequest {}
|
||||
|
||||
export class AllocatableLicenseInfo {
|
||||
@ApiProperty()
|
||||
licenseId: number;
|
||||
@ApiProperty()
|
||||
expiryDate: Date;
|
||||
}
|
||||
export class GetAllocatableLicensesResponse {
|
||||
@ApiProperty({ type: [AllocatableLicenseInfo] })
|
||||
allocatableLicenses: AllocatableLicenseInfo[];
|
||||
}
|
||||
|
||||
// ライセンス算出用に、その日の始まりの時刻(0:00:00.000)の日付を取得する
|
||||
export class DateWithZeroTime extends Date {
|
||||
constructor(...args: any[]) {
|
||||
@ -48,3 +66,44 @@ export class DateWithZeroTime extends Date {
|
||||
this.setHours(0, 0, 0, 0); // 時分秒を"0:00:00.000"に固定
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンスの算出用に、閾値となる時刻(23:59:59.999)の日付を取得する
|
||||
export class ExpirationThresholdDate extends Date {
|
||||
constructor(...args: any[]) {
|
||||
if (args.length === 0) {
|
||||
super(); // 引数がない場合、現在の日付で初期化
|
||||
} else {
|
||||
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
|
||||
}
|
||||
this.setDate(this.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS);
|
||||
this.setHours(23, 59, 59, 999); // 時分秒を"23:59:59.999"に固定
|
||||
}
|
||||
}
|
||||
|
||||
// 新規トライアルライセンス発行時の有効期限算出用に、30日後の日付を取得する
|
||||
export class NewTrialLicenseExpirationDate extends Date {
|
||||
constructor(...args: any[]) {
|
||||
if (args.length === 0) {
|
||||
super(); // 引数がない場合、現在の日付で初期化
|
||||
} else {
|
||||
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
|
||||
}
|
||||
this.setDate(this.getDate() + TRIAL_LICENSE_EXPIRATION_DAYS);
|
||||
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
|
||||
this.setMilliseconds(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 新規ライセンス割り当て時の有効期限算出用に、365日後の日付を取得する
|
||||
export class NewAllocatedLicenseExpirationDate extends Date {
|
||||
constructor(...args: any[]) {
|
||||
if (args.length === 0) {
|
||||
super(); // 引数がない場合、現在の日付で初期化
|
||||
} else {
|
||||
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
|
||||
}
|
||||
this.setDate(this.getDate() + LICENSE_EXPIRATION_DAYS);
|
||||
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
|
||||
this.setMilliseconds(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
|
||||
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
|
||||
import { UsersService } from '../users.service';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service';
|
||||
@ -31,6 +32,10 @@ export type UsersRepositoryMockValue = {
|
||||
existsAuthorId: boolean | Error;
|
||||
};
|
||||
|
||||
export type LicensesRepositoryMockValue = {
|
||||
// empty
|
||||
};
|
||||
|
||||
export type AdB2cMockValue = {
|
||||
getMetaData: B2cMetadata | Error;
|
||||
getSignKeySets: JwkSignKey[] | Error;
|
||||
@ -58,6 +63,7 @@ export type ConfigMockValue = {
|
||||
|
||||
export const makeUsersServiceMock = async (
|
||||
usersRepositoryMockValue: UsersRepositoryMockValue,
|
||||
licensesRepositoryMockValue: LicensesRepositoryMockValue,
|
||||
adB2cMockValue: AdB2cMockValue,
|
||||
sendGridMockValue: SendGridMockValue,
|
||||
configMockValue: ConfigMockValue,
|
||||
@ -75,6 +81,8 @@ export const makeUsersServiceMock = async (
|
||||
switch (token) {
|
||||
case UsersRepositoryService:
|
||||
return makeUsersRepositoryMock(usersRepositoryMockValue);
|
||||
case LicensesRepositoryService:
|
||||
return makeLicensesRepositoryMock();
|
||||
case AdB2cService:
|
||||
return makeAdB2cServiceMock(adB2cMockValue);
|
||||
case SendGridService:
|
||||
@ -239,6 +247,12 @@ export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const makeLicensesRepositoryMock = (): LicensesRepositoryMockValue => {
|
||||
return {
|
||||
// empty
|
||||
};
|
||||
};
|
||||
|
||||
export const makeSendGridMock = (value: SendGridMockValue) => {
|
||||
const { sendMail, createMailContentFromEmailConfirmForNormalUser } = value;
|
||||
|
||||
@ -283,7 +297,7 @@ export const makeDefaultSendGridlValue = (): SendGridMockValue => {
|
||||
|
||||
export const makeDefaultConfigValue = (): ConfigMockValue => {
|
||||
return {
|
||||
get: `test@example.co.jp`,
|
||||
get: `test@example.com`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -38,6 +38,72 @@ import { AdB2cMockValue, makeAdB2cServiceMock } from './users.service.mock';
|
||||
import { AdB2cService } from '../../../gateways/adb2c/adb2c.service';
|
||||
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../../constants';
|
||||
|
||||
export const createAccountAndAdminUser = async (
|
||||
datasource: DataSource,
|
||||
adminExternalId: string,
|
||||
): Promise<{
|
||||
accountId: number;
|
||||
adminId: number;
|
||||
role: string;
|
||||
tier: number;
|
||||
}> => {
|
||||
const { identifiers: account_idf } = await datasource
|
||||
.getRepository(Account)
|
||||
.insert({
|
||||
tier: 1,
|
||||
country: 'JP',
|
||||
delegation_permission: false,
|
||||
locked: false,
|
||||
company_name: 'test inc.',
|
||||
verified: true,
|
||||
deleted_at: '',
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const account = account_idf.pop() as Account;
|
||||
|
||||
const { identifiers: user_idf } = await datasource
|
||||
.getRepository(User)
|
||||
.insert({
|
||||
account_id: account.id,
|
||||
external_id: adminExternalId,
|
||||
role: 'admin none',
|
||||
accepted_terms_version: '1.0',
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
encryption_password: 'password',
|
||||
prompt: true,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const user = user_idf.pop() as User;
|
||||
|
||||
// Accountの管理者を設定する
|
||||
await datasource.getRepository(Account).update(
|
||||
{ id: user.account_id },
|
||||
{
|
||||
primary_admin_user_id: user.id,
|
||||
},
|
||||
);
|
||||
|
||||
const accountResult = await getAccount(datasource, account.id);
|
||||
const userResult = await getUser(datasource, user.id);
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
adminId: user.id,
|
||||
role: userResult.role,
|
||||
tier: accountResult.tier,
|
||||
};
|
||||
};
|
||||
|
||||
export const createAccount = async (
|
||||
datasource: DataSource,
|
||||
): Promise<{ accountId: number }> => {
|
||||
@ -68,6 +134,7 @@ export const createUser = async (
|
||||
encryption?: boolean | undefined,
|
||||
encryption_password?: string | undefined,
|
||||
prompt?: boolean | undefined,
|
||||
email_verified?: boolean | undefined,
|
||||
): Promise<{ userId: number; externalId: string }> => {
|
||||
const { identifiers } = await datasource.getRepository(User).insert({
|
||||
account_id: accountId,
|
||||
@ -75,7 +142,7 @@ export const createUser = async (
|
||||
role: role,
|
||||
accepted_terms_version: '1.0',
|
||||
author_id: author_id,
|
||||
email_verified: true,
|
||||
email_verified: email_verified ?? true,
|
||||
auto_renew: auto_renew,
|
||||
license_alert: true,
|
||||
notification: true,
|
||||
@ -91,6 +158,18 @@ export const createUser = async (
|
||||
return { userId: user.id, externalId: external_id };
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定IDのアカウントを取得する
|
||||
* @param dataSource データソース
|
||||
* @param id アカウントID
|
||||
* @returns 該当アカウント
|
||||
*/
|
||||
export const getAccount = async (dataSource: DataSource, id: number) => {
|
||||
return await dataSource.getRepository(Account).findOne({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
export const getUser = async (
|
||||
datasource: DataSource,
|
||||
id: number,
|
||||
@ -103,6 +182,45 @@ export const getUser = async (
|
||||
return user;
|
||||
};
|
||||
|
||||
export const getLicenses = async (
|
||||
datasource: DataSource,
|
||||
account_id: number,
|
||||
): Promise<License[]> => {
|
||||
const licenses = await datasource.getRepository(License).find({
|
||||
where: {
|
||||
account_id: account_id,
|
||||
},
|
||||
});
|
||||
return licenses;
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定外部IDを持つユーザーを取得する
|
||||
* @param dataSource データソース
|
||||
* @param externalId 外部ID
|
||||
* @returns 該当ユーザー
|
||||
*/
|
||||
export const getUserByExternalId = async (
|
||||
datasource: DataSource,
|
||||
externalId: string,
|
||||
): Promise<User> => {
|
||||
const user = await datasource.getRepository(User).findOne({
|
||||
where: {
|
||||
external_id: externalId,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのユーザーを取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当ユーザー一覧
|
||||
*/
|
||||
export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
|
||||
return await dataSource.getRepository(User).find();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param datasource
|
||||
|
||||
@ -239,3 +239,19 @@ export class PostUpdateUserRequest {
|
||||
}
|
||||
|
||||
export class PostUpdateUserResponse {}
|
||||
|
||||
export class AllocateLicenseRequest {
|
||||
@ApiProperty({ description: 'ユーザーID' })
|
||||
userId: number;
|
||||
@ApiProperty({ description: '割り当てるライセンスのID' })
|
||||
newLicenseId: number;
|
||||
}
|
||||
|
||||
export class AllocateLicenseResponse {}
|
||||
|
||||
export class DeallocateLicenseRequest {
|
||||
@ApiProperty({ description: 'ユーザーID' })
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export class DeallocateLicenseResponse {}
|
||||
|
||||
@ -33,6 +33,10 @@ import {
|
||||
GetSortCriteriaResponse,
|
||||
PostUpdateUserRequest,
|
||||
PostUpdateUserResponse,
|
||||
AllocateLicenseResponse,
|
||||
AllocateLicenseRequest,
|
||||
DeallocateLicenseResponse,
|
||||
DeallocateLicenseRequest,
|
||||
} from './types/types';
|
||||
import { UsersService } from './users.service';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@ -41,10 +45,11 @@ import {
|
||||
isSortDirection,
|
||||
isTaskListSortableAttribute,
|
||||
} from '../../common/types/sort';
|
||||
import { ADMIN_ROLES } from '../../constants';
|
||||
import { ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { UserRoles } from '../../common/types/role';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
@ -93,8 +98,8 @@ export class UsersController {
|
||||
async confirmUserAndInitPassword(
|
||||
@Body() body: ConfirmRequest,
|
||||
): Promise<ConfirmResponse> {
|
||||
console.log(body);
|
||||
await this.usersService.confirmUserAndInitPassword(body.token);
|
||||
const context = makeContext(uuidv4());
|
||||
await this.usersService.confirmUserAndInitPassword(context, body.token);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -171,8 +176,11 @@ export class UsersController {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(payload.userId);
|
||||
|
||||
//ユーザ作成処理
|
||||
await this.usersService.createUser(
|
||||
context,
|
||||
payload,
|
||||
name,
|
||||
role as UserRoles,
|
||||
@ -372,4 +380,94 @@ export class UsersController {
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: AllocateLicenseResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '割り当て失敗時',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'allocateLicense',
|
||||
description: 'ライセンスを割り当てます',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
)
|
||||
@Post('/license/allocate')
|
||||
async allocateLicense(
|
||||
@Body() body: AllocateLicenseRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<AllocateLicenseResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
await this.usersService.allocateLicense(
|
||||
context,
|
||||
body.userId,
|
||||
body.newLicenseId,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: DeallocateLicenseResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'すでにライセンスが割り当て解除されている時',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'deallocateLicense',
|
||||
description: 'ライセンス割り当てを解除します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
)
|
||||
@Post('/license/deallocate')
|
||||
async deallocateLicense(
|
||||
@Body() body: DeallocateLicenseRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<DeallocateLicenseResponse> {
|
||||
//API実装時に詳細をかいていく
|
||||
//const accessToken = retrieveAuthorizationToken(req);
|
||||
//const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
|
||||
//const context = makeContext(userId);
|
||||
|
||||
//await this.usersService.deallocateLicense(context, body.userId);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,12 +4,14 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersRepositoryModule,
|
||||
LicensesRepositoryModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
AdB2cModule,
|
||||
SendGridModule,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@ import {
|
||||
newUser,
|
||||
} from '../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
|
||||
import { GetRelationsResponse, User } from './types/types';
|
||||
import {
|
||||
AuthorIdAlreadyExistsError,
|
||||
@ -39,11 +40,16 @@ import {
|
||||
import { DateWithZeroTime } from '../licenses/types/types';
|
||||
import { Context } from '../../common/log';
|
||||
import { UserRoles } from '../../common/types/role';
|
||||
import {
|
||||
LicenseExpiredError,
|
||||
LicenseUnavailableError,
|
||||
} from '../../repositories/licenses/errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly licensesRepository: LicensesRepositoryService,
|
||||
private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly configService: ConfigService,
|
||||
@ -74,7 +80,9 @@ export class UsersService {
|
||||
try {
|
||||
// トランザクションで取得と更新をまとめる
|
||||
const userId = decodedToken.userId;
|
||||
await this.usersRepository.updateUserVerified(userId);
|
||||
await this.usersRepository.updateUserVerifiedAndCreateTrialLicense(
|
||||
userId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
if (e instanceof Error) {
|
||||
@ -115,6 +123,7 @@ export class UsersService {
|
||||
* @returns void
|
||||
*/
|
||||
async createUser(
|
||||
context: Context,
|
||||
accessToken: AccessToken,
|
||||
name: string,
|
||||
role: UserRoles,
|
||||
@ -127,7 +136,7 @@ export class UsersService {
|
||||
encryptionPassword?: string | undefined,
|
||||
prompt?: boolean | undefined,
|
||||
): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.createUser.name}`);
|
||||
this.logger.log(`[IN] [${context.trackingId}] ${this.createUser.name}`);
|
||||
|
||||
//DBよりアクセス者の所属するアカウントIDを取得する
|
||||
let adminUser: EntityUser;
|
||||
@ -176,6 +185,7 @@ export class UsersService {
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
ramdomPassword,
|
||||
name,
|
||||
@ -219,6 +229,10 @@ export class UsersService {
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create user failed');
|
||||
//リカバリー処理
|
||||
//Azure AD B2Cに登録したユーザー情報を削除する
|
||||
await this.deleteB2cUser(externalUser.sub, context);
|
||||
|
||||
switch (e.code) {
|
||||
case 'ER_DUP_ENTRY':
|
||||
//AuthorID重複エラー
|
||||
@ -248,11 +262,22 @@ export class UsersService {
|
||||
);
|
||||
|
||||
//SendGridAPIを呼び出してメールを送信する
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
await this.sendgridService.sendMail(
|
||||
context,
|
||||
email,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.error('create user failed');
|
||||
this.logger.error(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`);
|
||||
//リカバリー処理
|
||||
//Azure AD B2Cに登録したユーザー情報を削除する
|
||||
await this.deleteB2cUser(externalUser.sub, context);
|
||||
// DBからユーザーを削除する
|
||||
await this.deleteUser(newUser.id, context);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
@ -262,6 +287,35 @@ export class UsersService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Azure AD B2Cに登録したユーザー情報を削除する
|
||||
// TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteB2cUser(externalUserId: string, context: Context) {
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBに登録したユーザー情報を削除する
|
||||
private async deleteUser(userId: number, context: Context) {
|
||||
try {
|
||||
await this.usersRepository.deleteNormalUser(userId);
|
||||
this.logger.log(`[${context.trackingId}] delete user: ${userId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// roleを受け取って、roleに応じたnewUserを作成して返却する
|
||||
private createNewUserInfo(
|
||||
role: UserRoles,
|
||||
@ -313,8 +367,13 @@ export class UsersService {
|
||||
* confirm User And Init Password
|
||||
* @param token ユーザ仮登録時に払いだされるトークン
|
||||
*/
|
||||
async confirmUserAndInitPassword(token: string): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.confirmUserAndInitPassword.name}`);
|
||||
async confirmUserAndInitPassword(
|
||||
context: Context,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.confirmUserAndInitPassword.name}`,
|
||||
);
|
||||
|
||||
const pubKey = getPublicKey(this.configService);
|
||||
|
||||
@ -351,7 +410,14 @@ export class UsersService {
|
||||
const html = `<p>OMDS TOP PAGE URL.<p><a href="${domains}">${domains}"</a><br>temporary password: ${ramdomPassword}`;
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
await this.sendgridService.sendMail(
|
||||
context,
|
||||
email,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
@ -805,4 +871,51 @@ export class UsersService {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.updateUser.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ライセンスをユーザーに割り当てます
|
||||
* @param context
|
||||
* @param userId
|
||||
* @param newLicenseId
|
||||
*/
|
||||
async allocateLicense(
|
||||
context: Context,
|
||||
userId: number,
|
||||
newLicenseId: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.allocateLicense.name} | params: { ` +
|
||||
`userId: ${userId}, ` +
|
||||
`newLicenseId: ${newLicenseId}, `,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.licensesRepository.allocateLicense(userId, newLicenseId);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case LicenseExpiredError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010805'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case LicenseUnavailableError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010806'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.allocateLicense.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,11 +56,12 @@ export class AdB2cService {
|
||||
* @returns user
|
||||
*/
|
||||
async createUser(
|
||||
context: Context,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
): Promise<{ sub: string } | ConflictError> {
|
||||
this.logger.log(`[IN] ${this.createUser.name}`);
|
||||
this.logger.log(`[IN] [${context.trackingId}] ${this.createUser.name}`);
|
||||
try {
|
||||
// ユーザをADB2Cに登録
|
||||
const newUser = await this.graphClient.api('users/').post({
|
||||
@ -93,7 +94,7 @@ export class AdB2cService {
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] ${this.createUser.name}`);
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.createUser.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,6 +233,26 @@ export class AdB2cService {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.getUsers.name}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を削除する
|
||||
* @param externalId 外部ユーザーID
|
||||
* @param context コンテキスト
|
||||
*/
|
||||
async deleteUser(externalId: string, context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteUser.name} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
|
||||
await this.graphClient.api(`users/${externalId}`).delete();
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUser.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [Task2002] 文字列の配列を15要素ずつ区切る(この処理も別タスクで削除予定)
|
||||
|
||||
@ -51,13 +51,22 @@ export class BlobstorageService {
|
||||
this.sharedKeyCredentialEU,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates container
|
||||
* @param companyName
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @returns container
|
||||
*/
|
||||
async createContainer(accountId: number, country: string): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.createContainer.name}`);
|
||||
async createContainer(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createContainer.name}`,
|
||||
);
|
||||
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
@ -69,9 +78,54 @@ export class BlobstorageService {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] ${this.createContainer.name}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.createContainer.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定されたコンテナを削除します。(コンテナが存在しない場合、何もせず終了します)
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
*/
|
||||
async deleteContainer(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteContainer.name}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(accountId, country);
|
||||
const { succeeded, errorCode, date } =
|
||||
await containerClient.deleteIfExists();
|
||||
this.logger.log(
|
||||
`succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
|
||||
);
|
||||
|
||||
// 失敗時、コンテナが存在しない場合以外はエラーとして例外をスローする
|
||||
// コンテナ不在の場合のエラーコードは「ContainerNotFound」以下を参照
|
||||
// https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes
|
||||
if (!succeeded && errorCode !== 'ContainerNotFound') {
|
||||
throw new Error(
|
||||
`delete blob container failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.deleteContainer.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Containers exists
|
||||
* @param country
|
||||
|
||||
@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { sign } from '../../common/jwt';
|
||||
import sendgrid from '@sendgrid/mail';
|
||||
import { getPrivateKey } from '../../common/jwt/jwt';
|
||||
import { Context } from '../../common/log';
|
||||
|
||||
@Injectable()
|
||||
export class SendGridService {
|
||||
@ -20,10 +21,15 @@ export class SendGridService {
|
||||
* @returns メールのサブジェクトとコンテンツ
|
||||
*/
|
||||
async createMailContentFromEmailConfirm(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
userId: number,
|
||||
email: string,
|
||||
): Promise<{ subject: string; text: string; html: string }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`,
|
||||
);
|
||||
|
||||
const lifetime =
|
||||
this.configService.get<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
@ -39,6 +45,9 @@ export class SendGridService {
|
||||
const domains = this.configService.get<string>('APP_DOMAIN');
|
||||
const path = 'mail-confirm/';
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`,
|
||||
);
|
||||
return {
|
||||
subject: 'Verify your new account',
|
||||
text: `The verification URL. ${domains}${path}?verify=${token}`,
|
||||
@ -85,17 +94,23 @@ export class SendGridService {
|
||||
|
||||
/**
|
||||
* メールを送信する
|
||||
* @param accountId アカウントID
|
||||
* @param userId ユーザーID
|
||||
* @returns user confirm token
|
||||
* @param context
|
||||
* @param to
|
||||
* @param from
|
||||
* @param subject
|
||||
* @param text
|
||||
* @param html
|
||||
* @returns mail
|
||||
*/
|
||||
async sendMail(
|
||||
context: Context,
|
||||
to: string,
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(`[IN] [${context.trackingId}] ${this.sendMail.name}`);
|
||||
try {
|
||||
const res = await sendgrid
|
||||
.send({
|
||||
@ -114,8 +129,10 @@ export class SendGridService {
|
||||
`status code: ${res.statusCode} body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify(e));
|
||||
this.logger.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.sendMail.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ export class AccountsRepositoryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* プライマリ管理者とアカウントを同時に作成する
|
||||
* プライマリ管理者とアカウント、ソート条件を同時に作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param dealerAccountId
|
||||
@ -92,7 +92,7 @@ export class AccountsRepositoryService {
|
||||
async createAccount(
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | null,
|
||||
dealerAccountId: number | undefined,
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
@ -150,6 +150,27 @@ export class AccountsRepositoryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* プライマリ管理者とアカウント、ソート条件を同時に削除する
|
||||
* @param accountId
|
||||
* @returns delete
|
||||
*/
|
||||
async deleteAccount(accountId: number, userId: number): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
// ソート条件を削除
|
||||
await sortCriteriaRepo.delete({
|
||||
user_id: userId,
|
||||
});
|
||||
// プライマリ管理者を削除
|
||||
await usersRepo.delete({ id: userId });
|
||||
// アカウントを削除
|
||||
await accountsRepo.delete({ id: accountId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDからアカウント情報を取得する
|
||||
* @param id
|
||||
@ -429,6 +450,7 @@ export class AccountsRepositoryService {
|
||||
async getPartnerLicense(
|
||||
id: number,
|
||||
currentDate: Date,
|
||||
expiringSoonDate: Date,
|
||||
offset: number,
|
||||
limit: number,
|
||||
): Promise<{
|
||||
@ -486,6 +508,24 @@ export class AccountsRepositoryService {
|
||||
entityManager,
|
||||
);
|
||||
|
||||
// 第五の不足数を算出するためのライセンス数情報を取得する
|
||||
let expiringSoonLicense: number;
|
||||
let allocatableLicenseWithMargin: number;
|
||||
if (childAccount.tier === TIERS.TIER5) {
|
||||
expiringSoonLicense = await this.getExpiringSoonLicense(
|
||||
entityManager,
|
||||
childAccount.id,
|
||||
currentDate,
|
||||
expiringSoonDate,
|
||||
);
|
||||
allocatableLicenseWithMargin =
|
||||
await this.getAllocatableLicenseWithMargin(
|
||||
entityManager,
|
||||
childAccount.id,
|
||||
expiringSoonDate,
|
||||
);
|
||||
}
|
||||
|
||||
// 戻り値用の値を設定
|
||||
const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository =
|
||||
{
|
||||
@ -495,6 +535,8 @@ export class AccountsRepositoryService {
|
||||
stockLicense: childLicenseOrderStatus.stockLicense,
|
||||
issuedRequested: childLicenseOrderStatus.issuedRequested,
|
||||
issueRequesting: childLicenseOrderStatus.issueRequesting,
|
||||
expiringSoonLicense: expiringSoonLicense,
|
||||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||||
};
|
||||
|
||||
childPartnerLicensesFromRepository.push(
|
||||
@ -517,42 +559,6 @@ export class AccountsRepositoryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不足数を算出するためのライセンス数情報を取得する
|
||||
* @param id
|
||||
* @param currentDate
|
||||
* @param expiringSoonDate
|
||||
* @returns expiringSoonLicense
|
||||
* @returns expiringSoonDate
|
||||
*/
|
||||
async getLicenseCountForShortage(
|
||||
id: number,
|
||||
currentDate: Date,
|
||||
expiringSoonDate: Date,
|
||||
): Promise<{
|
||||
expiringSoonLicense: number;
|
||||
allocatableLicenseWithMargin: number;
|
||||
}> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const expiringSoonLicense = await this.getExpiringSoonLicense(
|
||||
entityManager,
|
||||
id,
|
||||
currentDate,
|
||||
expiringSoonDate,
|
||||
);
|
||||
const allocatableLicenseWithMargin =
|
||||
await this.getAllocatableLicenseWithMargin(
|
||||
entityManager,
|
||||
id,
|
||||
expiringSoonDate,
|
||||
);
|
||||
return {
|
||||
expiringSoonLicense: expiringSoonLicense,
|
||||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dealer(Tier4)アカウント情報を取得する
|
||||
* @returns dealer accounts
|
||||
|
||||
@ -22,16 +22,16 @@ export class Account {
|
||||
@Column()
|
||||
country: string;
|
||||
|
||||
@Column()
|
||||
@Column({ default: false })
|
||||
delegation_permission: boolean;
|
||||
|
||||
@Column()
|
||||
@Column({ default: false })
|
||||
locked: boolean;
|
||||
|
||||
@Column()
|
||||
company_name: string;
|
||||
|
||||
@Column()
|
||||
@Column({ default: false })
|
||||
verified: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@ -43,16 +43,16 @@ export class Account {
|
||||
@Column({ nullable: true })
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column()
|
||||
created_by: string;
|
||||
@Column({ nullable: true })
|
||||
created_by?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
updated_by: string;
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => User, (user) => user.id)
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../users/entity/user.entity';
|
||||
|
||||
@ -164,3 +165,43 @@ export class CardLicense {
|
||||
@UpdateDateColumn({})
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
@Entity({ name: 'license_allocation_history' })
|
||||
export class LicenseAllocationHistory {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column()
|
||||
license_id: number;
|
||||
|
||||
@Column()
|
||||
is_allocated: boolean;
|
||||
|
||||
@Column()
|
||||
executed_at: Date;
|
||||
|
||||
@Column()
|
||||
switch_from_type: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => License, (licenses) => licenses.id)
|
||||
@JoinColumn({ name: 'license_id' })
|
||||
license?: License;
|
||||
}
|
||||
|
||||
@ -6,3 +6,15 @@ export class LicenseNotExistError extends Error {}
|
||||
|
||||
// 取り込むライセンスが既に取り込み済みのエラー
|
||||
export class LicenseKeyAlreadyActivatedError extends Error {}
|
||||
|
||||
// 注文不在エラー
|
||||
export class OrderNotFoundError extends Error {}
|
||||
// 注文発行済エラー
|
||||
export class AlreadyIssuedError extends Error {}
|
||||
// ライセンス不足エラー
|
||||
export class LicensesShortageError extends Error {}
|
||||
|
||||
// ライセンス有効期限切れエラー
|
||||
export class LicenseExpiredError extends Error {}
|
||||
// ライセンス割り当て不可エラー
|
||||
export class LicenseUnavailableError extends Error {}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
CardLicenseIssue,
|
||||
License,
|
||||
LicenseOrder,
|
||||
LicenseAllocationHistory,
|
||||
} from './entity/license.entity';
|
||||
import { LicensesRepositoryService } from './licenses.repository.service';
|
||||
|
||||
@ -15,6 +16,7 @@ import { LicensesRepositoryService } from './licenses.repository.service';
|
||||
License,
|
||||
CardLicense,
|
||||
CardLicenseIssue,
|
||||
LicenseAllocationHistory,
|
||||
]),
|
||||
],
|
||||
providers: [LicensesRepositoryService],
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm';
|
||||
import {
|
||||
LicenseOrder,
|
||||
License,
|
||||
CardLicenseIssue,
|
||||
CardLicense,
|
||||
LicenseAllocationHistory,
|
||||
} from './entity/license.entity';
|
||||
import {
|
||||
CARD_LICENSE_LENGTH,
|
||||
@ -12,12 +13,24 @@ import {
|
||||
LICENSE_STATUS_ISSUE_REQUESTING,
|
||||
LICENSE_STATUS_ISSUED,
|
||||
LICENSE_TYPE,
|
||||
SWITCH_FROM_TYPE,
|
||||
TIERS,
|
||||
} from '../../constants';
|
||||
import {
|
||||
PoNumberAlreadyExistError,
|
||||
LicenseNotExistError,
|
||||
LicenseKeyAlreadyActivatedError,
|
||||
LicensesShortageError,
|
||||
AlreadyIssuedError,
|
||||
OrderNotFoundError,
|
||||
LicenseExpiredError,
|
||||
LicenseUnavailableError,
|
||||
} from './errors/types';
|
||||
import {
|
||||
AllocatableLicenseInfo,
|
||||
DateWithZeroTime,
|
||||
} from '../../features/licenses/types/types';
|
||||
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesRepositoryService {
|
||||
@ -90,6 +103,7 @@ export class LicensesRepositoryService {
|
||||
entityManager.getRepository(CardLicenseIssue);
|
||||
|
||||
const licenses = [];
|
||||
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
|
||||
// ライセンステーブルを作成する(BULK INSERT)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const license = new License();
|
||||
@ -146,6 +160,7 @@ export class LicensesRepositoryService {
|
||||
}
|
||||
|
||||
const cardLicenses = [];
|
||||
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
|
||||
// カードライセンステーブルを作成する(BULK INSERT)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cardLicense = new CardLicense();
|
||||
@ -296,4 +311,234 @@ export class LicensesRepositoryService {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 対象の注文を発行する
|
||||
* @context Context
|
||||
* @param orderedAccountId
|
||||
* @param myAccountId
|
||||
* @param tier
|
||||
* @param poNumber
|
||||
*/
|
||||
async issueLicense(
|
||||
orderedAccountId: number,
|
||||
myAccountId: number,
|
||||
tier: number,
|
||||
poNumber: string,
|
||||
): Promise<void> {
|
||||
const nowDate = new Date();
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
|
||||
const issuingOrder = await licenseOrderRepo.findOne({
|
||||
where: {
|
||||
from_account_id: orderedAccountId,
|
||||
po_number: poNumber,
|
||||
},
|
||||
});
|
||||
// 注文が存在しない場合、エラー
|
||||
if (!issuingOrder) {
|
||||
throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
|
||||
}
|
||||
// 既に発行済みの注文の場合、エラー
|
||||
if (issuingOrder.status !== LICENSE_STATUS_ISSUE_REQUESTING) {
|
||||
throw new AlreadyIssuedError(
|
||||
`An order for PONumber:${poNumber} has already been issued.`,
|
||||
);
|
||||
}
|
||||
|
||||
// ライセンステーブルのレコードを作成する
|
||||
const newLicenses = Array.from({ length: issuingOrder.quantity }, () => {
|
||||
const license = new License();
|
||||
license.account_id = orderedAccountId;
|
||||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||||
license.type = LICENSE_TYPE.NORMAL;
|
||||
license.order_id = issuingOrder.id;
|
||||
return license;
|
||||
});
|
||||
// ライセンス注文テーブルを更新(注文元)
|
||||
await licenseOrderRepo.update(
|
||||
{ id: issuingOrder.id },
|
||||
{
|
||||
issued_at: nowDate,
|
||||
status: LICENSE_STATUS_ISSUED,
|
||||
},
|
||||
);
|
||||
// ライセンステーブルを登録(注文元)
|
||||
await licenseRepo.save(newLicenses);
|
||||
|
||||
// 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
|
||||
if (tier !== TIERS.TIER1) {
|
||||
const licensesToUpdate = await licenseRepo.find({
|
||||
where: {
|
||||
account_id: myAccountId,
|
||||
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
type: LICENSE_TYPE.NORMAL,
|
||||
},
|
||||
order: {
|
||||
id: 'ASC',
|
||||
},
|
||||
take: newLicenses.length,
|
||||
});
|
||||
|
||||
// 登録したライセンスに対して自身のライセンスが不足していた場合、エラー
|
||||
if (newLicenses.length > licensesToUpdate.length) {
|
||||
throw new LicensesShortageError(
|
||||
`Shortage Licenses.Number of licenses attempted to be issued is ${newLicenses.length}.`,
|
||||
);
|
||||
}
|
||||
for (const licenseToUpdate of licensesToUpdate) {
|
||||
licenseToUpdate.status = LICENSE_ALLOCATED_STATUS.DELETED;
|
||||
licenseToUpdate.deleted_at = nowDate;
|
||||
licenseToUpdate.delete_order_id = issuingOrder.id;
|
||||
}
|
||||
// 自身のライセンスを削除(論理削除)する
|
||||
await licenseRepo.save(licensesToUpdate);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 対象のアカウントの割り当て可能なライセンスを取得する
|
||||
* @context Context
|
||||
* @param accountId
|
||||
* @param tier
|
||||
* @return AllocatableLicenseInfo[]
|
||||
*/
|
||||
async getAllocatableLicenses(
|
||||
myAccountId: number,
|
||||
): Promise<AllocatableLicenseInfo[]> {
|
||||
const nowDate = new DateWithZeroTime();
|
||||
const licenseRepo = this.dataSource.getRepository(License);
|
||||
const queryBuilder = licenseRepo
|
||||
.createQueryBuilder('license')
|
||||
.where('license.account_id = :accountId', { accountId: myAccountId })
|
||||
.andWhere('license.status IN (:...statuses)', {
|
||||
statuses: [
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||||
],
|
||||
})
|
||||
.andWhere(
|
||||
'(license.expiry_date >= :nowDate OR license.expiry_date IS NULL)',
|
||||
{ nowDate },
|
||||
)
|
||||
.orderBy('license.expiry_date IS NULL', 'DESC')
|
||||
.addOrderBy('license.expiry_date', 'DESC')
|
||||
.addOrderBy('license.id', 'ASC');
|
||||
const allocatableLicenses = await queryBuilder.getMany();
|
||||
return allocatableLicenses.map((license) => ({
|
||||
licenseId: license.id,
|
||||
expiryDate: license.expiry_date,
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* ライセンスをユーザーに割り当てる
|
||||
* @param userId
|
||||
* @param newLicenseId
|
||||
*/
|
||||
async allocateLicense(userId: number, newLicenseId: number): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||||
LicenseAllocationHistory,
|
||||
);
|
||||
// 割り当て対象のライセンス情報を取得
|
||||
const targetLicense = await licenseRepo.findOne({
|
||||
where: {
|
||||
id: newLicenseId,
|
||||
},
|
||||
});
|
||||
|
||||
// 期限切れの場合はエラー
|
||||
if (targetLicense.expiry_date) {
|
||||
const currentDay = new Date();
|
||||
currentDay.setHours(23, 59, 59, 999);
|
||||
if (targetLicense.expiry_date < currentDay) {
|
||||
throw new LicenseExpiredError(
|
||||
`License is expired. expiration date: ${targetLicense.expiry_date} current Date: ${currentDay}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// ライセンス状態が「未割当」「再利用可能」以外の場合はエラー
|
||||
if (
|
||||
targetLicense.status === LICENSE_ALLOCATED_STATUS.ALLOCATED ||
|
||||
targetLicense.status === LICENSE_ALLOCATED_STATUS.DELETED
|
||||
) {
|
||||
throw new LicenseUnavailableError(
|
||||
`License is unavailable. License status: ${targetLicense.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 対象ユーザーのライセンス割り当て状態を取得
|
||||
const allocatedLicense = await licenseRepo.findOne({
|
||||
where: {
|
||||
allocated_user_id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// 既にライセンスが割り当てられているなら、割り当てを解除
|
||||
if (allocatedLicense) {
|
||||
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
|
||||
allocatedLicense.allocated_user_id = null;
|
||||
|
||||
await licenseRepo.save(allocatedLicense);
|
||||
|
||||
// ライセンス割り当て履歴テーブルへ登録
|
||||
const deallocationHistory = new LicenseAllocationHistory();
|
||||
deallocationHistory.user_id = userId;
|
||||
deallocationHistory.license_id = allocatedLicense.id;
|
||||
deallocationHistory.is_allocated = false;
|
||||
deallocationHistory.executed_at = new Date();
|
||||
deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
|
||||
await licenseAllocationHistoryRepo.save(deallocationHistory);
|
||||
}
|
||||
|
||||
// ライセンス割り当てを実施
|
||||
targetLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
|
||||
targetLicense.allocated_user_id = userId;
|
||||
// 有効期限が未設定なら365日後に設定
|
||||
if (!targetLicense.expiry_date) {
|
||||
targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate();
|
||||
}
|
||||
await licenseRepo.save(targetLicense);
|
||||
|
||||
// 直近割り当てたライセンス種別を取得
|
||||
const oldLicenseType = await licenseAllocationHistoryRepo.findOne({
|
||||
relations: {
|
||||
license: true,
|
||||
},
|
||||
where: { user_id: userId, is_allocated: true },
|
||||
order: { executed_at: 'DESC' },
|
||||
});
|
||||
|
||||
let switchFromType = '';
|
||||
if (oldLicenseType) {
|
||||
switch (oldLicenseType.license.type) {
|
||||
case LICENSE_TYPE.CARD:
|
||||
switchFromType = SWITCH_FROM_TYPE.CARD;
|
||||
break;
|
||||
case LICENSE_TYPE.TRIAL:
|
||||
switchFromType = SWITCH_FROM_TYPE.TRIAL;
|
||||
break;
|
||||
default:
|
||||
switchFromType = SWITCH_FROM_TYPE.NONE;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switchFromType = SWITCH_FROM_TYPE.NONE;
|
||||
}
|
||||
|
||||
// ライセンス割り当て履歴テーブルへ登録
|
||||
const allocationHistory = new LicenseAllocationHistory();
|
||||
allocationHistory.user_id = userId;
|
||||
allocationHistory.license_id = targetLicense.id;
|
||||
allocationHistory.is_allocated = true;
|
||||
allocationHistory.executed_at = new Date();
|
||||
// TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応
|
||||
allocationHistory.switch_from_type = switchFromType;
|
||||
|
||||
await licenseAllocationHistoryRepo.save(allocationHistory);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,40 +33,40 @@ export class User {
|
||||
@Column({ nullable: true })
|
||||
accepted_terms_version?: string;
|
||||
|
||||
@Column()
|
||||
@Column({ default: false })
|
||||
email_verified: boolean;
|
||||
|
||||
@Column()
|
||||
@Column({ default: true })
|
||||
auto_renew: boolean;
|
||||
|
||||
@Column()
|
||||
@Column({ default: true })
|
||||
license_alert: boolean;
|
||||
|
||||
@Column()
|
||||
@Column({ default: true })
|
||||
notification: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ default: false })
|
||||
encryption?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
encryption_password?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ default: false })
|
||||
prompt?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
updated_by: string;
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.user)
|
||||
|
||||
@ -13,37 +13,19 @@ import {
|
||||
InvalidRoleChangeError,
|
||||
EncryptionPasswordNeedError,
|
||||
} from './errors/types';
|
||||
import { USER_ROLES } from '../../constants';
|
||||
import {
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_TYPE,
|
||||
TRIAL_LICENSE_ISSUE_NUM,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
import { License } from '../licenses/entity/license.entity';
|
||||
import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async create(
|
||||
accountId: number,
|
||||
externalUserId: string,
|
||||
role: string,
|
||||
acceptedTermsVersion: string,
|
||||
): Promise<User> {
|
||||
const user = new User();
|
||||
{
|
||||
user.account_id = accountId;
|
||||
user.external_id = externalUserId;
|
||||
user.role = role;
|
||||
user.accepted_terms_version = acceptedTermsVersion;
|
||||
}
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
const repo = entityManager.getRepository(User);
|
||||
const newUser = repo.create(user);
|
||||
const persisted = await repo.save(newUser);
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
return createdEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般ユーザーを作成する
|
||||
* @param user
|
||||
@ -285,6 +267,68 @@ export class UsersRepositoryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emailを認証済みにして、トライアルライセンスを作成する
|
||||
* @param id
|
||||
* @returns user verified and create trial license
|
||||
*/
|
||||
async updateUserVerifiedAndCreateTrialLicense(id: number): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const targetUser = await userRepo.findOne({
|
||||
relations: {
|
||||
account: true,
|
||||
},
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!targetUser) {
|
||||
throw new UserNotFoundError();
|
||||
}
|
||||
|
||||
if (targetUser.email_verified) {
|
||||
throw new EmailAlreadyVerifiedError();
|
||||
}
|
||||
|
||||
targetUser.email_verified = true;
|
||||
|
||||
await userRepo.update({ id: targetUser.id }, targetUser);
|
||||
|
||||
// トライアルライセンス100件を作成する
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
const licenses: License[] = [];
|
||||
|
||||
// トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
|
||||
const expiryDate = new NewTrialLicenseExpirationDate();
|
||||
|
||||
for (let i = 0; i < TRIAL_LICENSE_ISSUE_NUM; i++) {
|
||||
const license = new License();
|
||||
license.expiry_date = expiryDate;
|
||||
license.account_id = targetUser.account.id;
|
||||
license.type = LICENSE_TYPE.TRIAL;
|
||||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||||
licenses.push(license);
|
||||
}
|
||||
|
||||
await licenseRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(License)
|
||||
.values(
|
||||
licenses.map((value) => ({
|
||||
expiry_date: value.expiry_date,
|
||||
account_id: value.account_id,
|
||||
type: value.type,
|
||||
status: value.status,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同じアカウントIDを持つユーザーを探す
|
||||
* @param externalId
|
||||
@ -337,4 +381,22 @@ export class UsersRepositoryService {
|
||||
return typists;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UserID指定のユーザーとソート条件を同時に削除する
|
||||
* @param userId
|
||||
* @returns delete
|
||||
*/
|
||||
async deleteNormalUser(userId: number): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
// ソート条件を削除
|
||||
await sortCriteriaRepo.delete({
|
||||
user_id: userId,
|
||||
});
|
||||
// プライマリ管理者を削除
|
||||
await usersRepo.delete({ id: userId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user