Merge remote-tracking branch 'origin/develop' into main

This commit is contained in:
oura.a 2023-09-01 15:25:00 +09:00
commit 2f5daa46f5
75 changed files with 10585 additions and 7282 deletions

View File

@ -12,6 +12,7 @@ jobs:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin main:main
if git merge-base --is-ancestor $(Build.SourceVersion) main; then

View File

@ -15,6 +15,7 @@ jobs:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin main:main
if git merge-base --is-ancestor $(Build.SourceVersion) main; then

View File

@ -11,7 +11,7 @@
"networkInterfaces_pep_odms_app_dev_nic_6b27b52b_0703_4bfa_b69a_66b82ec6ca3e_name": {
"type": "String"
},
"networkInterfaces_pep_odms_app_test_nic_e7e4687e_685e_4023_bbab_a16ccfe8822b_name": {
"networkInterfaces_pep_odms_app_test_nic_714ca5c0_83a1_42fb_b8e4_8a2b5a2660ed_name": {
"type": "String"
},
"networkInterfaces_pep_odms_staapp_dev_nic_a67c70a7_750f_47d4_9844_b82b66095ef1_name": {
@ -436,6 +436,141 @@
"rules": [
{
"ruleId": "942440"
},
{
"ruleId": "942100"
},
{
"ruleId": "942110"
},
{
"ruleId": "942120"
},
{
"ruleId": "942130"
},
{
"ruleId": "942140"
},
{
"ruleId": "942150"
},
{
"ruleId": "942160"
},
{
"ruleId": "942170"
},
{
"ruleId": "942180"
},
{
"ruleId": "942190"
},
{
"ruleId": "942200"
},
{
"ruleId": "942210"
},
{
"ruleId": "942220"
},
{
"ruleId": "942230"
},
{
"ruleId": "942240"
},
{
"ruleId": "942250"
},
{
"ruleId": "942251"
},
{
"ruleId": "942270"
},
{
"ruleId": "942280"
},
{
"ruleId": "942290"
},
{
"ruleId": "942300"
},
{
"ruleId": "942310"
},
{
"ruleId": "942320"
},
{
"ruleId": "942330"
},
{
"ruleId": "942340"
},
{
"ruleId": "942350"
},
{
"ruleId": "942360"
},
{
"ruleId": "942361"
},
{
"ruleId": "942370"
},
{
"ruleId": "942380"
},
{
"ruleId": "942390"
},
{
"ruleId": "942400"
},
{
"ruleId": "942410"
},
{
"ruleId": "942420"
},
{
"ruleId": "942421"
},
{
"ruleId": "942430"
},
{
"ruleId": "942431"
},
{
"ruleId": "942432"
},
{
"ruleId": "942450"
},
{
"ruleId": "942460"
},
{
"ruleId": "942470"
},
{
"ruleId": "942480"
},
{
"ruleId": "942490"
},
{
"ruleId": "942500"
},
{
"ruleId": "942260"
}
]
}
@ -1739,7 +1874,7 @@
],
"kind": "Regular",
"location": "japaneast",
"name": "[parameters('networkInterfaces_pep_odms_app_test_nic_e7e4687e_685e_4023_bbab_a16ccfe8822b_name')]",
"name": "[parameters('networkInterfaces_pep_odms_app_test_nic_714ca5c0_83a1_42fb_b8e4_8a2b5a2660ed_name')]",
"properties": {
"disableTcpStateTracking": false,
"dnsSettings": {
@ -1749,9 +1884,9 @@
"enableIPForwarding": false,
"ipConfigurations": [
{
"etag": "W/\"4ae02394-b8c4-4949-b8c9-afa8f9a4816c\"",
"id": "[concat(resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaces_pep_odms_app_test_nic_e7e4687e_685e_4023_bbab_a16ccfe8822b_name')), '/ipConfigurations/privateEndpointIpConfig.2c5fae85-4959-4d63-ae7b-569ad00b2fdc')]",
"name": "privateEndpointIpConfig.2c5fae85-4959-4d63-ae7b-569ad00b2fdc",
"etag": "W/\"de5f333a-686a-419a-be07-4fb339cbf7b8\"",
"id": "[concat(resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaces_pep_odms_app_test_nic_714ca5c0_83a1_42fb_b8e4_8a2b5a2660ed_name')), '/ipConfigurations/privateEndpointIpConfig.474c2657-ac02-4810-8202-004da3c9cd93')]",
"name": "privateEndpointIpConfig.474c2657-ac02-4810-8202-004da3c9cd93",
"properties": {
"primary": true,
"privateIPAddress": "10.1.1.9",
@ -2047,7 +2182,7 @@
}
],
"metadata": {
"creator": "created by private endpoint pep-odms-app-test with resource guid 78a4dbd3-7b3f-436e-a7ae-3aba5cea7341"
"creator": "created by private endpoint pep-odms-app-test with resource guid f272f317-2526-4bbe-bfe9-18083902e925"
},
"ttl": 10
},
@ -2066,7 +2201,7 @@
}
],
"metadata": {
"creator": "created by private endpoint pep-odms-app-test with resource guid 78a4dbd3-7b3f-436e-a7ae-3aba5cea7341"
"creator": "created by private endpoint pep-odms-app-test with resource guid f272f317-2526-4bbe-bfe9-18083902e925"
},
"ttl": 10
},
@ -2298,8 +2433,8 @@
"manualPrivateLinkServiceConnections": [],
"privateLinkServiceConnections": [
{
"id": "[concat(resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpoints_pep_odms_app_test_name')), concat('/privateLinkServiceConnections/', parameters('privateEndpoints_pep_odms_app_test_name'), '-81c1'))]",
"name": "[concat(parameters('privateEndpoints_pep_odms_app_test_name'), '-81c1')]",
"id": "[concat(resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpoints_pep_odms_app_test_name')), concat('/privateLinkServiceConnections/', parameters('privateEndpoints_pep_odms_app_test_name'), '-bd85'))]",
"name": "[concat(parameters('privateEndpoints_pep_odms_app_test_name'), '-bd85')]",
"properties": {
"groupIds": [
"sites"
@ -11148,6 +11283,10 @@
"properties": {}
}
],
"sslPolicy": {
"policyName": "AppGwSslPolicy20220101",
"policyType": "Predefined"
},
"sslProfiles": [],
"trustedClientCertificates": [],
"trustedRootCertificates": [],

View File

@ -414,6 +414,141 @@
"rules": [
{
"ruleId": "942440"
},
{
"ruleId": "942100"
},
{
"ruleId": "942110"
},
{
"ruleId": "942120"
},
{
"ruleId": "942130"
},
{
"ruleId": "942140"
},
{
"ruleId": "942150"
},
{
"ruleId": "942160"
},
{
"ruleId": "942170"
},
{
"ruleId": "942180"
},
{
"ruleId": "942190"
},
{
"ruleId": "942200"
},
{
"ruleId": "942210"
},
{
"ruleId": "942220"
},
{
"ruleId": "942230"
},
{
"ruleId": "942240"
},
{
"ruleId": "942250"
},
{
"ruleId": "942251"
},
{
"ruleId": "942260"
},
{
"ruleId": "942270"
},
{
"ruleId": "942280"
},
{
"ruleId": "942290"
},
{
"ruleId": "942300"
},
{
"ruleId": "942310"
},
{
"ruleId": "942320"
},
{
"ruleId": "942330"
},
{
"ruleId": "942340"
},
{
"ruleId": "942350"
},
{
"ruleId": "942360"
},
{
"ruleId": "942361"
},
{
"ruleId": "942370"
},
{
"ruleId": "942380"
},
{
"ruleId": "942390"
},
{
"ruleId": "942400"
},
{
"ruleId": "942410"
},
{
"ruleId": "942420"
},
{
"ruleId": "942421"
},
{
"ruleId": "942430"
},
{
"ruleId": "942431"
},
{
"ruleId": "942432"
},
{
"ruleId": "942450"
},
{
"ruleId": "942460"
},
{
"ruleId": "942470"
},
{
"ruleId": "942480"
},
{
"ruleId": "942490"
},
{
"ruleId": "942500"
}
]
}
@ -813,7 +948,7 @@
"direction": "Inbound",
"priority": 903,
"protocol": "TCP",
"sourceAddressPrefix": "211.125.140.74",
"sourceAddressPrefix": "211.125.140.76",
"sourceAddressPrefixes": [],
"sourcePortRange": "*",
"sourcePortRanges": []
@ -1353,7 +1488,7 @@
"direction": "Inbound",
"priority": 903,
"protocol": "TCP",
"sourceAddressPrefix": "211.125.140.74",
"sourceAddressPrefix": "211.125.140.76",
"sourceAddressPrefixes": [],
"sourcePortRange": "*",
"sourcePortRanges": []
@ -10880,6 +11015,10 @@
"properties": {}
}
],
"sslPolicy": {
"policyName": "AppGwSslPolicy20220101",
"policyType": "Predefined"
},
"sslProfiles": [],
"trustedClientCertificates": [],
"trustedRootCertificates": [],

View File

@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.6.0"
"version": "7.0.0"
}
}

View File

@ -1 +1 @@
6.6.0
7.0.0

File diff suppressed because it is too large Load Diff

View File

@ -144,7 +144,7 @@ export const toPathString = function (url: URL) {
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View File

@ -46,4 +46,9 @@ export const errorCodes = [
"E010804", // ライセンス数不足エラー
"E010805", // ライセンス有効期限切れエラー
"E010806", // ライセンス割り当て不可エラー
"E010807", // ライセンス割り当て解除不可エラー
"E010808", // ライセンス注文キャンセル不可エラー
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
] as const;

View File

@ -1,6 +1,11 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LicenseOrderHistoryState } from "./state";
import { getLicenseOrderHistoriesAsync, issueLicenseAsync } from "./operations";
import {
getLicenseOrderHistoriesAsync,
issueLicenseAsync,
cancelOrderAsync,
cancelIssueAsync,
} from "./operations";
import { LIMIT_ORDER_HISORY_NUM } from "./constants";
const initialState: LicenseOrderHistoryState = {
@ -61,6 +66,24 @@ export const licenseOrderHistorySlice = createSlice({
builder.addCase(issueLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(cancelOrderAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(cancelOrderAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(cancelOrderAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(cancelIssueAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(cancelIssueAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(cancelIssueAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -2,7 +2,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { AccountsApi } from "../../../api/api";
import { AccountsApi, LicensesApi } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
import { OrderHistoryView } from "./types";
@ -141,3 +141,137 @@ export const issueLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const cancelOrderAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
poNumber: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/cancelOrderAsync", async (args, thunkApi) => {
const { poNumber } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
await licensesApi.cancelOrder(
{
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 === "E010808") {
errorMessage = getTranslationID(
"orderHistoriesPage.message.alreadyLicenseIssueOrCancel"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const cancelIssueAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
orderedAccountId: number;
poNumber: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/cancelIssueAsync", 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.cancelIssue(
{
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 === "E000108") {
errorMessage = getTranslationID("common.message.permissionDeniedError");
} else if (error.code === "E010809") {
errorMessage = getTranslationID(
"orderHistoriesPage.message.alreadyLicenseStatusChanged"
);
} else if (error.code === "E010810") {
errorMessage = getTranslationID(
"orderHistoriesPage.message.expiredSinceIssued"
);
} else if (error.code === "E010811") {
errorMessage = getTranslationID(
"orderHistoriesPage.message.alreadyLicenseAllocated"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -306,3 +306,67 @@ export const allocateLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deallocateLicenseAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
{
userId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/deallocateLicenseAsync", async (args, thunkApi) => {
const { userId } = 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.deallocateLicense(
{
userId,
},
{
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 === "E010807") {
errorMessage = getTranslationID(
"userListPage.message.alreadyLicenseDeallocatedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -6,6 +6,7 @@ import {
listUsersAsync,
updateUserAsync,
getAllocatableLicensesAsync,
deallocateLicenseAsync,
} from "./operations";
import { RoleType, UserView } from "./types";
@ -298,6 +299,15 @@ export const userSlice = createSlice({
builder.addCase(getAllocatableLicensesAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deallocateLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deallocateLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deallocateLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -2,7 +2,14 @@ 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 {
AccountsApi,
GetTypistGroupsResponse,
GetTypistsResponse,
CreateTypistGroupRequest,
Typist,
TypistGroup,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
@ -15,7 +22,7 @@ export const listTypistGroupsAsync = createAsyncThunk<
error: ErrorObject;
};
}
>("dictations/listTypistGroupsAsync", async (args, thunkApi) => {
>("workflow/listTypistGroupsAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
@ -41,3 +48,205 @@ export const listTypistGroupsAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const listTypistsAsync = createAsyncThunk<
GetTypistsResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/listTypistsAsync", 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 typists = await accountsApi.getTypists({
headers: { authorization: `Bearer ${accessToken}` },
});
return typists.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const createTypistGroupAsync = createAsyncThunk<
{
/* Empty Object */
},
CreateTypistGroupRequest,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/createTypistGroupAsync", 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 {
await accountsApi.createTypistGroup(args, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const message =
error.statusCode === 400
? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
: getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getTypistGroupAsync = createAsyncThunk<
{
typists: Typist[];
typistGroup: TypistGroup;
selectedTypistIds: number[];
},
{ typistGroupId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/getTypistGroupAsync", async (args, thunkApi) => {
const { typistGroupId } = 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 {
// タイピスト取得処理が別にあるが、storeの状態を意識せずに処理を行うためにここで取得する
const { typists } = (
await accountsApi.getTypists({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { typistGroupName, typistIds } = (
await accountsApi.getTypistGroup(typistGroupId, {
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
return {
typists,
typistGroup: { id: typistGroupId, name: typistGroupName },
selectedTypistIds: typistIds,
};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const message = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateTypistGroupAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/updateTypistGroupAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const { updateTypistGroupId, selectedTypists, groupName } =
state.typistGroup.apps;
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
if (!updateTypistGroupId) {
throw new Error("updateTypistGroupId is undefined.");
}
try {
await accountsApi.updateTypistGroup(
updateTypistGroupId,
{
typistGroupName: groupName,
typistIds: selectedTypists.map((x) => x.id),
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const message =
error.statusCode === 400
? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
: getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -5,3 +5,25 @@ export const selectTypistGroups = (state: RootState) =>
export const selectIsLoading = (state: RootState) =>
state.typistGroup.apps.isLoading;
export const selectTypists = (state: RootState) =>
state.typistGroup.domain.typists;
export const selectPoolTypists = (state: RootState) =>
state.typistGroup.domain.typists.filter(
(t) => !state.typistGroup.apps.selectedTypists.some((x) => t.id === x.id)
);
export const selectSelectedTypists = (state: RootState) =>
state.typistGroup.apps.selectedTypists;
export const selectGroupName = (state: RootState) =>
state.typistGroup.apps.groupName;
export const selectAddGroupErrors = (state: RootState) => {
const hasErrorEmptyGroupName = state.typistGroup.apps.groupName === "";
const hasErrorSelectedTypistsEmpty =
state.typistGroup.apps.selectedTypists.length === 0;
return { hasErrorEmptyGroupName, hasErrorSelectedTypistsEmpty };
};

View File

@ -1,4 +1,4 @@
import { TypistGroup } from "../../../api/api";
import { Typist, TypistGroup } from "../../../api/api";
export interface TypistGroupState {
apps: Apps;
@ -7,8 +7,12 @@ export interface TypistGroupState {
export interface Apps {
isLoading: boolean;
selectedTypists: Typist[];
groupName: string;
updateTypistGroupId?: number;
}
export interface Domain {
typistGroups: TypistGroup[];
typists: Typist[];
}

View File

@ -1,20 +1,61 @@
import { createSlice } from "@reduxjs/toolkit";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { Typist } from "api";
import { TypistGroupState } from "./state";
import { listTypistGroupsAsync } from "./operations";
import {
getTypistGroupAsync,
listTypistGroupsAsync,
listTypistsAsync,
updateTypistGroupAsync,
} from "./operations";
const initialState: TypistGroupState = {
apps: {
isLoading: false,
selectedTypists: [],
groupName: "",
},
domain: {
typistGroups: [],
typists: [],
},
};
export const typistGroupSlice = createSlice({
name: "typistGroup",
initialState,
reducers: {},
reducers: {
cleanupTypistGroup: (state) => {
state.apps.groupName = "";
state.apps.selectedTypists = [];
state.apps.updateTypistGroupId = undefined;
state.domain.typists = [];
},
addSelectedTypist: (state, action: PayloadAction<{ typist: Typist }>) => {
const { typist } = action.payload;
const selectedTypists = [...state.apps.selectedTypists, typist];
if (!state.apps.selectedTypists.find((x) => x.id === typist.id)) {
state.apps.selectedTypists = selectedTypists.sort(
(a, b) => a.id - b.id
);
}
},
removeSelectedTypist: (
state,
action: PayloadAction<{ typist: Typist }>
) => {
const { typist } = action.payload;
const selectedTypists = state.apps.selectedTypists.filter(
(x) => x.id !== typist.id
);
state.apps.selectedTypists = selectedTypists.sort((a, b) => a.id - b.id);
},
changeGroupName: (state, action: PayloadAction<{ groupName: string }>) => {
const { groupName } = action.payload;
state.apps.groupName = groupName;
},
},
extraReducers: (builder) => {
builder.addCase(listTypistGroupsAsync.pending, (state) => {
state.apps.isLoading = true;
@ -26,7 +67,53 @@ export const typistGroupSlice = createSlice({
builder.addCase(listTypistGroupsAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(listTypistsAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(listTypistsAsync.fulfilled, (state, action) => {
state.domain.typists = action.payload.typists;
state.apps.isLoading = false;
});
builder.addCase(listTypistsAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTypistGroupAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getTypistGroupAsync.fulfilled, (state, action) => {
const { typistGroup, selectedTypistIds, typists } = action.payload;
// 対象タイピストグループのID・名前を設定
state.apps.updateTypistGroupId = typistGroup.id;
state.apps.groupName = typistGroup.name;
// 選択済みのタイピストを設定
state.apps.selectedTypists = typists
.filter((x) => selectedTypistIds.includes(x.id))
.sort((a, b) => a.id - b.id);
// すべてのタイピストを設定
state.domain.typists = typists;
state.apps.isLoading = false;
});
builder.addCase(updateTypistGroupAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateTypistGroupAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
cleanupTypistGroup,
addSelectedTypist,
removeSelectedTypist,
changeGroupName,
} = typistGroupSlice.actions;
export default typistGroupSlice.reducer;

View File

@ -1,6 +1,8 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { useCallback, useEffect } from "react";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { isApproveTier } from "features/auth/utils";
import { TIERS } from "components/auth/constants";
import Footer from "components/footer";
import Header from "components/header";
import styles from "styles/app.module.scss";
@ -18,9 +20,11 @@ import {
selectTotal,
selectTotalPage,
issueLicenseAsync,
cancelOrderAsync,
selectOffset,
savePageInfo,
selectCompanyName,
cancelIssueAsync,
} from "features/license/licenseOrderHistory";
import { selectSelectedRow } from "features/license/partnerLicense";
import undo from "../../assets/images/undo.svg";
@ -85,6 +89,49 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
[dispatch]
);
// Order Cancelボタンを押下時の処理
const onCancelOrder = useCallback(
async (poNumber: string) => {
// ダイアログ確認
// eslint-disable-next-line no-alert
if (window.confirm(t(getTranslationID("common.message.dialogConfirm")))) {
// ライセンス注文キャンセルAPIの呼び出し
const { meta } = await dispatch(
cancelOrderAsync({
poNumber,
})
);
if (meta.requestStatus === "fulfilled") {
UpdateHistoriesList();
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch]
);
// Issue Cancelボタンを押下時の処理
const onCancelIssue = useCallback(
async (orderedAccountId: number, poNumber: string) => {
// ダイアログ確認
// eslint-disable-next-line no-alert
if (window.confirm(t(getTranslationID("common.message.dialogConfirm")))) {
// ライセンス発行キャンセルAPIの呼び出し
const { meta } = await dispatch(
cancelIssueAsync({
orderedAccountId,
poNumber,
})
);
if (meta.requestStatus === "fulfilled") {
UpdateHistoriesList();
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch]
);
// ページネーションのボタンクリック時のアクション
const movePage = (targetOffset: number) => {
dispatch(
@ -207,12 +254,16 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
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.menuLink} ${
x.status === STATUS.ISSUE_REQESTING
? styles.isActive
: ""
}`}
onClick={() => {
onCancelOrder(x.poNumber);
}}
>
{t(
getTranslationID(
@ -231,7 +282,12 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
{/* 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
x.status === STATUS.ISSUE_REQESTING &&
/* ログインユーザーの子階層のみissueボタンを表示 */
selectedRow.tier > 1 &&
isApproveTier([
(selectedRow.tier - 1).toString(),
])
? styles.isActive
: ""
}`}
@ -248,12 +304,21 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${
x.status === STATUS.ISSUED
x.status === STATUS.ISSUED &&
isIssueCancelVisibleTier(selectedRow.tier)
? styles.isActive
: ""
}`}
onClick={(event) => {
event.preventDefault();
onCancelIssue(
selectedRow.accountId,
x.poNumber
);
}}
>
{t(
getTranslationID(
@ -345,4 +410,10 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
);
};
// isIssueCanceボタンが表示できる階層かどうかを判定する
const isIssueCancelVisibleTier = (selectedRowsTier: number) =>
// 自アカウントが階層1または2、かつ対象が階層5の場合ボタン表示
isApproveTier([TIERS.TIER1, TIERS.TIER2]) &&
TIERS.TIER5 === selectedRowsTier.toString();
export default LicenseOrderHistory;

View File

@ -0,0 +1,203 @@
import React, { useCallback, useEffect, useState } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { Typist } from "api";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
addSelectedTypist,
listTypistsAsync,
removeSelectedTypist,
selectPoolTypists,
selectSelectedTypists,
selectGroupName,
changeGroupName,
selectAddGroupErrors,
cleanupTypistGroup,
} from "features/workflow/typistGroup";
import {
createTypistGroupAsync,
listTypistGroupsAsync,
} from "features/workflow/typistGroup/operations";
import { openSnackbar } from "features/ui";
import close from "../../assets/images/close.svg";
interface AddTypistGroupPopupProps {
onClose: (isChanged: boolean) => void;
isOpen: boolean;
}
export const AddTypistGroupPopup: React.FC<AddTypistGroupPopupProps> = (
props
) => {
const { onClose, isOpen } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const [isPushAddButton, setIsPushAddButton] = useState<boolean>(false);
const poolTypists = useSelector(selectPoolTypists);
const selectedTypists = useSelector(selectSelectedTypists);
const groupName = useSelector(selectGroupName);
const { hasErrorEmptyGroupName, hasErrorSelectedTypistsEmpty } =
useSelector(selectAddGroupErrors);
// 開閉時のみ実行
useEffect(() => {
if (isOpen) {
dispatch(listTypistsAsync());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
setIsPushAddButton(false);
onClose(false);
dispatch(cleanupTypistGroup());
}, [dispatch, onClose]);
// グループ追加を実行
const addTypistGroup = useCallback(async () => {
setIsPushAddButton(true);
// 入力チェック
if (hasErrorEmptyGroupName || hasErrorSelectedTypistsEmpty) {
if (hasErrorSelectedTypistsEmpty) {
dispatch(
openSnackbar({
level: "error",
message: t(
getTranslationID(
"typistGroupSetting.message.selectedTypistEmptyError"
)
),
})
);
}
return;
}
// グループ追加APIを実行
const { meta } = await dispatch(
createTypistGroupAsync({
typistGroupName: groupName,
typistIds: selectedTypists.map((typist) => typist.id),
})
);
setIsPushAddButton(false);
if (meta.requestStatus === "fulfilled") {
closePopup();
dispatch(listTypistGroupsAsync());
}
}, [
t,
closePopup,
dispatch,
groupName,
selectedTypists,
hasErrorEmptyGroupName,
hasErrorSelectedTypistsEmpty,
]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("typistGroupSetting.label.addTypistGroup"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("typistGroupSetting.label.groupName"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={50}
className={styles.formInput}
value={groupName}
onChange={(e) =>
dispatch(changeGroupName({ groupName: e.target.value }))
}
/>
{isPushAddButton && hasErrorEmptyGroupName && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("typistGroupSetting.label.transcriptionist"))}
</dt>
<dd className={`${styles.formChange} ${styles.last}`}>
<ul className={styles.chooseMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.selected"))}
</li>
{selectedTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
checked
onClick={() => dispatch(removeSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.remove")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
<p />
<ul className={styles.holdMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.pool"))}
</li>
{poolTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
onClick={() => dispatch(addSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.add")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("typistGroupSetting.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={addTypistGroup}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,197 @@
import React, { useCallback, useEffect, useState } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { Typist } from "api";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
addSelectedTypist,
cleanupTypistGroup,
removeSelectedTypist,
selectPoolTypists,
selectSelectedTypists,
selectGroupName,
changeGroupName,
selectAddGroupErrors,
} from "features/workflow/typistGroup";
import {
getTypistGroupAsync,
listTypistGroupsAsync,
updateTypistGroupAsync,
} from "features/workflow/typistGroup/operations";
import { openSnackbar } from "features/ui";
import close from "../../assets/images/close.svg";
interface EditTypistGroupPopupProps {
onClose: (isChanged: boolean) => void;
isOpen: boolean;
typistGroupId: number;
}
export const EditTypistGroupPopup: React.FC<EditTypistGroupPopupProps> = (
props
) => {
const { onClose, isOpen, typistGroupId } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const [isPushEditButton, setIsPushEditButton] = useState<boolean>(false);
const poolTypists = useSelector(selectPoolTypists);
const selectedTypists = useSelector(selectSelectedTypists);
const groupName = useSelector(selectGroupName);
const { hasErrorEmptyGroupName, hasErrorSelectedTypistsEmpty } =
useSelector(selectAddGroupErrors);
// 表示時のみ実行
useEffect(() => {
if (isOpen) {
dispatch(getTypistGroupAsync({ typistGroupId }));
}
}, [dispatch, isOpen, typistGroupId]);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
setIsPushEditButton(false);
onClose(false);
dispatch(cleanupTypistGroup());
}, [dispatch, onClose]);
// グループ更新を実行
const editTypistGroup = useCallback(async () => {
setIsPushEditButton(true);
// 入力チェック
if (hasErrorEmptyGroupName || hasErrorSelectedTypistsEmpty) {
if (hasErrorSelectedTypistsEmpty) {
dispatch(
openSnackbar({
level: "error",
message: t(
getTranslationID(
"typistGroupSetting.message.selectedTypistEmptyError"
)
),
})
);
}
return;
}
// グループ追加APIを実行
const { meta } = await dispatch(updateTypistGroupAsync());
setIsPushEditButton(false);
if (meta.requestStatus === "fulfilled") {
closePopup();
dispatch(listTypistGroupsAsync());
dispatch(cleanupTypistGroup());
}
}, [
t,
closePopup,
dispatch,
hasErrorEmptyGroupName,
hasErrorSelectedTypistsEmpty,
]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("typistGroupSetting.label.editTypistGroup"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("typistGroupSetting.label.groupName"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={50}
className={styles.formInput}
value={groupName}
onChange={(e) =>
dispatch(changeGroupName({ groupName: e.target.value }))
}
/>
{isPushEditButton && hasErrorEmptyGroupName && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("typistGroupSetting.label.transcriptionist"))}
</dt>
<dd className={`${styles.formChange} ${styles.last}`}>
<ul className={styles.chooseMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.selected"))}
</li>
{selectedTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
checked
onClick={() => dispatch(removeSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.remove")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
<p />
<ul className={styles.holdMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.pool"))}
</li>
{poolTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
onClick={() => dispatch(addSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.add")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("typistGroupSetting.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={editTypistGroup}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
@ -15,6 +15,8 @@ import {
import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { AddTypistGroupPopup } from "./addTypistGroupPopup";
import { EditTypistGroupPopup } from "./editTypistGroupPopup";
const TypistGroupSettingPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -23,95 +25,135 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
const isLoading = useSelector(selectIsLoading);
const typistGroup = useSelector(selectTypistGroups);
const [isAddPopupOpen, setIsAddPopupOpen] = useState(false);
const [isEditPopupOpen, setIsEditPopupOpen] = useState(false);
const [editTypistGroupId, setEditTypistGroupId] = useState<number>(NaN);
const onAddPopupOpen = useCallback(() => {
// typist一覧を取得
setIsAddPopupOpen(true);
}, [setIsAddPopupOpen]);
const onEditPopupOpen = useCallback(
(typistGroupId: number) => {
setEditTypistGroupId(typistGroupId);
setIsEditPopupOpen(true);
},
[setIsEditPopupOpen]
);
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"
/>
)}
<>
<AddTypistGroupPopup
onClose={() => {
setIsAddPopupOpen(false);
}}
isOpen={isAddPopupOpen}
/>
<EditTypistGroupPopup
onClose={() => {
setIsEditPopupOpen(false);
}}
isOpen={isEditPopupOpen}
typistGroupId={editTypistGroupId}
/>
<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>
</div>
</main>
<Footer />
</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>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onAddPopupOpen}
>
<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>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
onEditPopupOpen(group.id);
}}
>
{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>
</>
);
};

View File

@ -9,6 +9,7 @@ import {
listUsersAsync,
selectUserViews,
selectIsLoading,
deallocateLicenseAsync,
} from "features/user";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
@ -57,6 +58,24 @@ const UserListPage: React.FC = (): JSX.Element => {
[setIsAllocateLicensePopupOpen, dispatch]
);
const onLicenseDeallocation = useCallback(
async (userId: number) => {
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listUsersAsync());
}
},
[dispatch, t]
);
useEffect(() => {
// ユーザ一覧取得処理を呼び出す
dispatch(listUsersAsync());
@ -196,7 +215,18 @@ const UserListPage: React.FC = (): JSX.Element => {
</a>
</li>
<li>
<a href="">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={
user.licenseStatus ===
LICENSE_STATUS.NOLICENSE
? styles.isDisable
: ""
}
onClick={() => {
onLicenseDeallocation(user.id);
}}
>
{t(
getTranslationID(
"userListPage.label.licenseDeallocation"

View File

@ -1,13 +1,14 @@
{
"common": {
"message": {
"inputEmptyError": "(de)Error Message",
"inputEmptyError": "(de)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(de)Error Message",
"emailIncorrectError": "(de)Error Message",
"internalServerError": "(de)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"listEmpty": "(de)検索結果が0件です。",
"dialogConfirm": "(de)操作を実行しますか?",
"success": "(de)処理に成功しました。"
"success": "(de)処理に成功しました。",
"permissionDeniedError": "(de)操作を実行する権限がありません。"
},
"label": {
"cancel": "(de)Cancel",
@ -113,7 +114,8 @@
"authorIdConflictError": "(de)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(de)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"roleChangeError": "(de)Roleの変更に失敗しました。画面を更新して再度ユーザー情報を取得してください。",
"encryptionPasswordCorrectError": "(de)EncryptionPasswordがルールを満たしていません。"
"encryptionPasswordCorrectError": "(de)EncryptionPasswordがルールを満たしていません。",
"alreadyLicenseDeallocatedError": "(de)すでにライセンス割り当てが解除されています。画面を更新して再度ご確認下さい。"
},
"label": {
"title": "(de)User",
@ -235,7 +237,7 @@
"deleteDictation": "(de)Delete Dictation",
"selectedTranscriptionist": "(de)Selected",
"poolTranscriptionist": "(de)Pool",
"saveChanges": "(de)Save changes"
"saveChanges": "(de)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -328,7 +330,11 @@
},
"message": {
"notEnoughOfNumberOfLicense": "(de)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
"alreadyIssueLicense": "(de)すでに発行済みの注文です。画面を更新してください。"
"alreadyIssueLicense": "(de)すでに発行済みの注文です。画面を更新してください。",
"alreadyLicenseIssueOrCancel": "(de)ライセンス注文のキャンセルに失敗しました。選択された注文はすでに発行またはキャンセルされています。画面を更新して再度ご確認ください。",
"alreadyLicenseStatusChanged": "(de)ライセンス発行のキャンセルに失敗しました。選択された注文の状態が変更されています。画面を更新して再度ご確認ください。",
"expiredSinceIssued": "(de)発行日から一定期間経過しているため、発行キャンセルできません。",
"alreadyLicenseAllocated": "(de)発行したライセンスがすでに割り当てられたため、発行キャンセルできません。"
}
},
"allocateLicensePopupPage": {
@ -364,7 +370,19 @@
"return": "(de)Return",
"addGroup": "(de)Add Group",
"groupName": "(de)Group Name",
"edit": "(de)Edit"
"edit": "(de)Edit",
"addTypistGroup": "(de)Add Transcriptionist Group",
"transcriptionist": "(de)Transcriptionist",
"selected": "(de)Selected",
"pool": "(de)Pool",
"add": "(de)Add",
"remove": "(de)Remove",
"editTypistGroup": "(de)Edit Transcriptionist Group",
"save": "(de)Save"
},
"message": {
"selectedTypistEmptyError": "(de)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(de)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -1,13 +1,14 @@
{
"common": {
"message": {
"inputEmptyError": "Error Message",
"inputEmptyError": "この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "Error Message",
"emailIncorrectError": "Error Message",
"internalServerError": "処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"listEmpty": "検索結果が0件です。",
"dialogConfirm": "操作を実行しますか?",
"success": "処理に成功しました。"
"success": "処理に成功しました。",
"permissionDeniedError": "操作を実行する権限がありません。"
},
"label": {
"cancel": "Cancel",
@ -113,7 +114,8 @@
"authorIdConflictError": "このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"roleChangeError": "Roleの変更に失敗しました。画面を更新して再度ユーザー情報を取得してください。",
"encryptionPasswordCorrectError": "EncryptionPasswordがルールを満たしていません。"
"encryptionPasswordCorrectError": "EncryptionPasswordがルールを満たしていません。",
"alreadyLicenseDeallocatedError": "すでにライセンス割り当てが解除されています。画面を更新して再度ご確認下さい。"
},
"label": {
"title": "User",
@ -235,7 +237,7 @@
"deleteDictation": "Delete Dictation",
"selectedTranscriptionist": "Selected",
"poolTranscriptionist": "Pool",
"saveChanges": "Save changes"
"saveChanges": "Save"
}
},
"cardLicenseIssuePopupPage": {
@ -328,7 +330,11 @@
},
"message": {
"notEnoughOfNumberOfLicense": "ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
"alreadyIssueLicense": "すでに発行済みの注文です。画面を更新してください。"
"alreadyIssueLicense": "すでに発行済みの注文です。画面を更新してください。",
"alreadyLicenseIssueOrCancel": "ライセンス注文のキャンセルに失敗しました。選択された注文はすでに発行またはキャンセルされています。画面を更新して再度ご確認ください。",
"alreadyLicenseStatusChanged": "ライセンス発行のキャンセルに失敗しました。選択された注文の状態が変更されています。画面を更新して再度ご確認ください。",
"expiredSinceIssued": "発行日から一定期間経過しているため、発行キャンセルできません。",
"alreadyLicenseAllocated": "発行したライセンスがすでに割り当てられたため、発行キャンセルできません。"
}
},
"allocateLicensePopupPage": {
@ -364,7 +370,19 @@
"return": "Return",
"addGroup": "Add Group",
"groupName": "Group Name",
"edit": "Edit"
"edit": "Edit",
"addTypistGroup": "Add Transcriptionist Group",
"transcriptionist": "Transcriptionist",
"selected": "Selected",
"pool": "Pool",
"add": "Add",
"remove": "Remove",
"editTypistGroup": "Edit Transcriptionist Group",
"save": "Save"
},
"message": {
"selectedTypistEmptyError": "TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -1,13 +1,14 @@
{
"common": {
"message": {
"inputEmptyError": "(es)Error Message",
"inputEmptyError": "(es)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(es)Error Message",
"emailIncorrectError": "(es)Error Message",
"internalServerError": "(es)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"listEmpty": "(es)検索結果が0件です。",
"dialogConfirm": "(es)操作を実行しますか?",
"success": "(es)処理に成功しました。"
"success": "(es)処理に成功しました。",
"permissionDeniedError": "(es)操作を実行する権限がありません。"
},
"label": {
"cancel": "(es)Cancel",
@ -113,7 +114,8 @@
"authorIdConflictError": "(es)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(es)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"roleChangeError": "(es)Roleの変更に失敗しました。画面を更新して再度ユーザー情報を取得してください。",
"encryptionPasswordCorrectError": "(es)EncryptionPasswordがルールを満たしていません。"
"encryptionPasswordCorrectError": "(es)EncryptionPasswordがルールを満たしていません。",
"alreadyLicenseDeallocatedError": "(es)すでにライセンス割り当てが解除されています。画面を更新して再度ご確認下さい。"
},
"label": {
"title": "(es)User",
@ -235,7 +237,7 @@
"deleteDictation": "(es)Delete Dictation",
"selectedTranscriptionist": "(es)Selected",
"poolTranscriptionist": "(es)Pool",
"saveChanges": "(es)Save changes"
"saveChanges": "(es)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -328,7 +330,11 @@
},
"message": {
"notEnoughOfNumberOfLicense": "(es)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
"alreadyIssueLicense": "(es)すでに発行済みの注文です。画面を更新してください。"
"alreadyIssueLicense": "(es)すでに発行済みの注文です。画面を更新してください。",
"alreadyLicenseIssueOrCancel": "(es)ライセンス注文のキャンセルに失敗しました。選択された注文はすでに発行またはキャンセルされています。画面を更新して再度ご確認ください。",
"alreadyLicenseStatusChanged": "(es)ライセンス発行のキャンセルに失敗しました。選択された注文の状態が変更されています。画面を更新して再度ご確認ください。",
"expiredSinceIssued": "(es)発行日から一定期間経過しているため、発行キャンセルできません。",
"alreadyLicenseAllocated": "(es)発行したライセンスがすでに割り当てられたため、発行キャンセルできません。"
}
},
"allocateLicensePopupPage": {
@ -364,7 +370,19 @@
"return": "(es)Return",
"addGroup": "(es)Add Group",
"groupName": "(es)Group Name",
"edit": "(es)Edit"
"edit": "(es)Edit",
"addTypistGroup": "(es)Add Transcriptionist Group",
"transcriptionist": "(es)Transcriptionist",
"selected": "(es)Selected",
"pool": "(es)Pool",
"add": "(es)Add",
"remove": "(es)Remove",
"editTypistGroup": "(es)Edit Transcriptionist Group",
"save": "(es)Save"
},
"message": {
"selectedTypistEmptyError": "(es)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(es)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -1,13 +1,14 @@
{
"common": {
"message": {
"inputEmptyError": "(fr)Error Message",
"inputEmptyError": "(fr)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(fr)Error Message",
"emailIncorrectError": "(fr)Error Message",
"internalServerError": "(fr)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"listEmpty": "(fr)検索結果が0件です。",
"dialogConfirm": "(fr)操作を実行しますか?",
"success": "(fr)処理に成功しました。"
"success": "(fr)処理に成功しました。",
"permissionDeniedError": "(fr)操作を実行する権限がありません。"
},
"label": {
"cancel": "(fr)Cancel",
@ -113,7 +114,8 @@
"authorIdConflictError": "(fr)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(fr)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"roleChangeError": "(fr)Roleの変更に失敗しました。画面を更新して再度ユーザー情報を取得してください。",
"encryptionPasswordCorrectError": "(fr)EncryptionPasswordがルールを満たしていません。"
"encryptionPasswordCorrectError": "(fr)EncryptionPasswordがルールを満たしていません。",
"alreadyLicenseDeallocatedError": "(fr)すでにライセンス割り当てが解除されています。画面を更新して再度ご確認下さい。"
},
"label": {
"title": "(fr)User",
@ -235,7 +237,7 @@
"deleteDictation": "(fr)Delete Dictation",
"selectedTranscriptionist": "(fr)Selected",
"poolTranscriptionist": "(fr)Pool",
"saveChanges": "(fr)Save changes"
"saveChanges": "(fr)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -328,7 +330,11 @@
},
"message": {
"notEnoughOfNumberOfLicense": "(fr)ライセンスが不足しているため、発行することができませんでした。ライセンスの注文を行ってください。",
"alreadyIssueLicense": "(fr)すでに発行済みの注文です。画面を更新してください。"
"alreadyIssueLicense": "(fr)すでに発行済みの注文です。画面を更新してください。",
"alreadyLicenseIssueOrCancel": "(fr)ライセンス注文のキャンセルに失敗しました。選択された注文はすでに発行またはキャンセルされています。画面を更新して再度ご確認ください。",
"alreadyLicenseStatusChanged": "(fr)ライセンス発行のキャンセルに失敗しました。選択された注文の状態が変更されています。画面を更新して再度ご確認ください。",
"expiredSinceIssued": "(fr)発行日から一定期間経過しているため、発行キャンセルできません。",
"alreadyLicenseAllocated": "(fr)発行したライセンスがすでに割り当てられたため、発行キャンセルできません。"
}
},
"allocateLicensePopupPage": {
@ -364,7 +370,19 @@
"return": "(fr)Return",
"addGroup": "(fr)Add Group",
"groupName": "(fr)Group Name",
"edit": "(fr)Edit"
"edit": "(fr)Edit",
"addTypistGroup": "(fr)Add Transcriptionist Group",
"transcriptionist": "(fr)Transcriptionist",
"selected": "(fr)Selected",
"pool": "(fr)Pool",
"add": "(fr)Add",
"remove": "(fr)Remove",
"editTypistGroup": "(fr)Edit Transcriptionist Group",
"save": "(fr)Save"
},
"message": {
"selectedTypistEmptyError": "(fr)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(fr)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -0,0 +1,16 @@
-- +migrate Up
DROP TABLE IF EXISTS `licenses_history`;
-- +migrate Down
CREATE TABLE IF NOT EXISTS `licenses_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',
`allocated` BOOLEAN NOT NULL COMMENT '割り当てたか',
`executed_at` TIMESTAMP NOT NULL COMMENT '実施日時',
`exchange_type` VARCHAR(255) NOT NULL COMMENT 'ライセンス切り替え種別(なし/トライアル→通常/紙→通常)',
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
`created_by` VARCHAR(255) COMMENT '作成者',
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
`updated_by` VARCHAR(255) COMMENT '更新者'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

View File

@ -0,0 +1,16 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS `worktypes` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'worktypeの内部ID',
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'アカウントID',
`custom_worktype_id` VARCHAR(255) NOT NULL COMMENT 'ユーザーが決めるWorktypeID',
`description` VARCHAR(255) COMMENT 'Worktypeの説明',
`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() on UPDATE now() COMMENT '更新時刻',
UNIQUE custom_worktype_id_index (custom_worktype_id, account_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-- +migrate Down
DROP TABLE `worktypes`;

View File

@ -354,6 +354,120 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/typist-groups/{typistGroupId}": {
"get": {
"operationId": "getTypistGroup",
"summary": "",
"description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します",
"parameters": [
{
"name": "typistGroupId",
"required": true,
"in": "path",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTypistGroupResponse"
}
}
}
},
"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": [] }]
},
"post": {
"operationId": "updateTypistGroup",
"summary": "",
"description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します",
"parameters": [
{
"name": "typistGroupId",
"required": true,
"in": "path",
"schema": { "type": "number" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateTypistGroupRequest"
}
}
}
},
"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": {
"post": {
"operationId": "createPartnerAccount",
@ -582,6 +696,148 @@
"tags": ["accounts"]
}
},
"/accounts/issue/cancel": {
"post": {
"operationId": "cancelIssue",
"summary": "",
"description": "ライセンス発行をキャンセルします",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CancelIssueRequest" }
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CancelIssueResponse" }
}
}
},
"400": {
"description": "対象注文のステータスが発行済以外/発行日から15日以降/ライセンスをユーザに割り当てている",
"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/worktypes": {
"get": {
"operationId": "getWorktypes",
"summary": "",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetWorktypesResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"],
"security": [{ "bearer": [] }]
},
"post": {
"operationId": "createWorktype",
"summary": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWorktypesRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWorktypeResponse"
}
}
}
},
"400": {
"description": "WorktypeIDが重複 / WorktypeIDが空 / WorktypeIDが20件登録済み",
"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": [] }]
}
},
"/users/confirm": {
"post": {
"operationId": "confirmUser",
@ -2020,6 +2276,58 @@
"security": [{ "bearer": [] }]
}
},
"/licenses/orders/cancel": {
"post": {
"operationId": "cancelOrder",
"summary": "",
"description": "ライセンス注文をキャンセルします",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CancelOrderRequest" }
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CancelOrderResponse" }
}
}
},
"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": ["licenses"],
"security": [{ "bearer": [] }]
}
},
"/notification/register": {
"post": {
"operationId": "register",
@ -2231,6 +2539,14 @@
},
"required": ["typistGroups"]
},
"GetTypistGroupResponse": {
"type": "object",
"properties": {
"typistGroupName": { "type": "string" },
"typistIds": { "type": "array", "items": { "type": "integer" } }
},
"required": ["typistGroupName", "typistIds"]
},
"CreateTypistGroupRequest": {
"type": "object",
"properties": {
@ -2242,12 +2558,28 @@
"typistIds": {
"minItems": 1,
"type": "array",
"items": { "type": "string" }
"items": { "type": "integer" }
}
},
"required": ["typistGroupName", "typistIds"]
},
"CreateTypistGroupResponse": { "type": "object", "properties": {} },
"UpdateTypistGroupRequest": {
"type": "object",
"properties": {
"typistGroupName": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"typistIds": {
"minItems": 1,
"type": "array",
"items": { "type": "integer" }
}
},
"required": ["typistGroupName", "typistIds"]
},
"CreatePartnerAccountRequest": {
"type": "object",
"properties": {
@ -2391,6 +2723,50 @@
},
"required": ["dealers"]
},
"CancelIssueRequest": {
"type": "object",
"properties": {
"orderedAccountId": {
"type": "number",
"description": "注文元アカウントID"
},
"poNumber": { "type": "string", "description": "POナンバー" }
},
"required": ["orderedAccountId", "poNumber"]
},
"CancelIssueResponse": { "type": "object", "properties": {} },
"Worktype": {
"type": "object",
"properties": {
"id": { "type": "number", "description": "WorktypeのID" },
"worktypeId": { "type": "string", "description": "WorktypeID" },
"description": { "type": "string", "description": "Worktypeの説明" }
},
"required": ["id", "worktypeId"]
},
"GetWorktypesResponse": {
"type": "object",
"properties": {
"worktypes": {
"type": "array",
"items": { "$ref": "#/components/schemas/Worktype" }
}
},
"required": ["worktypes"]
},
"CreateWorktypesRequest": {
"type": "object",
"properties": {
"worktypeId": {
"type": "string",
"minLength": 1,
"description": "WorktypeID"
},
"description": { "type": "string", "description": "Worktypeの説明" }
},
"required": ["worktypeId"]
},
"CreateWorktypeResponse": { "type": "object", "properties": {} },
"ConfirmRequest": {
"type": "object",
"properties": { "token": { "type": "string" } },
@ -2929,6 +3305,12 @@
},
"required": ["allocatableLicenses"]
},
"CancelOrderRequest": {
"type": "object",
"properties": { "poNumber": { "type": "string" } },
"required": ["poNumber"]
},
"CancelOrderResponse": { "type": "object", "properties": {} },
"RegisterRequest": {
"type": "object",
"properties": {

View File

@ -40,6 +40,7 @@ import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_per
import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module';
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module';
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
@Module({
imports: [
@ -94,6 +95,7 @@ import { TemplateFilesRepositoryModule } from './repositories/template_files/tem
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
],
controllers: [
HealthController,

View File

@ -46,4 +46,7 @@ export const ErrorCodes = [
'E010804', // ライセンス不足エラー
'E010805', // ライセンス有効期限切れエラー
'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー
'E010908', // タイピストグループ不在エラー
] as const;

View File

@ -35,4 +35,7 @@ export const errors: Errors = {
E010804: 'License shortage Error',
E010805: 'License is expired Error',
E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error',
E010908: 'Typist Group not exist Error',
};

View File

@ -17,6 +17,7 @@ import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.r
import { AudioFilesRepositoryModule } from '../../repositories/audio_files/audio_files.repository.module';
import { AudioOptionItemsRepositoryModule } from '../../repositories/audio_option_items/audio_option_items.repository.module';
import { CheckoutPermissionsRepositoryModule } from '../../repositories/checkout_permissions/checkout_permissions.repository.module';
import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module';
import { NotificationModule } from '../../features//notification/notification.module';
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
@ -63,6 +64,7 @@ export const makeTestingModule = async (
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
],
providers: [
AuthService,

View File

@ -0,0 +1,363 @@
import { v4 as uuidv4 } from 'uuid';
import { DataSource } from 'typeorm';
import { User } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
tier2Accounts: { account: Account; users: User[] }[];
tier3Accounts: { account: Account; users: User[] }[];
tier4Accounts: { account: Account; users: User[] }[];
tier5Accounts: { account: Account; users: User[] }[];
};
// 上書きされたら困る項目を除外したAccount型
type OverrideAccount = Omit<
Account,
'id' | 'primary_admin_user_id' | 'secondary_admin_user_id' | 'user'
>;
// 上書きされたら困る項目を除外したUser型
type OverrideUser = Omit<
User,
'id' | 'account' | 'license' | 'userGroupMembers'
>;
type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] };
type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] };
/**
* ユーティリティ: 1~4
* @param dataSource
* @returns
*/
export const makeHierarchicalAccounts = async (
datasource: DataSource,
): Promise<InitialTestDBState> => {
const state: InitialTestDBState = {
tier1Accounts: [],
tier2Accounts: [],
tier3Accounts: [],
tier4Accounts: [],
tier5Accounts: [],
};
// 第1階層を作成
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 1,
company_name: 'OMDS',
});
state.tier1Accounts.push({
account: account,
users: [admin],
});
}
// 第2階層を作成
{
const { account: tier1 } = state.tier1Accounts.slice().shift();
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
company_name: 'OMDS_US',
});
state.tier2Accounts.push({
account: account,
users: [admin],
});
}
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
company_name: 'OMDS_EU',
});
state.tier2Accounts.push({
account: account,
users: [admin],
});
}
}
// 第3階層を作成
{
for (const v of state.tier2Accounts) {
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 3,
parent_account_id: v.account.id,
company_name: `Agency_${v.account.id}_01`,
});
state.tier3Accounts.push({
account: account,
users: [admin],
});
}
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 3,
parent_account_id: v.account.id,
company_name: `Agency_${v.account.id}_02`,
});
state.tier3Accounts.push({
account: account,
users: [admin],
});
}
}
// 第4階層を作成
for (const v of state.tier3Accounts) {
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 4,
parent_account_id: v.account.id,
company_name: `Distributor_${v.account.id}_01`,
});
state.tier4Accounts.push({
account: account,
users: [admin],
});
}
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 4,
parent_account_id: v.account.id,
company_name: `Distributor_${v.account.id}_02`,
});
state.tier4Accounts.push({
account: account,
users: [admin],
});
}
}
}
return state;
};
/**
* ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する
* @param dataSource
* @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @param defaultAdminUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト(account_id等の所属関係が破壊される上書きは無視する)
* @returns
*/
export const makeTestAccount = async (
datasource: DataSource,
defaultAccountValue?: AccountDefault,
defaultAdminUserValue?: UserDefault,
): Promise<{ account: Account; admin: User }> => {
let accountId: number;
let userId: number;
{
const d = defaultAccountValue;
const { identifiers } = await datasource.getRepository(Account).insert({
tier: d?.tier ?? 1,
parent_account_id: d?.parent_account_id ?? undefined,
country: d?.country ?? 'US',
delegation_permission: d?.delegation_permission ?? false,
locked: d?.locked ?? false,
company_name: d?.company_name ?? 'test inc.',
verified: d?.verified ?? true,
deleted_at: d?.deleted_at ?? '',
created_by: d?.created_by ?? 'test_runner',
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? 'updater',
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as Account;
accountId = result.id;
}
{
const d = defaultAdminUserValue;
const { identifiers } = await datasource.getRepository(User).insert({
external_id: d?.external_id ?? uuidv4(),
account_id: accountId,
role: d?.role ?? 'admin none',
author_id: d?.author_id ?? undefined,
accepted_terms_version: d?.accepted_terms_version ?? '1.0',
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password ?? 'password',
prompt: d?.prompt ?? true,
deleted_at: d?.deleted_at ?? '',
created_by: d?.created_by ?? 'test_runner',
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? 'updater',
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as User;
userId = result.id;
}
// Accountの管理者を設定する
await datasource.getRepository(Account).update(
{ id: accountId },
{
primary_admin_user_id: userId,
},
);
const account = await datasource.getRepository(Account).findOne({
where: {
id: accountId,
},
});
const admin = await datasource.getRepository(User).findOne({
where: {
id: userId,
},
});
return {
account: account,
admin: admin,
};
};
/**
* ユーティリティ: 指定したプロパティを上書きした管理者ユーザーの存在しないアカウントを作成する
* @param dataSource
* @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @returns
*/
export const makeTestSimpleAccount = async (
datasource: DataSource,
defaultAccountValue?: AccountDefault,
): Promise<Account> => {
const d = defaultAccountValue;
const { identifiers } = await datasource.getRepository(Account).insert({
tier: d?.tier ?? 1,
parent_account_id: d?.parent_account_id ?? undefined,
country: d?.country ?? 'US',
delegation_permission: d?.delegation_permission ?? false,
locked: d?.locked ?? false,
company_name: d?.company_name ?? 'test inc.',
verified: d?.verified ?? true,
deleted_at: d?.deleted_at ?? '',
created_by: d?.created_by ?? 'test_runner',
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? 'updater',
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as Account;
const account = await datasource.getRepository(Account).findOne({
where: {
id: result.id,
},
});
return account;
};
/**
* ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する
* @param dataSource
* @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @returns
*/
export const makeTestUser = async (
datasource: DataSource,
defaultUserValue?: UserDefault,
): Promise<User> => {
const d = defaultUserValue;
const { identifiers } = await datasource.getRepository(User).insert({
account_id: d?.account_id ?? -1,
external_id: d?.external_id ?? uuidv4(),
role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
author_id: d?.author_id,
accepted_terms_version: d?.accepted_terms_version,
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password,
prompt: d?.prompt ?? true,
created_by: d?.created_by ?? 'test_runner',
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? 'updater',
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as User;
return await datasource.getRepository(User).findOne({
where: {
id: result.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();
};

View File

@ -0,0 +1,36 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
ValidationOptions,
registerDecorator,
} from 'class-validator';
@ValidatorConstraint()
export class IsUniqueArray implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate(arr: any[], args: ValidationArguments) {
return arr.length === new Set(arr).size;
}
defaultMessage(args: ValidationArguments) {
return `${args.property} should be an array of unique values`;
}
}
/**
*
* @param [validationOptions]
* @returns
*/
export function IsUnique(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isUnique',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: IsUniqueArray,
});
};
}

View File

@ -1,5 +1,6 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => {
registerDecorator({

View File

@ -4,6 +4,7 @@ import {
ValidationArguments,
} from 'class-validator';
import { Assignee } from '../../features/tasks/types/types';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
/**
* Validations options
* @param [validationOptions]

View File

@ -4,7 +4,7 @@ import {
ValidationOptions,
} from 'class-validator';
import { SignupRequest } from '../../features/users/types/types';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => {
registerDecorator({
@ -35,7 +35,7 @@ export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
});
};
};
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsEncryptionPasswordPresent = (
validationOptions?: ValidationOptions,
) => {

View File

@ -9,6 +9,7 @@ import {
} from '../../features/users/types/types';
import { USER_ROLES } from '../../constants';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsRoleAuthorDataValid = <
T extends SignupRequest | PostUpdateUserRequest,
>(

View File

@ -89,16 +89,14 @@ export const USER_ROLES = {
} as const;
/**
*
* @const {string}
*
* @const {string[]}
*/
export const LICENSE_STATUS_ISSUE_REQUESTING = 'Issue Requesting';
/**
*
* @const {string}
*/
export const LICENSE_STATUS_ISSUED = 'Issued';
export const LICENSE_ISSUE_STATUS = {
ISSUE_REQUESTING: 'Issue Requesting',
ISSUED: 'Issued',
CANCELED: 'Order Canceled',
};
/**
*

View File

@ -6,6 +6,7 @@ import {
Get,
Req,
UseGuards,
Param,
} from '@nestjs/common';
import {
ApiOperation,
@ -35,6 +36,15 @@ import {
GetDealersResponse,
CreateTypistGroupResponse,
CreateTypistGroupRequest,
GetTypistGroupResponse,
GetTypistGroupRequest,
UpdateTypistGroupRequest,
UpdateTypistGroupRequestParam,
CancelIssueRequest,
CancelIssueResponse,
GetWorktypesResponse,
CreateWorktypeResponse,
CreateWorktypesRequest,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -127,9 +137,6 @@ export class AccountsController {
@Req() req: Request,
@Body() body: GetLicenseSummaryRequest,
): Promise<GetLicenseSummaryResponse> {
console.log(req.header('Authorization'));
console.log(body);
const response = await this.accountService.getLicenseSummary(
body.accountId,
);
@ -165,8 +172,6 @@ export class AccountsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('me')
async getMyAccount(@Req() req: Request): Promise<GetMyAccountResponse> {
console.log(req.header('Authorization'));
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -199,8 +204,6 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typists')
async getTypists(@Req() req: Request): Promise<GetTypistsResponse> {
console.log(req.header('Authorization'));
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -233,8 +236,6 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typist-groups')
async getTypistGroups(@Req() req: Request): Promise<GetTypistGroupsResponse> {
console.log(req.header('Authorization'));
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -245,6 +246,56 @@ export class AccountsController {
return { typistGroups };
}
@ApiResponse({
status: HttpStatus.OK,
type: GetTypistGroupResponse,
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: 'getTypistGroup',
description:
'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('typist-groups/:typistGroupId')
async getTypistGroup(
@Req() req: Request,
@Param() param: GetTypistGroupRequest,
): Promise<GetTypistGroupResponse> {
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(userId);
const typistGroup = await this.accountService.getTypistGroup(
context,
userId,
typistGroupId,
);
return typistGroup;
}
@ApiResponse({
status: HttpStatus.OK,
type: CreateTypistGroupResponse,
@ -278,9 +329,70 @@ export class AccountsController {
@Req() req: Request,
@Body() body: CreateTypistGroupRequest,
): Promise<CreateTypistGroupResponse> {
const { typistGroupName, typistIds } = body;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(userId);
await this.accountService.createTypistGroup(
context,
userId,
typistGroupName,
typistIds,
);
return {};
}
@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: 'updateTypistGroup',
description:
'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post('typist-groups/:typistGroupId')
async updateTypistGroup(
@Req() req: Request,
@Body() body: UpdateTypistGroupRequest,
@Param() param: UpdateTypistGroupRequestParam,
): Promise<CreateTypistGroupResponse> {
const { typistGroupName, typistIds } = body;
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(userId);
await this.accountService.updateTypistGroup(
context,
userId,
typistGroupId,
typistGroupName,
typistIds,
);
return {};
}
@ -446,8 +558,6 @@ export class AccountsController {
@Req() req: Request,
@Body() body: IssueLicenseRequest,
): Promise<IssueLicenseResponse> {
console.log(req.header('Authorization'));
console.log(body);
const { orderedAccountId, poNumber } = body;
const token = retrieveAuthorizationToken(req);
@ -479,4 +589,127 @@ export class AccountsController {
async getDealers(): Promise<GetDealersResponse> {
return await this.accountService.getDealers();
}
@Post('/issue/cancel')
@ApiResponse({
status: HttpStatus.OK,
type: CancelIssueResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description:
'対象注文のステータスが発行済以外/発行日から15日以降/ライセンスをユーザに割り当てている',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'cancelIssue',
description: 'ライセンス発行をキャンセルします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN],
tiers: [TIERS.TIER1, TIERS.TIER2],
}),
)
async cancelIssue(
@Req() req: Request,
@Body() body: CancelIssueRequest,
): Promise<CancelIssueResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
// TODO: 発行キャンセル処理。API実装のタスク2498で本実装
// await this.accountService.cancelIssue(
// context,
// body.poNumber,
// body.orderedAccountId,
// );
return {};
}
@Get('/worktypes')
@ApiResponse({
status: HttpStatus.OK,
type: GetWorktypesResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'getWorktypes' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async getWorktypes(@Req() req: Request): Promise<GetWorktypesResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
const worktypes = await this.accountService.getWorktypes(context, userId);
return worktypes;
}
@Post('/worktypes')
@ApiResponse({
status: HttpStatus.OK,
type: CreateWorktypeResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'WorktypeIDが重複 / WorktypeIDが空 / WorktypeIDが20件登録済み',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'createWorktype' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async createWorktype(
@Req() req: Request,
@Body() body: CreateWorktypesRequest,
): Promise<CreateWorktypeResponse> {
const { worktypeId, description } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
console.log(worktypeId);
console.log(description);
return {};
}
}

View File

@ -8,6 +8,7 @@ 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';
import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module';
@Module({
imports: [
@ -15,6 +16,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module
UsersRepositoryModule,
LicensesRepositoryModule,
UserGroupsRepositoryModule,
WorktypesRepositoryModule,
SendGridModule,
AdB2cModule,
BlobstorageModule,

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,8 @@ import {
GetDealersResponse,
Dealer,
GetMyAccountResponse,
GetTypistGroupResponse,
GetWorktypesResponse,
} from './types/types';
import {
DateWithZeroTime,
@ -40,6 +42,11 @@ import {
OrderNotFoundError,
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import {
TypistGroupNotExistError,
TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
@Injectable()
export class AccountsService {
@ -48,6 +55,7 @@ export class AccountsService {
private readonly licensesRepository: LicensesRepositoryService,
private readonly usersRepository: UsersRepositoryService,
private readonly userGroupsRepository: UserGroupsRepositoryService,
private readonly worktypesRepository: WorktypesRepositoryService,
private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService,
private readonly blobStorageService: BlobstorageService,
@ -106,8 +114,8 @@ export class AccountsService {
};
return licenseSummaryResponse;
} catch (e) {
console.log(e);
console.log('get licenseSummary failed');
this.logger.error(`error=${e}`);
this.logger.error('get licenseSummary failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -415,6 +423,61 @@ export class AccountsService {
this.logger.log(`[OUT] ${this.getTypistGroups.name}`);
}
}
/**
* IDを指定してタイピストグループを取得する
* @param context
* @param externalId
* @param typistGroupId
* @returns typist group
*/
async getTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
): Promise<GetTypistGroupResponse> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getTypistGroup.name} | params: { externalId: ${externalId}, typistGroupId: ${typistGroupId} };`,
);
try {
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
const userGroup = await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
return {
typistGroupName: userGroup.name,
typistIds: userGroup.userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getTypistGroup.name}`,
);
}
}
/**
* Gets typists
@ -870,4 +933,162 @@ export class AccountsService {
this.logger.log(`[OUT] ${this.getDealers.name}`);
}
}
/**
*
* @param context
* @param externalId
* @param typistGroupName
* @param typistIds
* @returns createTypistGroupResponse
**/
async createTypistGroup(
context: Context,
externalId: string,
typistGroupName: string,
typistIds: number[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createTypistGroup.name} | params: { ` +
`externalId: ${externalId}, ` +
`typistGroupName: ${typistGroupName}, ` +
`typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
// API実行ユーザーのアカウントIDでタイピストグループを作成し、タイピストグループとtypistIdsのユーザーを紐付ける
await this.userGroupsRepository.createTypistGroup(
typistGroupName,
typistIds,
account_id,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistIdInvalidError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
*
* @param context
* @param externalId
* @param typistGroupId
* @param typistGroupName
* @param typistIds
* @returns typist group
*/
async updateTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
typistGroupName: string,
typistIds: number[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateTypistGroup.name} | params: { typistGroupId: ${typistGroupId}, typistGroupName: ${typistGroupName}, typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
// タイピストグループと所属するタイピストを更新する
await this.userGroupsRepository.updateTypistGroup(
account_id,
typistGroupId,
typistGroupName,
typistIds,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// タイピストIDが存在しない場合は400エラーを返す
case TypistIdInvalidError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
// タイピストグループIDが存在しない場合は400エラーを返す
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.updateTypistGroup.name}`,
);
}
}
/**
*
* @param context
* @param externalId
* @returns worktypes
*/
async getWorktypes(
context: Context,
externalId: string,
): Promise<GetWorktypesResponse> {
this.logger.log(`[IN] [${context.trackingId}] ${this.getWorktypes.name}`);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ワークタイプ一覧を取得する
const worktypes = await this.worktypesRepository.getWorktypes(accountId);
return {
worktypes: worktypes.map((x) => ({
id: x.id,
worktypeId: x.custom_worktype_id,
description: x.description ?? undefined,
})),
};
} catch (e) {
this.logger.error(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getWorktypes.name}`,
);
}
}
}

View File

@ -16,6 +16,12 @@ 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';
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
import { WorktypesRepositoryService } from '../../../repositories/worktypes/worktypes.repository.service';
export type WorktypesRepositoryMockValue = {
getWorktypes: Worktype[] | Error;
};
export type LicensesRepositoryMockValue = {
getLicenseOrderHistoryInfo:
@ -79,6 +85,7 @@ export const makeAccountsServiceMock = async (
sendGridMockValue: SendGridMockValue,
blobStorageMockValue: BlobStorageServiceMockValue,
licensesRepositoryMockValue: LicensesRepositoryMockValue,
worktypesRepositoryMockValue: WorktypesRepositoryMockValue,
): Promise<AccountsService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [AccountsService],
@ -107,6 +114,8 @@ export const makeAccountsServiceMock = async (
return makeBlobStorageServiceMock(blobStorageMockValue);
case LicensesRepositoryService:
return makeLicensesRepositoryMock(licensesRepositoryMockValue);
case WorktypesRepositoryService:
return makeWorktypesRepositoryMock(worktypesRepositoryMockValue);
}
})
.compile();
@ -114,6 +123,19 @@ export const makeAccountsServiceMock = async (
return module.get<AccountsService>(AccountsService);
};
export const makeWorktypesRepositoryMock = (
value: WorktypesRepositoryMockValue,
) => {
const { getWorktypes } = value;
return {
getWorktypes:
getWorktypes instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(getWorktypes)
: jest.fn<Promise<Worktype[]>, []>().mockResolvedValue(getWorktypes),
};
};
export const makeAccountsRepositoryMock = (
value: AccountsRepositoryMockValue,
) => {
@ -295,6 +317,13 @@ export const makeBlobStorageServiceMock = (
};
// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
export const makeDefaultWorktypesRepositoryMockValue =
(): WorktypesRepositoryMockValue => {
return {
getWorktypes: [],
};
};
export const makeDefaultAccountsRepositoryMockValue =
(): AccountsRepositoryMockValue => {
let licenseSummaryInfo = new LicenseSummaryInfo();

View File

@ -1,167 +1,12 @@
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,
parentAccountId: number,
tier: number,
companyName: string,
): Promise<{ accountId: number }> => {
const { identifiers } = await datasource.getRepository(Account).insert({
parent_account_id: parentAccountId,
tier: tier,
country: 'JP',
delegation_permission: false,
locked: false,
company_name: companyName,
verified: true,
deleted_at: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const account = identifiers.pop() as Account;
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();
};
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
/**
* ユーティリティ: すべてのソート条件を取得する
@ -242,30 +87,58 @@ export const createLicenseOrder = async (
identifiers.pop() as License;
};
export const createUser = async (
// タイピストグループを取得する
export const getTypistGroup = 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({
): Promise<UserGroup[]> => {
return await datasource.getRepository(UserGroup).find({
where: {
account_id: accountId,
},
});
};
// タイピストグループメンバーを取得する
export const getTypistGroupMember = async (
datasource: DataSource,
userGroupId: number,
): Promise<UserGroupMember[]> => {
return await datasource.getRepository(UserGroupMember).find({
where: {
user_group_id: userGroupId,
},
});
};
// Worktypeを作成する
export const createWorktype = async (
datasource: DataSource,
accountId: number,
worktypeId: string,
description?: string,
): Promise<Worktype> => {
const { identifiers } = await datasource.getRepository(Worktype).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,
custom_worktype_id: worktypeId,
description: description ?? null,
deleted_at: null,
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 };
const worktype = identifiers.pop() as Worktype;
return worktype;
};
// Worktypeを取得する
export const getWorktypes = async (
datasource: DataSource,
accountId: number,
): Promise<Worktype[]> => {
return await datasource.getRepository(Worktype).find({
where: {
account_id: accountId,
},
});
};

View File

@ -8,8 +8,11 @@ import {
Min,
ArrayMinSize,
MinLength,
IsArray,
} from 'class-validator';
import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator';
import { IsUnique } from '../../../common/validators/IsUnique.validator';
import { Type } from 'class-transformer';
export class CreateAccountRequest {
@ApiProperty()
@ -131,18 +134,58 @@ export class GetTypistGroupsResponse {
typistGroups: TypistGroup[];
}
export class GetTypistGroupRequest {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(0)
typistGroupId: number;
}
export class GetTypistGroupResponse {
@ApiProperty()
typistGroupName: string;
@ApiProperty({ isArray: true, type: 'integer' })
typistIds: number[];
}
export class CreateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1 })
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(0, { each: true })
@IsUnique()
typistIds: number[];
}
export class CreateTypistGroupResponse {}
export class UpdateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(0, { each: true })
@IsUnique()
typistIds: number[];
}
export class UpdateTypistGroupRequestParam {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(0)
typistGroupId: number;
}
export class UpdateTypistGroupResponse {}
export class CreatePartnerAccountRequest {
@ApiProperty()
companyName: string;
@ -278,3 +321,38 @@ export class GetDealersResponse {
@ApiProperty({ type: [Dealer] })
dealers: Dealer[];
}
export class CancelIssueRequest {
@ApiProperty({ description: '注文元アカウントID' })
orderedAccountId: number;
@ApiProperty({ description: 'POナンバー' })
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class CancelIssueResponse {}
export class Worktype {
@ApiProperty({ description: 'WorktypeのID' })
id: number;
@ApiProperty({ description: 'WorktypeID' })
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
description?: string;
}
export class GetWorktypesResponse {
@ApiProperty({ type: [Worktype] })
worktypes: Worktype[];
}
export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1)
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
description?: string;
}
export class CreateWorktypeResponse {}

View File

@ -51,7 +51,6 @@ export class AuthController {
operationId: 'token',
})
async token(@Body() body: TokenRequest): Promise<TokenResponse> {
console.log(body);
const idToken = await this.authService.getVerifiedIdToken(body.idToken);
const isVerified = await this.authService.isVerifiedUser(idToken);

View File

@ -7,14 +7,10 @@ import {
makeFilesServiceMock,
} from './test/files.service.mock';
import { DataSource } from 'typeorm';
import {
createAccount,
createTask,
createUser,
makeTestingModuleWithBlob,
} from './test/utility';
import { createTask, makeTestingModuleWithBlob } from './test/utility';
import { FilesService } from './files.service';
import { makeContext } from '../../common/log';
import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility';
describe('音声ファイルアップロードURL取得', () => {
it('アップロードSASトークンが乗っているURLを返却する', async () => {
@ -306,14 +302,17 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
const { accountId } = await createAccount(source);
const { externalId, userId, authorId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
const { audioFileId } = await createTask(
@ -343,20 +342,18 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
const { accountId } = await createAccount(source);
const { externalId, userId } = await createUser(
source,
accountId,
'typist-user-external-id',
'typist',
);
const { userId: authorUserId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
});
const { id: authorUserId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${authorUserId}`;
@ -388,26 +385,23 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'typist-user-external-id',
'typist',
);
const { userId: otherId } = await createUser(
source,
accountId,
'other-typist-user-external-id',
'typist',
);
const { userId: authorUserId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
});
const { id: otherId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'other-typist-user-external-id',
role: 'typist',
});
const { id: authorUserId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${authorUserId}`;
const { audioFileId } = await createTask(
@ -438,14 +432,13 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
const { accountId } = await createAccount(source);
const { externalId, userId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
const { audioFileId } = await createTask(
@ -477,14 +470,13 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const blobParam = makeBlobstorageServiceMockValue();
@ -503,14 +495,17 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
const { accountId } = await createAccount(source);
const { externalId, userId, authorId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
const { audioFileId } = await createTask(
@ -561,13 +556,15 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
const { accountId } = await createAccount(source);
const { externalId, authorId } = await createUser(
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
{
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
},
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
@ -598,14 +595,13 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
const { accountId } = await createAccount(source);
const { externalId, userId } = await createUser(
source,
accountId,
'typist-user-external-id',
'typist',
undefined,
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
author_id: undefined,
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask(
@ -636,21 +632,19 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'typist-user-external-id',
'typist',
undefined,
);
const { userId: otherId } = await createUser(
source,
accountId,
'other-typist-user-external-id',
'typist',
undefined,
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
author_id: undefined,
});
const { id: otherId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'other-typist-user-external-id',
role: 'typist',
author_id: undefined,
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask(
@ -681,14 +675,13 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask(
@ -720,14 +713,13 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const blobParam = makeBlobstorageServiceMockValue();
@ -746,13 +738,15 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
const { accountId } = await createAccount(source);
const { externalId, authorId } = await createUser(
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
accountId,
'author-user-external-id',
'author',
'AUTHOR_ID',
{
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
},
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;

View File

@ -36,58 +36,8 @@ import {
BlobstorageServiceMockValue,
makeBlobstorageServiceMock,
} from './files.service.mock';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity';
export const createAccount = async (
datasource: DataSource,
): Promise<{ accountId: number }> => {
const { identifiers } = await datasource.getRepository(Account).insert({
tier: 5,
country: 'US',
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 = identifiers.pop() as Account;
return { accountId: account.id };
};
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 };
};
export const createTask = async (
datasource: DataSource,
account_id: number,

View File

@ -23,7 +23,8 @@ import {
ActivateCardLicensesResponse,
ActivateCardLicensesRequest,
GetAllocatableLicensesResponse,
GetAllocatableLicensesRequest,
CancelOrderRequest,
CancelOrderResponse,
} from './types/types';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../common/http/helper';
@ -72,10 +73,6 @@ export class LicensesController {
@Req() req: Request,
@Body() body: CreateOrdersRequest,
): Promise<CreateOrdersResponse> {
console.log(req.header('Authorization'));
console.log(body);
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -114,9 +111,6 @@ export class LicensesController {
@Req() req: Request,
@Body() body: IssueCardLicensesRequest,
): Promise<IssueCardLicensesResponse> {
console.log(req.header('Authorization'));
console.log(body);
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -160,9 +154,6 @@ export class LicensesController {
@Req() req: Request,
@Body() body: ActivateCardLicensesRequest,
): Promise<ActivateCardLicensesResponse> {
console.log(req.header('Authorization'));
console.log(body);
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -216,4 +207,54 @@ export class LicensesController {
return allocatableLicenses;
}
@ApiResponse({
status: HttpStatus.OK,
type: CancelOrderResponse,
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: 'cancelOrder',
description: 'ライセンス注文をキャンセルします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN],
tiers: [TIERS.TIER2, TIERS.TIER3, TIERS.TIER4, TIERS.TIER5],
}),
)
@Post('/orders/cancel')
async cancelOrder(
@Req() req: Request,
@Body() body: CancelOrderRequest,
): Promise<CancelOrderResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
await this.licensesService.cancelOrder(
context,
payload.userId,
body.poNumber,
);
return {};
}
}

View File

@ -23,8 +23,6 @@ import { LicensesService } from './licenses.service';
import { makeTestingModule } from '../../common/test/modules';
import { DataSource } from 'typeorm';
import {
createAccount,
createUser,
createCardLicense,
createLicense,
createCardLicenseIssue,
@ -33,11 +31,17 @@ import {
selectCardLicense,
selectLicense,
selectLicenseAllocationHistory,
createOrder,
selectOrderLicense,
} 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';
import {
makeHierarchicalAccounts,
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
describe('LicensesService', () => {
it('ライセンス注文が完了する', async () => {
@ -303,16 +307,16 @@ describe('DBテスト', () => {
it('カードライセンス発行が完了する(発行数が合っているか確認)', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'userId',
'admin',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const service = module.get<LicensesService>(LicensesService);
const issueCount = 1000;
const issueCount = 500;
await service.issueCardLicenseKeys(externalId, issueCount);
const dbSelectResult = await selectCardLicensesCount(source);
expect(dbSelectResult.count).toEqual(issueCount);
@ -321,13 +325,13 @@ describe('DBテスト', () => {
it('カードライセンス取り込みが完了する', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'userId',
'admin',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
const defaultAccountId = 150;
@ -364,13 +368,13 @@ describe('DBテスト', () => {
const module = await makeTestingModule(source);
const now = new Date();
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'userId',
'admin',
);
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
// ライセンスを作成する
// 1件目
@ -499,8 +503,13 @@ describe('ライセンス割り当て', () => {
it('未割当のライセンスに対して、ライセンス割り当てが完了する', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
await createLicense(
source,
1,
@ -544,8 +553,13 @@ describe('ライセンス割り当て', () => {
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -585,8 +599,13 @@ describe('ライセンス割り当て', () => {
it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -661,8 +680,13 @@ describe('ライセンス割り当て', () => {
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がNORMALのとき', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -701,8 +725,13 @@ describe('ライセンス割り当て', () => {
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がCARDのとき', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -741,8 +770,13 @@ describe('ライセンス割り当て', () => {
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がTRIALのとき', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -781,8 +815,13 @@ describe('ライセンス割り当て', () => {
it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() - 30);
await createLicense(
@ -807,8 +846,13 @@ describe('ライセンス割り当て', () => {
it('割り当て不可なライセンスを割り当てようとした場合、エラーになる', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(source, accountId, 'userId', 'admin');
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
@ -844,3 +888,237 @@ describe('ライセンス割り当て', () => {
);
});
});
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 { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
source,
1,
date,
accountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
const service = module.get<UsersService>(UsersService);
await service.deallocateLicense(makeContext('trackingId'), userId);
// 割り当て解除したライセンスの状態確認
const deallocatedLicense = await selectLicense(source, 1);
expect(deallocatedLicense.license.allocated_user_id).toBe(null);
expect(deallocatedLicense.license.status).toBe(
LICENSE_ALLOCATED_STATUS.REUSABLE,
);
expect(deallocatedLicense.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,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
).toBe('NONE');
});
it('ライセンスが既に割り当て解除されていた場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'admin',
author_id: undefined,
});
await makeTestUser(source, {
account_id: accountId,
external_id: 'userId2',
role: 'admin',
author_id: undefined,
});
const date = new Date();
date.setDate(date.getDate() + 30);
await createLicense(
source,
1,
date,
accountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
2,
);
await createLicense(
source,
2,
date,
accountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE,
userId,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
const service = module.get<UsersService>(UsersService);
await expect(
service.deallocateLicense(makeContext('trackingId'), userId),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010807'), HttpStatus.BAD_REQUEST),
);
});
});
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 { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Issue Requesting',
);
// キャンセル済みの同名poNumoberが存在しても正常に動作することの確認用order
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Order Canceled',
);
const service = module.get<LicensesService>(LicensesService);
await service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
);
// 割り当て解除したライセンスの状態確認
const orderRecord = await selectOrderLicense(
source,
tier2Accounts[0].account.id,
poNumber,
);
expect(orderRecord.orderLicense.canceled_at).toBeDefined();
expect(orderRecord.orderLicense.status).toBe('Order Canceled');
});
it('ライセンスが既に発行済みの場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Issued',
);
const service = module.get<LicensesService>(LicensesService);
await expect(
service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010808'), HttpStatus.BAD_REQUEST),
);
});
it('ライセンスが既にキャンセル済みの場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Order Canceled',
);
const service = module.get<LicensesService>(LicensesService);
await expect(
service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010808'), HttpStatus.BAD_REQUEST),
);
});
});

View File

@ -8,6 +8,7 @@ import {
PoNumberAlreadyExistError,
LicenseNotExistError,
LicenseKeyAlreadyActivatedError,
CancelOrderFailedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
@ -255,4 +256,54 @@ export class LicensesService {
);
}
}
/**
*
* @param context
* @param externalId
* @param poNumber
*/
async cancelOrder(
context: Context,
externalId: string,
poNumber: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.cancelOrder.name} | params: { ` +
`externalId: ${externalId}, ` +
`poNumber: ${poNumber}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(externalId)
).account_id;
// 注文キャンセル処理
await this.licensesRepository.cancelOrder(myAccountId, poNumber);
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case CancelOrderFailedError:
throw new HttpException(
makeErrorResponse('E010808'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelOrder.name}`);
}
return;
}
}

View File

@ -1,61 +1,12 @@
import { DataSource } from 'typeorm';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import {
License,
CardLicense,
CardLicenseIssue,
LicenseAllocationHistory,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
export const createAccount = async (
datasource: DataSource,
): Promise<{ accountId: number }> => {
const { identifiers } = 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 = identifiers.pop() as Account;
return { accountId: account.id };
};
export const createUser = async (
datasource: DataSource,
accountId: number,
external_id: string,
role: string,
author_id?: string | undefined,
): Promise<{ userId: number; externalId: 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 };
};
export const createLicense = async (
datasource: DataSource,
licenseId: number,
@ -144,6 +95,31 @@ export const createLicenseAllocationHistory = async (
identifiers.pop() as LicenseAllocationHistory;
};
export const createOrder = async (
datasource: DataSource,
poNumber: string,
fromId: number,
toId: number,
quantity: number,
status: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(LicenseOrder).insert({
po_number: poNumber,
from_account_id: fromId,
to_account_id: toId,
ordered_at: new Date(),
issued_at: null,
quantity: quantity,
status: status,
canceled_at: null,
created_by: null,
created_at: new Date(),
updated_by: null,
updated_at: new Date(),
});
identifiers.pop() as LicenseOrder;
};
export const selectCardLicensesCount = async (
datasource: DataSource,
): Promise<{ count: number }> => {
@ -193,3 +169,17 @@ export const selectLicenseAllocationHistory = async (
});
return { licenseAllocationHistory };
};
export const selectOrderLicense = async (
datasource: DataSource,
accountId: number,
poNumber: string,
): Promise<{ orderLicense: LicenseOrder }> => {
const orderLicense = await datasource.getRepository(LicenseOrder).findOne({
where: {
from_account_id: accountId,
po_number: poNumber,
},
});
return { orderLicense };
};

View File

@ -55,6 +55,14 @@ export class GetAllocatableLicensesResponse {
allocatableLicenses: AllocatableLicenseInfo[];
}
export class CancelOrderRequest {
@ApiProperty()
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class CancelOrderResponse {}
// ライセンス算出用に、その日の始まりの時刻0:00:00.000)の日付を取得する
export class DateWithZeroTime extends Date {
constructor(...args: any[]) {

View File

@ -137,9 +137,6 @@ export class TasksController {
@Headers() headers,
@Query() body: AudioNextRequest,
): Promise<AudioNextResponse> {
const { endedFileId } = body;
console.log(endedFileId);
return { nextFileId: 1234 };
}
@ -403,9 +400,6 @@ export class TasksController {
@Headers() headers,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
console.log(audioFileId);
return {};
}
@ -445,9 +439,6 @@ export class TasksController {
@Headers() headers,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
console.log(audioFileId);
return {};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
import { DataSource } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { Task } from '../../../repositories/tasks/entity/task.entity';
import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity';
import { CheckoutPermission } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity';
@ -102,54 +100,6 @@ export const makeTaskTestingModule = async (
}
};
export const createAccount = async (
datasource: DataSource,
): Promise<{ accountId: number }> => {
const { identifiers } = 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 = identifiers.pop() as Account;
return { accountId: account.id };
};
export const createUser = async (
datasource: DataSource,
accountId: number,
external_id: string,
role: string,
author_id?: string | undefined,
): Promise<{ userId: number; externalId: 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 };
};
export const createTask = async (
datasource: DataSource,
account_id: number,

View File

@ -29,8 +29,6 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi
import { FilesService } from '../../../features/files/files.service';
import { LicensesService } from '../../../features/licenses/licenses.service';
import { TasksService } from '../../../features/tasks/tasks.service';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { License } from '../../../repositories/licenses/entity/license.entity';
@ -38,150 +36,6 @@ 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 }> => {
const { identifiers } = 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 = identifiers.pop() as Account;
return { accountId: account.id };
};
export const createUser = async (
datasource: DataSource,
accountId: number,
external_id: string,
role: string,
author_id?: string | undefined,
auto_renew?: boolean,
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,
external_id: external_id,
role: role,
accepted_terms_version: '1.0',
author_id: author_id,
email_verified: email_verified ?? true,
auto_renew: auto_renew,
license_alert: true,
notification: true,
encryption: encryption ?? false,
encryption_password: encryption_password,
prompt: 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 };
};
/**
* ユーティリティ: 指定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,
): Promise<User> => {
const user = await datasource.getRepository(User).findOne({
where: {
id: id,
},
});
return user;
};
export const getLicenses = async (
datasource: DataSource,
account_id: number,
@ -194,33 +48,6 @@ export const getLicenses = async (
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

View File

@ -461,13 +461,12 @@ export class UsersController {
@Body() body: DeallocateLicenseRequest,
@Req() req: Request,
): Promise<DeallocateLicenseResponse> {
//API実装時に詳細をかいていく
//const accessToken = retrieveAuthorizationToken(req);
//const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
//const context = makeContext(userId);
const context = makeContext(userId);
//await this.usersService.deallocateLicense(context, body.userId);
await this.usersService.deallocateLicense(context, body.userId);
return {};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@ import { DateWithZeroTime } from '../licenses/types/types';
import { Context } from '../../common/log';
import { UserRoles } from '../../common/types/role';
import {
LicenseAlreadyDeallocatedError,
LicenseExpiredError,
LicenseUnavailableError,
} from '../../repositories/licenses/errors/types';
@ -886,7 +887,7 @@ export class UsersService {
this.logger.log(
`[IN] [${context.trackingId}] ${this.allocateLicense.name} | params: { ` +
`userId: ${userId}, ` +
`newLicenseId: ${newLicenseId}, `,
`newLicenseId: ${newLicenseId}, };`,
);
try {
@ -918,4 +919,40 @@ export class UsersService {
);
}
}
/**
*
* @param context
* @param userId
*/
async deallocateLicense(context: Context, userId: number): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.deallocateLicense.name} | params: { ` +
`userId: ${userId}, };`,
);
try {
await this.licensesRepository.deallocateLicense(userId);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case LicenseAlreadyDeallocatedError:
throw new HttpException(
makeErrorResponse('E010807'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.deallocateLicense.name}`,
);
}
}
}

View File

@ -20,7 +20,7 @@ import {
} from '../../common/types/sort/util';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_ISSUE_STATUS,
TIERS,
} from '../../constants';
import {
@ -330,7 +330,7 @@ export class AccountsRepositoryService {
const numberOfRequesting = await licenseOrder.count({
where: {
from_account_id: id,
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
});
@ -340,7 +340,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.from_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issueRequesting = parseInt(result.sum, 10) || 0;
@ -415,7 +415,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.to_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;
@ -426,7 +426,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.from_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;

View File

@ -97,26 +97,6 @@ export class License {
@JoinColumn({ name: 'allocated_user_id' })
user?: User;
}
@Entity({ name: 'licenses_history' })
export class LicenseHistory {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_id: number;
@Column()
license_id: number;
@Column()
allocated: boolean;
@Column()
executed_at: Date;
@Column()
exchange_type: string;
}
@Entity({ name: 'card_license_issue' })
export class CardLicenseIssue {

View File

@ -18,3 +18,9 @@ export class LicensesShortageError extends Error {}
export class LicenseExpiredError extends Error {}
// ライセンス割り当て不可エラー
export class LicenseUnavailableError extends Error {}
// ライセンス割り当て解除済みエラー
export class LicenseAlreadyDeallocatedError extends Error {}
// 注文キャンセル失敗エラー
export class CancelOrderFailedError extends Error {}

View File

@ -10,8 +10,7 @@ import {
import {
CARD_LICENSE_LENGTH,
LICENSE_ALLOCATED_STATUS,
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_STATUS_ISSUED,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
SWITCH_FROM_TYPE,
TIERS,
@ -25,6 +24,8 @@ import {
OrderNotFoundError,
LicenseExpiredError,
LicenseUnavailableError,
LicenseAlreadyDeallocatedError,
CancelOrderFailedError,
} from './errors/types';
import {
AllocatableLicenseInfo,
@ -48,7 +49,7 @@ export class LicensesRepositoryService {
licenseOrder.from_account_id = fromAccountId;
licenseOrder.to_account_id = toAccountId;
licenseOrder.quantity = quantity;
licenseOrder.status = LICENSE_STATUS_ISSUE_REQUESTING;
licenseOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
// ライセンス注文テーブルに登録する
const createdEntity = await this.dataSource.transaction(
@ -61,12 +62,12 @@ export class LicensesRepositoryService {
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUED,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
],
});
@ -102,8 +103,7 @@ export class LicensesRepositoryService {
const cardLicenseIssueRepo =
entityManager.getRepository(CardLicenseIssue);
const licenses = [];
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
const licenses: License[] = [];
// ライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < count; i++) {
const license = new License();
@ -112,7 +112,12 @@ export class LicensesRepositoryService {
license.type = LICENSE_TYPE.CARD;
licenses.push(license);
}
const savedLicenses = await licensesRepo.save(licenses);
const savedLicenses = await licensesRepo
.createQueryBuilder()
.insert()
.into(License)
.values(licenses)
.execute();
// カードライセンス発行テーブルを作成する
const cardLicenseIssue = new CardLicenseIssue();
@ -159,17 +164,21 @@ export class LicensesRepositoryService {
isDuplicateKeysExist = false;
}
const cardLicenses = [];
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
const cardLicenses: CardLicense[] = [];
// カードライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < count; i++) {
const cardLicense = new CardLicense();
cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入
cardLicense.license_id = savedLicenses.generatedMaps[i].id; // Licenseテーブルの自動採番されたIDを挿入
cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
cardLicense.card_license_key = licenseKeys[i];
cardLicenses.push(cardLicense);
}
await cardLicenseRepo.save(cardLicenses);
await cardLicenseRepo
.createQueryBuilder()
.insert()
.into(CardLicense)
.values(cardLicenses)
.execute();
});
return licenseKeys;
}
@ -342,7 +351,7 @@ export class LicensesRepositoryService {
throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
}
// 既に発行済みの注文の場合、エラー
if (issuingOrder.status !== LICENSE_STATUS_ISSUE_REQUESTING) {
if (issuingOrder.status !== LICENSE_ISSUE_STATUS.ISSUE_REQUESTING) {
throw new AlreadyIssuedError(
`An order for PONumber:${poNumber} has already been issued.`,
);
@ -362,11 +371,16 @@ export class LicensesRepositoryService {
{ id: issuingOrder.id },
{
issued_at: nowDate,
status: LICENSE_STATUS_ISSUED,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
);
// ライセンステーブルを登録(注文元)
await licenseRepo.save(newLicenses);
await licenseRepo
.createQueryBuilder()
.insert()
.into(License)
.values(newLicenses)
.execute();
// 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
if (tier !== TIERS.TIER1) {
@ -410,6 +424,7 @@ export class LicensesRepositoryService {
): Promise<AllocatableLicenseInfo[]> {
const nowDate = new DateWithZeroTime();
const licenseRepo = this.dataSource.getRepository(License);
// EntityManagerではorderBy句で、expiry_dateに対して複数条件でソートを使用するため出来ない為、createQueryBuilderを使用する。
const queryBuilder = licenseRepo
.createQueryBuilder('license')
.where('license.account_id = :accountId', { accountId: myAccountId })
@ -541,4 +556,77 @@ export class LicensesRepositoryService {
await licenseAllocationHistoryRepo.save(allocationHistory);
});
}
/**
*
* @param userId
*/
async deallocateLicense(userId: number): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory,
);
// 対象ユーザーのライセンス割り当て状態を取得
const allocatedLicense = await licenseRepo.findOne({
where: {
allocated_user_id: userId,
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
});
// ライセンスが割り当てられていない場合はエラー
if (!allocatedLicense) {
throw new LicenseAlreadyDeallocatedError(
`License is already deallocated. userId: ${userId}`,
);
}
// ライセンスの割り当てを解除
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);
});
}
/**
*
* @param accountId
* @param poNumber
*/
async cancelOrder(accountId: number, poNumber: string): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const orderRepo = entityManager.getRepository(LicenseOrder);
// キャンセル対象の注文を取得
const targetOrder = await orderRepo.findOne({
where: {
from_account_id: accountId,
po_number: poNumber,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
});
// キャンセル対象の注文が存在しない場合エラー
if (!targetOrder) {
throw new CancelOrderFailedError(
`Cancel order is failed. accountId: ${accountId}, poNumber: ${poNumber}`,
);
}
// 注文キャンセル処理
targetOrder.status = LICENSE_ISSUE_STATUS.CANCELED;
targetOrder.canceled_at = new Date();
await orderRepo.save(targetOrder);
});
}
}

View File

@ -1,4 +1,11 @@
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { UserGroupMember } from './user_group_member.entity';
@Entity({ name: 'user_group' })
@ -15,16 +22,16 @@ export class UserGroup {
@Column({ nullable: true })
deleted_at?: Date;
@Column()
created_by: string;
@Column({ nullable: true })
created_by?: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at?: Date;
@Column()
updated_by: string;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at?: Date;
@OneToMany(

View File

@ -5,6 +5,8 @@ import {
PrimaryGeneratedColumn,
JoinColumn,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { UserGroup } from './user_group.entity';
@ -22,16 +24,16 @@ export class UserGroupMember {
@Column({ nullable: true })
deleted_at?: Date;
@Column()
created_by: string;
@Column({ nullable: true })
created_by?: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at?: Date;
@Column()
updated_by: string;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at?: Date;
@ManyToOne(() => User, (user) => user.id)

View File

@ -0,0 +1,4 @@
// タイピストグループが存在しないエラー
export class TypistGroupNotExistError extends Error {}
// typistIdが不正な場合のエラー
export class TypistIdInvalidError extends Error {}

View File

@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import { UserGroup } from './entity/user_group.entity';
import { UserGroupMember } from './entity/user_group_member.entity';
import { User } from '../users/entity/user.entity';
import { TypistGroupNotExistError, TypistIdInvalidError } from './errors/types';
import { USER_ROLES } from '../../constants';
@Injectable()
export class UserGroupsRepositoryService {
@ -43,4 +46,155 @@ export class UserGroupsRepositoryService {
return groupMembers;
});
}
/**
* IDを取得します
* @param accountId
* @param typistGroupId
* @returns typist group
*/
async getTypistGroup(
accountId: number,
typistGroupId: number,
): Promise<UserGroup> {
return await this.dataSource.transaction(async (entityManager) => {
const userGroupRepo = entityManager.getRepository(UserGroup);
const userGroup = await userGroupRepo.findOne({
where: {
id: typistGroupId,
account_id: accountId,
deleted_at: IsNull(),
},
relations: {
userGroupMembers: true,
},
});
if (!userGroup) {
throw new TypistGroupNotExistError(
`Typist Group is not exist. typistGroupId: ${typistGroupId}`,
);
}
return userGroup;
});
}
/**
* IDでタイピストグループを作成しtypistIdsのユーザーを紐付ける
* @param accountId
* @param name
* @param typistIds
* @returns createdTypistGroup
*/
async createTypistGroup(
name: string,
typistIds: number[],
accountId: number,
): Promise<UserGroup> {
return await this.dataSource.transaction(async (entityManager) => {
const userGroupRepo = entityManager.getRepository(UserGroup);
const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
// typistIdsのidを持つユーザーが、account_idのアカウントに所属していて、かつ、roleがtypistであることを確認する
const userRepo = entityManager.getRepository(User);
const userRecords = await userRepo.find({
where: {
id: In(typistIds),
account_id: accountId,
role: USER_ROLES.TYPIST,
},
});
if (userRecords.length !== typistIds.length) {
throw new TypistIdInvalidError(
`Typist user not exists Error. typistIds:${typistIds}; typistIds(DB):${userRecords.map(
(x) => x.id,
)}`,
);
}
// userGroupをDBに保存する
const userGroup = await userGroupRepo.save({
account_id: accountId,
name,
});
const userGroupMembers = userRecords.map((user) => {
return {
user_group_id: userGroup.id,
user_id: user.id,
};
});
// userGroupMembersをDBに保存する
await userGroupMemberRepo.save(userGroupMembers);
return userGroup;
});
}
/**
* IDのタイピストグループを更新しtypistIdsのユーザーを紐付ける
* @param accountId
* @param name
* @param typistIds
* @returns createdTypistGroup
*/
async updateTypistGroup(
accountId: number,
typistGroupId: number,
typistGroupName: string,
typistIds: number[],
): Promise<UserGroup> {
return await this.dataSource.transaction(async (entityManager) => {
const userGroupRepo = entityManager.getRepository(UserGroup);
const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
// typistIdsのidを持つユーザーが、account_idのアカウントに所属していて、かつ、roleがtypistであることを確認する
const userRepo = entityManager.getRepository(User);
const userRecords = await userRepo.find({
where: {
id: In(typistIds),
account_id: accountId,
role: USER_ROLES.TYPIST,
},
});
if (userRecords.length !== typistIds.length) {
throw new TypistIdInvalidError(
`Typist user not exists Error. typistIds:${typistIds}; typistIds(DB):${userRecords.map(
(x) => x.id,
)}`,
);
}
// GroupIdが自アカウント内に存在するか確認する
const typistGroup = await userGroupRepo.findOne({
where: {
id: typistGroupId,
account_id: accountId,
},
});
if (!typistGroup) {
throw new TypistGroupNotExistError(
`TypistGroup not exists Error. accountId: ${accountId}; typistGroupId: ${typistGroupId}`,
);
}
// 対象のタイピストグループを更新する
// ユーザーグループ名を更新する
typistGroup.name = typistGroupName;
await userGroupRepo.save(typistGroup);
// user_group_membersテーブルから対象のタイピストグループのユーザーを削除する
await userGroupMemberRepo.delete({
user_group_id: typistGroupId,
});
const typistGroupMembers = userRecords.map((typist) => {
return {
user_group_id: typistGroup.id,
user_id: typist.id,
};
});
await userGroupMemberRepo.save(typistGroupMembers);
return typistGroup;
});
}
}

View File

@ -317,14 +317,7 @@ export class UsersRepositoryService {
.createQueryBuilder()
.insert()
.into(License)
.values(
licenses.map((value) => ({
expiry_date: value.expiry_date,
account_id: value.account_id,
type: value.type,
status: value.status,
})),
)
.values(licenses)
.execute();
});
}

View File

@ -0,0 +1,38 @@
import { Account } from '../../accounts/entity/account.entity';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'worktypes' })
export class Worktype {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
custom_worktype_id: string;
@Column({ nullable: true })
description?: string;
@Column({ nullable: true })
deleted_at?: Date;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Worktype } from './entity/worktype.entity';
import { WorktypesRepositoryService } from './worktypes.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([Worktype])],
providers: [WorktypesRepositoryService],
exports: [WorktypesRepositoryService],
})
export class WorktypesRepositoryModule {}

View File

@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Worktype } from './entity/worktype.entity';
@Injectable()
export class WorktypesRepositoryService {
constructor(private dataSource: DataSource) {}
/**
*
* @param accountId
* @returns worktypes
*/
async getWorktypes(accountId: number): Promise<Worktype[]> {
return await this.dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(Worktype);
const worktypes = await repo.find({ where: { account_id: accountId } });
return worktypes;
});
}
}