Merge branch 'develop' into main

This commit is contained in:
maruyama.t 2023-09-07 11:22:32 +09:00
commit 0e800c6aec
63 changed files with 3392 additions and 245 deletions

1
.env
View File

@ -5,4 +5,5 @@ DB_NAME=omds
DB_ROOT_PASS=omdsdbpass DB_ROOT_PASS=omdsdbpass
DB_USERNAME=omdsdbuser DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass DB_PASSWORD=omdsdbpass
REDIS_PASSWORD=omdsredispass
NO_COLOR=TRUE NO_COLOR=TRUE

View File

@ -1,3 +1,6 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成し、
# 環境変数 AZURE_SERVICE_CONNECTION の値としてServiceConenction名を設定しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger: trigger:
tags: tags:
include: include:
@ -22,36 +25,98 @@ jobs:
exit 1 exit 1
fi fi
displayName: 'タグが付けられたCommitがmainブランチに存在するか確認' displayName: 'タグが付けられたCommitがmainブランチに存在するか確認'
- job: backend_build - job: backend_deploy
dependsOn: initialize displayName: Backend Deploy
condition: succeeded('initialize')
displayName: Dictation App Service Deploy
pool: pool:
vmImage: ubuntu-latest name: odms-deploy-pipeline
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- job: frontend_build - task: AzureRmWebAppDeployment@4
dependsOn: initialize inputs:
condition: succeeded('initialize') ConnectionType: 'AzureRM'
displayName: Dictation Static App Service Deploy azureSubscription: $(AZURE_SERVICE_CONNECTION)
appType: 'webAppContainer'
WebAppName: 'app-odms-dictation-prod'
ResourceGroupName: 'prod-application-rg'
DockerNamespace: 'crodmsregistrymaintenance.azurecr.io'
DockerRepository: '$(Build.Repository.Name)/staging/dictation'
DockerImageTag: '$(Build.SourceVersion)'
- job: frontend_deploy
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
containerName: staging
pool: pool:
vmImage: ubuntu-latest name: odms-deploy-pipeline
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-prod'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-prod
SecretsFilter: '*'
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(containerName) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: migration - job: migration
condition: succeeded('initialize') condition: succeeded('initialize')
displayName: DB migration displayName: DB migration
dependsOn: dependsOn:
- initialize - initialize
- backend_build - backend_deploy
- frontend_build - frontend_deploy
pool: pool:
name: db-migrate-pipelines name: db-migrate-pipelines
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-prod'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-prod
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci

View File

@ -1,3 +1,6 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成し、
# 環境変数 AZURE_SERVICE_CONNECTION の値としてServiceConenction名を設定しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger: trigger:
branches: branches:
include: include:
@ -28,33 +31,203 @@ jobs:
- job: backend_build - job: backend_build
dependsOn: initialize dependsOn: initialize
condition: succeeded('initialize') condition: succeeded('initialize')
displayName: Dictation App Service Deploy displayName: Build And Push Backend Image
pool: pool:
vmImage: ubuntu-latest name: odms-deploy-pipeline
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
script: |
cd dictation_server
npm run test
env:
JWT_PUBLIC_KEY: $(token-public-key)
SENDGRID_API_KEY: $(sendgrid-api-key)
NOTIFICATION_HUB_NAME: $(notification-hub-name)
NOTIFICATION_HUB_CONNECT_STRING: $(notification-hub-connect-string)
STORAGE_ACCOUNT_NAME_US: $(storage-account-name-us)
STORAGE_ACCOUNT_NAME_AU: $(storage-account-name-au)
STORAGE_ACCOUNT_NAME_EU: $(storage-account-name-eu)
STORAGE_ACCOUNT_KEY_US: $(storage-account-key-us)
STORAGE_ACCOUNT_KEY_AU: $(storage-account-key-au)
STORAGE_ACCOUNT_KEY_EU: $(storage-account-key-eu)
STORAGE_ACCOUNT_ENDPOINT_US: $(storage-account-endpoint-us)
STORAGE_ACCOUNT_ENDPOINT_AU: $(storage-account-endpoint-au)
STORAGE_ACCOUNT_ENDPOINT_EU: $(storage-account-endpoint-eu)
ADB2C_TENANT_ID: $(adb2c-tenant-id)
ADB2C_CLIENT_ID: $(adb2c-client-id)
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: $(AZURE_SERVICE_CONNECTION)
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: $(Build.Repository.Name)/staging/dictation:$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: $(AZURE_SERVICE_CONNECTION)
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: $(Build.Repository.Name)/staging/dictation:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Backend Deploy
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureRmWebAppDeployment@4
inputs:
ConnectionType: 'AzureRM'
azureSubscription: $(AZURE_SERVICE_CONNECTION)
appType: 'webAppContainer'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
DockerNamespace: 'crodmsregistrymaintenance.azurecr.io'
DockerRepository: '$(Build.Repository.Name)/staging/dictation'
DockerImageTag: '$(Build.SourceVersion)'
- job: frontend_build - job: frontend_build
dependsOn: initialize dependsOn: initialize
condition: succeeded('initialize') condition: succeeded('initialize')
displayName: Dictation Static App Service Deploy displayName: Build Frontend Files
variables:
storageAccountName: saomdspipeline
containerName: staging
pool: pool:
vmImage: ubuntu-latest name: odms-deploy-pipeline
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(containerName) \
--name $(Build.SourceVersion).zip \
--type block \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: frontend_deploy
dependsOn: frontend_build
condition: succeeded('frontend_build')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
containerName: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: AzureCLI@2
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(containerName) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: migration - job: migration
condition: succeeded('initialize') condition: succeeded('initialize')
displayName: DB migration displayName: DB migration
dependsOn: dependsOn:
- initialize - initialize
- backend_build - backend_deploy
- frontend_build - frontend_deploy
pool: pool:
name: db-migrate-pipelines name: db-migrate-pipelines
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
fetchDepth: 1 fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: $(AZURE_SERVICE_CONNECTION)
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci

View File

@ -18,6 +18,7 @@ import DictationPage from "pages/DictationPage";
import PartnerPage from "pages/PartnerPage"; import PartnerPage from "pages/PartnerPage";
import WorkflowPage from "pages/WorkflowPage"; import WorkflowPage from "pages/WorkflowPage";
import TypistGroupSettingPage from "pages/TypistGroupSettingPage"; import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage";
const AppRouter: React.FC = () => ( const AppRouter: React.FC = () => (
<Routes> <Routes>
@ -66,6 +67,10 @@ const AppRouter: React.FC = () => (
path="/workflow/typist-group" path="/workflow/typist-group"
element={<RouteAuthGuard component={<TypistGroupSettingPage />} />} element={<RouteAuthGuard component={<TypistGroupSettingPage />} />}
/> />
<Route
path="/workflow/worktype-id"
element={<RouteAuthGuard component={<WorktypeIdSettingPage />} />}
/>
<Route <Route
path="/partners" path="/partners"
element={<RouteAuthGuard component={<PartnerPage />} />} element={<RouteAuthGuard component={<PartnerPage />} />}

View File

@ -462,6 +462,25 @@ export interface CreateTypistGroupRequest {
*/ */
'typistIds': Array<number>; 'typistIds': Array<number>;
} }
/**
*
* @export
* @interface CreateWorktypesRequest
*/
export interface CreateWorktypesRequest {
/**
* WorktypeID
* @type {string}
* @memberof CreateWorktypesRequest
*/
'worktypeId': string;
/**
* Worktypeの説明
* @type {string}
* @memberof CreateWorktypesRequest
*/
'description'?: string;
}
/** /**
* *
* @export * @export
@ -873,15 +892,15 @@ export interface GetUsersResponse {
/** /**
* *
* @export * @export
* @interface GetWorkTypesResponse * @interface GetWorktypesResponse
*/ */
export interface GetWorkTypesResponse { export interface GetWorktypesResponse {
/** /**
* *
* @type {Array<WorkType>} * @type {Array<Worktype>}
* @memberof GetWorkTypesResponse * @memberof GetWorktypesResponse
*/ */
'workTypes': Array<WorkType>; 'worktypes': Array<Worktype>;
} }
/** /**
* *
@ -1609,25 +1628,25 @@ export interface User {
/** /**
* *
* @export * @export
* @interface WorkType * @interface Worktype
*/ */
export interface WorkType { export interface Worktype {
/** /**
* WorkTypeのID * WorktypeのID
* @type {number} * @type {number}
* @memberof WorkType * @memberof Worktype
*/ */
'id': number; 'id': number;
/** /**
* WorkTypeID * WorktypeID
* @type {string} * @type {string}
* @memberof WorkType * @memberof Worktype
*/ */
'workTypeId': string; 'worktypeId': string;
/** /**
* WorkTypeの説明 * Worktypeの説明
* @type {string} * @type {string}
* @memberof WorkType * @memberof Worktype
*/ */
'description'?: string; 'description'?: string;
} }
@ -1794,6 +1813,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @summary
* @param {CreateWorktypesRequest} createWorktypesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createWorktype: async (createWorktypesRequest: CreateWorktypesRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createWorktypesRequest' is not null or undefined
assertParamExists('createWorktype', 'createWorktypesRequest', createWorktypesRequest)
const localVarPath = `/accounts/worktypes`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createWorktypesRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @summary * @summary
@ -2256,6 +2315,17 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createTypistGroup(createTypistGroupRequest, options); const localVarAxiosArgs = await localVarAxiosParamCreator.createTypistGroup(createTypistGroupRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @summary
* @param {CreateWorktypesRequest} createWorktypesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createWorktype(createWorktypesRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @summary * @summary
@ -2346,7 +2416,7 @@ export const AccountsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getWorktypes(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetWorkTypesResponse>> { async getWorktypes(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetWorktypesResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getWorktypes(options); const localVarAxiosArgs = await localVarAxiosParamCreator.getWorktypes(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -2423,6 +2493,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: any): AxiosPromise<object> { createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: any): AxiosPromise<object> {
return localVarFp.createTypistGroup(createTypistGroupRequest, options).then((request) => request(axios, basePath)); return localVarFp.createTypistGroup(createTypistGroupRequest, options).then((request) => request(axios, basePath));
}, },
/**
*
* @summary
* @param {CreateWorktypesRequest} createWorktypesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: any): AxiosPromise<object> {
return localVarFp.createWorktype(createWorktypesRequest, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -2505,7 +2585,7 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getWorktypes(options?: any): AxiosPromise<GetWorkTypesResponse> { getWorktypes(options?: any): AxiosPromise<GetWorktypesResponse> {
return localVarFp.getWorktypes(options).then((request) => request(axios, basePath)); return localVarFp.getWorktypes(options).then((request) => request(axios, basePath));
}, },
/** /**
@ -2587,6 +2667,18 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).createTypistGroup(createTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); return AccountsApiFp(this.configuration).createTypistGroup(createTypistGroupRequest, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @summary
* @param {CreateWorktypesRequest} createWorktypesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).createWorktype(createWorktypesRequest, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary * @summary

View File

@ -14,6 +14,7 @@ import dictation from "features/dictation/dictationSlice";
import partner from "features/partner/partnerSlice"; import partner from "features/partner/partnerSlice";
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice"; import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
import typistGroup from "features/workflow/typistGroup/typistGroupSlice"; import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
import worktype from "features/workflow/worktype/worktypeSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -32,6 +33,7 @@ export const store = configureStore({
dictation, dictation,
partner, partner,
typistGroup, typistGroup,
worktype,
}, },
}); });

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M9,40c-0.8,0-1.5-0.3-2.1-0.9C6.3,38.5,6,37.8,6,37V7c0-0.8,0.3-1.5,0.9-2.1S8.2,4,9,4h30
c0.8,0,1.5,0.3,2.1,0.9C41.7,5.5,42,6.2,42,7v15.1c-0.3,0-0.5-0.1-0.7-0.1s-0.5,0-0.7,0H39V7H9v30h6.9c0.2,0.5,0.3,1,0.5,1.5
s0.4,1,0.7,1.5H9z M9,34v3V7V34z M14.5,31.5H16c0.4-1.2,1-2.5,1.8-3.7s1.8-2.3,2.8-3.3h-6.1C14.5,24.5,14.5,31.5,14.5,31.5z
M14.5,19.5h7v-7h-7V19.5z M26.5,19.5h7v-7h-7V19.5z M29.6,41.9v-6.5H23v-3.9h6.5V25h3.9v6.5H40v3.9h-6.5v6.5H29.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 880 B

View File

@ -51,4 +51,6 @@ export const errorCodes = [
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
"E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー
] as const; ] as const;

View File

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

View File

@ -0,0 +1,112 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { openSnackbar } from "features/ui/uiSlice";
import { getTranslationID } from "translation";
import { AccountsApi, GetWorktypesResponse } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const listWorktypesAsync = createAsyncThunk<
GetWorktypesResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/listWorktypesAsync", 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 worktypes = (
await accountsApi.getWorktypes({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
return worktypes;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const addWorktypeAsync = createAsyncThunk<
{
// return empty
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/addWorktypeAsync", 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);
// stateからworktypeIdとdescriptionを取得する
const { worktypeId, description } = state.worktype.apps;
try {
await accountsApi.createWorktype(
{
worktypeId,
description,
},
{
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");
// 既に同じworktypeIdが存在する場合
if (error.code === "E011001") {
errorMessage = getTranslationID(
"worktypeIdSetting.message.alreadyWorktypeIdExistError"
);
}
// worktypeIdが上限に達している場合
if (error.code === "E011002") {
errorMessage = getTranslationID(
"worktypeIdSetting.message.worktypeIDLimitError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,25 @@
import { RootState } from "app/store";
export const selectWorktypes = (state: RootState) =>
state.worktype.domain.worktypes;
export const selectIsLoading = (state: RootState) =>
state.worktype.apps.isLoading;
export const selectWorktypeId = (state: RootState) =>
state.worktype.apps.worktypeId;
export const selectDescription = (state: RootState) =>
state.worktype.apps.description;
// worktypeIdの値をチェックする
export const selectHasErrorWorktypeId = (state: RootState) => {
const { worktypeId } = state.worktype.apps;
// worktypeIdが空文字の場合はエラー
const isEmptyWorktypeId = worktypeId === "";
// worktypeIdに\/ : * ? “< > | .が含まれている場合はエラー
const incorrectPattern = /[\\/:*?"<>|.]|[^ -~]/;
const hasIncorrectPatternWorktypeId = incorrectPattern.test(worktypeId);
return { isEmptyWorktypeId, hasIncorrectPatternWorktypeId };
};

View File

@ -0,0 +1,16 @@
import { Worktype } from "api";
export interface WorktypeState {
apps: Apps;
domain: Domain;
}
export interface Apps {
isLoading: boolean;
worktypeId: string;
description?: string;
}
export interface Domain {
worktypes?: Worktype[];
}

View File

@ -0,0 +1,58 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { WorktypeState } from "./state";
import { addWorktypeAsync, listWorktypesAsync } from "./operations";
const initialState: WorktypeState = {
apps: {
isLoading: false,
worktypeId: "",
},
domain: {},
};
export const worktypeSlice = createSlice({
name: "worktype",
initialState,
reducers: {
cleanupWorktype: (state) => {
state.apps.worktypeId = initialState.apps.worktypeId;
state.apps.description = undefined;
},
changeWorktypeId: (
state,
action: PayloadAction<{ worktypeId: string }>
) => {
const { worktypeId } = action.payload;
state.apps.worktypeId = worktypeId;
},
changeDescription: (
state,
action: PayloadAction<{ description?: string }>
) => {
const { description } = action.payload;
state.apps.description = description;
},
},
extraReducers: (builder) => {
builder.addCase(listWorktypesAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(listWorktypesAsync.fulfilled, (state, action) => {
const { worktypes } = action.payload;
state.domain.worktypes = worktypes;
state.apps.isLoading = false;
});
builder.addCase(listWorktypesAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(addWorktypeAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(addWorktypeAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { changeDescription, changeWorktypeId, cleanupWorktype } =
worktypeSlice.actions;
export default worktypeSlice.reducer;

View File

@ -185,7 +185,7 @@ export const ChangeTranscriptionistPopup: React.FC<
<input <input
type="button" type="button"
name="submit" name="submit"
value={t(getTranslationID("dictationPage.label.saveChanges"))} value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${ className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : "" !isLoading ? styles.isActive : ""
}`} }`}

View File

@ -52,6 +52,8 @@ const DictationPage: React.FC = (): JSX.Element => {
const isAdmin = isAdminUser(); const isAdmin = isAdminUser();
const isAuthor = isAuthorUser(); const isAuthor = isAuthorUser();
const isTypist = isTypistUser(); const isTypist = isTypistUser();
const isNone = !isAuthor && !isTypist;
// popup制御関係 // popup制御関係
const [ const [
isChangeTranscriptionistPopupOpen, isChangeTranscriptionistPopupOpen,
@ -1001,7 +1003,10 @@ const DictationPage: React.FC = (): JSX.Element => {
<ul className={styles.menuInTable}> <ul className={styles.menuInTable}>
<li> <li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a onClick={() => onPlayBack(x.audioFileId)}> <a
className={isNone ? styles.isDisable : ""}
onClick={() => onPlayBack(x.audioFileId)}
>
{t( {t(
getTranslationID( getTranslationID(
"dictationPage.label.playback" "dictationPage.label.playback"

View File

@ -181,7 +181,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
onClick={returnGui} onClick={returnGui}
> >
<img src={undo} alt="" className={styles.menuIcon} /> <img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("orderHistoriesPage.label.return"))} {t(getTranslationID("common.label.return"))}
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -126,11 +126,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
alt="" alt=""
className={styles.menuIcon} className={styles.menuIcon}
/> />
{t( {t(getTranslationID("common.label.return"))}
getTranslationID(
"partnerLicense.label.returnButton"
)
)}
</a> </a>
)} )}
</li> </li>

View File

@ -277,9 +277,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
alt="" alt=""
className={styles.menuIcon} className={styles.menuIcon}
/> />
{t( {t(getTranslationID("common.label.return"))}
getTranslationID("partnerLicense.label.returnButton")
)}
</a> </a>
)} )}
</li> </li>

View File

@ -190,7 +190,7 @@ export const AddTypistGroupPopup: React.FC<AddTypistGroupPopupProps> = (
<input <input
type="button" type="button"
name="submit" name="submit"
value={t(getTranslationID("typistGroupSetting.label.save"))} value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`} className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={addTypistGroup} onClick={addTypistGroup}
/> />

View File

@ -184,7 +184,7 @@ export const EditTypistGroupPopup: React.FC<EditTypistGroupPopupProps> = (
<input <input
type="button" type="button"
name="submit" name="submit"
value={t(getTranslationID("typistGroupSetting.label.save"))} value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`} className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={editTypistGroup} onClick={editTypistGroup}
/> />

View File

@ -83,7 +83,7 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
className={`${styles.menuLink} ${styles.isActive}`} className={`${styles.menuLink} ${styles.isActive}`}
> >
<img src={undo} alt="" className={styles.menuIcon} /> <img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("typistGroupSetting.label.return"))} {t(getTranslationID("common.label.return"))}
</a> </a>
</li> </li>
<li> <li>
@ -127,11 +127,7 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
onEditPopupOpen(group.id); onEditPopupOpen(group.id);
}} }}
> >
{t( {t(getTranslationID("common.label.edit"))}
getTranslationID(
"typistGroupSetting.label.edit"
)
)}
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -0,0 +1,144 @@
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import {
addWorktypeAsync,
changeDescription,
changeWorktypeId,
cleanupWorktype,
listWorktypesAsync,
selectDescription,
selectHasErrorWorktypeId,
selectWorktypeId,
} from "features/workflow/worktype";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import close from "../../assets/images/close.svg";
// popupのpropsの型定義
interface AddWorktypeIdPopupProps {
onClose: () => void;
isOpen: boolean;
}
export const AddWorktypeIdPopup: React.FC<AddWorktypeIdPopupProps> = (
props: AddWorktypeIdPopupProps
): JSX.Element => {
const { onClose, isOpen } = props;
const [t] = useTranslation();
const dispatch: AppDispatch = useDispatch();
const worktypeId = useSelector(selectWorktypeId);
const description = useSelector(selectDescription);
// 追加ボタンを押したかどうか
const [isPushAddButton, setIsPushAddButton] = useState<boolean>(false);
// WorktypeIdのバリデーションチェック
const { hasIncorrectPatternWorktypeId, isEmptyWorktypeId } = useSelector(
selectHasErrorWorktypeId
);
// ×ボタンを押した時の処理
const closePopup = useCallback(() => {
dispatch(cleanupWorktype());
setIsPushAddButton(false);
onClose();
}, [onClose, dispatch]);
// 追加ボタンを押した時の処理
const addWorktypeId = useCallback(async () => {
setIsPushAddButton(true);
if (isEmptyWorktypeId || hasIncorrectPatternWorktypeId) {
return;
}
const { meta } = await dispatch(addWorktypeAsync());
if (meta.requestStatus === "fulfilled") {
dispatch(listWorktypesAsync());
closePopup();
}
}, [closePopup, dispatch, hasIncorrectPatternWorktypeId, isEmptyWorktypeId]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("worktypeIdSetting.label.addWorktypeId"))}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
onClick={closePopup}
/>
</p>
<form action="" name="" method="" className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={255}
value={worktypeId ?? ""}
className={styles.formInput}
onChange={(e) => {
dispatch(changeWorktypeId({ worktypeId: e.target.value }));
}}
/>
{isPushAddButton && isEmptyWorktypeId && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
{isPushAddButton && hasIncorrectPatternWorktypeId && (
<span className={styles.formError}>
{t(
getTranslationID(
"worktypeIdSetting.message.worktypeIdIncorrectError"
)
)}
</span>
)}
<span
style={{ whiteSpace: "pre-line" }}
className={styles.formComment}
>
{t(getTranslationID("worktypeIdSetting.label.worktypeIdTerms"))}
</span>
</dd>
<dt className={styles.overLine}>
{t(
getTranslationID("worktypeIdSetting.label.descriptionOptional")
)}
</dt>
<dd className={styles.last}>
<input
type="text"
size={40}
maxLength={255}
value={description ?? ""}
className={styles.formInput}
onChange={(e) => {
const description =
e.target.value === "" ? undefined : e.target.value;
dispatch(changeDescription({ description }));
}}
/>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="Add Worktype"
value={t(
getTranslationID("worktypeIdSetting.label.addWorktype")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={addWorktypeId}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,194 @@
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import React, { useEffect, useState } from "react";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import undo from "assets/images/undo.svg";
import worktype_add from "assets/images/worktype_add.svg";
import progress_activit from "assets/images/progress_activit.svg";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
listWorktypesAsync,
selectIsLoading,
selectWorktypes,
} from "features/workflow/worktype";
import { AppDispatch } from "app/store";
import { AddWorktypeIdPopup } from "./addWorktypeIdPopup";
const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const worktypes = useSelector(selectWorktypes);
const [selectedRow, setSelectedRow] = useState<number>(NaN);
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
useEffect(() => {
dispatch(listWorktypesAsync());
}, [dispatch]);
return (
<>
<AddWorktypeIdPopup
onClose={() => {
setIsShowAddPopup(false);
}}
isOpen={isShowAddPopup}
/>
<div className={styles.wrap}>
<Header userName="XXXXXXX" />
<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("worktypeIdSetting.label.title"))}
</p>
</div>
<section className={styles.workflow}>
<div>
<ul className={`${styles.menuAction} ${styles.worktype}`}>
<li>
<a
href="/workflow"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("common.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={() => {
setIsShowAddPopup(true);
}}
>
<img
src={worktype_add}
alt=""
className={styles.menuIcon}
/>
{t(
getTranslationID(
"worktypeIdSetting.label.addWorktypeId"
)
)}
</a>
</li>
<li className={styles.selectMenu}>
{`${t(
getTranslationID(
"worktypeIdSetting.label.activeWorktypeId"
)
)}:`}
<select name="Active Worktype" className={styles.formInput}>
{worktypes?.map((worktype) => (
<option key={worktype.id} value={worktype.id}>
{worktype.worktypeId}
</option>
))}
</select>
</li>
</ul>
<table className={`${styles.table} ${styles.worktype}`}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(
getTranslationID("worktypeIdSetting.label.worktypeId")
)}
</th>
<th className={styles.noLine}>
{t(
getTranslationID("worktypeIdSetting.label.description")
)}
</th>
<th>{/** empty th */}</th>
</tr>
{worktypes?.map((worktype) => (
<tr
key={worktype.id}
className={
worktype.id === selectedRow ? styles.isSelected : ""
}
onMouseEnter={() => {
setSelectedRow(worktype.id);
}}
onMouseLeave={() => {
setSelectedRow(NaN);
}}
>
<td>{worktype.worktypeId}</td>
<td>{worktype.description}</td>
<td>
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>
<li>
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
>
{t(getTranslationID("common.label.edit"))}
</a>
</li>
<li>
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
>
{t(
getTranslationID(
"worktypeIdSetting.label.optionItem"
)
)}
</a>
</li>
<li>
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>
</ul>
</td>
</tr>
))}
</table>
{!isLoading && worktypes?.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
width: "1000px",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</div>
</section>
</div>
</main>
<Footer />
</div>
</>
);
};
export default WorktypeIdSettingPage;

View File

@ -15,6 +15,11 @@ const WorkflowPage: React.FC = (): JSX.Element => (
Transcriptionist Group Setting Transcriptionist Group Setting
</a> </a>
</span> </span>
<span>
<a style={{ margin: 20 }} href="/workflow/worktype-id">
Worktype ID Setting
</a>
</span>
</div> </div>
</main> </main>
<Footer /> <Footer />

View File

@ -5,15 +5,19 @@
"passwordIncorrectError": "(de)Error Message", "passwordIncorrectError": "(de)Error Message",
"emailIncorrectError": "(de)Error Message", "emailIncorrectError": "(de)Error Message",
"internalServerError": "(de)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "internalServerError": "(de)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"permissionDeniedError": "(de)操作を実行する権限がありません。",
"listEmpty": "(de)検索結果が0件です。", "listEmpty": "(de)検索結果が0件です。",
"dialogConfirm": "(de)操作を実行しますか?", "dialogConfirm": "(de)操作を実行しますか?",
"success": "(de)処理に成功しました。", "success": "(de)処理に成功しました。"
"permissionDeniedError": "(de)操作を実行する権限がありません。"
}, },
"label": { "label": {
"cancel": "(de)Cancel", "cancel": "(de)Cancel",
"headerTitle": "(de)ODMS Cloud", "headerTitle": "(de)ODMS Cloud",
"copyRight": "(de)OM Digital Solutions 2023", "copyRight": "(de)OM Digital Solutions 2023",
"edit": "(de)Edit",
"save": "(de)Save",
"delete": "(de)Delete",
"return": "(de)Return",
"tier1": "(de)Admin", "tier1": "(de)Admin",
"tier2": "(de)BC", "tier2": "(de)BC",
"tier3": "(de)Distributor", "tier3": "(de)Distributor",
@ -120,8 +124,6 @@
"label": { "label": {
"title": "(de)User", "title": "(de)User",
"addUser": "(de)Add User", "addUser": "(de)Add User",
"edit": "(de)Edit",
"delete": "(de)Delete",
"licenseAllocation": "(de)License Allocation", "licenseAllocation": "(de)License Allocation",
"name": "(de)Name", "name": "(de)Name",
"role": "(de)Role", "role": "(de)Role",
@ -236,8 +238,7 @@
"changeTranscriptionist": "(de)Change Transcriptionist", "changeTranscriptionist": "(de)Change Transcriptionist",
"deleteDictation": "(de)Delete Dictation", "deleteDictation": "(de)Delete Dictation",
"selectedTranscriptionist": "(de)Selected", "selectedTranscriptionist": "(de)Selected",
"poolTranscriptionist": "(de)Pool", "poolTranscriptionist": "(de)Pool"
"saveChanges": "(de)Save"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -294,7 +295,6 @@
"label": { "label": {
"title": "(de)License", "title": "(de)License",
"subTitle": "(de)License for partners", "subTitle": "(de)License for partners",
"returnButton": "(de)Return",
"orderLicenseButton": "(de)Order License", "orderLicenseButton": "(de)Order License",
"orderHistoryButton": "(de)Order History", "orderHistoryButton": "(de)Order History",
"IssueLicenseCardButton": "(de)License Card", "IssueLicenseCardButton": "(de)License Card",
@ -314,7 +314,6 @@
"label": { "label": {
"title": "(de)License", "title": "(de)License",
"orderHistory": "(de)Order History", "orderHistory": "(de)Order History",
"return": "(de)Return",
"orderDate": "(de)Order date", "orderDate": "(de)Order date",
"issueDate": "(de)Issue date", "issueDate": "(de)Issue date",
"numberOfOrder": "(de)Number of Order", "numberOfOrder": "(de)Number of Order",
@ -367,22 +366,52 @@
"typistGroupSetting": { "typistGroupSetting": {
"label": { "label": {
"title": "(de)Transctiprionist Group", "title": "(de)Transctiprionist Group",
"return": "(de)Return",
"addGroup": "(de)Add Group", "addGroup": "(de)Add Group",
"groupName": "(de)Group Name", "groupName": "(de)Group Name",
"edit": "(de)Edit",
"addTypistGroup": "(de)Add Transcriptionist Group", "addTypistGroup": "(de)Add Transcriptionist Group",
"transcriptionist": "(de)Transcriptionist", "transcriptionist": "(de)Transcriptionist",
"selected": "(de)Selected", "selected": "(de)Selected",
"pool": "(de)Pool", "pool": "(de)Pool",
"add": "(de)Add", "add": "(de)Add",
"remove": "(de)Remove", "remove": "(de)Remove",
"editTypistGroup": "(de)Edit Transcriptionist Group", "editTypistGroup": "(de)Edit Transcriptionist Group"
"save": "(de)Save"
}, },
"message": { "message": {
"selectedTypistEmptyError": "(de)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。", "selectedTypistEmptyError": "(de)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(de)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください" "groupSaveFailedError": "(de)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
} }
},
"worktypeIdSetting": {
"label": {
"title": "(de)Worktype ID Setting",
"activeWorktypeId": "(de)Active Worktype ID",
"addWorktypeId": "(de)Add Worktype ID",
"worktypeId": "(de)Worktype ID",
"description": "(de)Description",
"descriptionOptional": "(de)Description (Optional)",
"optionItem": "(de)Option Item",
"worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .",
"addWorktype": "(de)Add Worktype"
},
"message": {
"worktypeIdIncorrectError": "(de)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",
"alreadyWorktypeIdExistError": "(de)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。",
"worktypeIDLimitError": "(de)Worktype IDが登録件数の上限に達しているため追加できません。"
}
},
"partnerPage": {
"label": {
"title": "(de)Partners",
"addAccount": "(de)Add Account",
"name": "(de)Name",
"category": "(de)Category",
"accountId": "(de)Account ID",
"country": "(de)Country",
"primaryAdmin": "(de)Primary Admin",
"email": "(de)E-mail",
"dealerManagement": "(de)Dealer Management",
"partners": "(de)partners",
"deleteAccount": "(de)Delete Account"
}
} }
} }

View File

@ -5,15 +5,19 @@
"passwordIncorrectError": "Error Message", "passwordIncorrectError": "Error Message",
"emailIncorrectError": "Error Message", "emailIncorrectError": "Error Message",
"internalServerError": "処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "internalServerError": "処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"permissionDeniedError": "操作を実行する権限がありません。",
"listEmpty": "検索結果が0件です。", "listEmpty": "検索結果が0件です。",
"dialogConfirm": "操作を実行しますか?", "dialogConfirm": "操作を実行しますか?",
"success": "処理に成功しました。", "success": "処理に成功しました。"
"permissionDeniedError": "操作を実行する権限がありません。"
}, },
"label": { "label": {
"cancel": "Cancel", "cancel": "Cancel",
"headerTitle": "ODMS Cloud", "headerTitle": "ODMS Cloud",
"copyRight": "OM Digital Solutions 2023", "copyRight": "OM Digital Solutions 2023",
"edit": "Edit",
"save": "Save",
"delete": "Delete",
"return": "Return",
"tier1": "Admin", "tier1": "Admin",
"tier2": "BC", "tier2": "BC",
"tier3": "Distributor", "tier3": "Distributor",
@ -120,8 +124,6 @@
"label": { "label": {
"title": "User", "title": "User",
"addUser": "Add User", "addUser": "Add User",
"edit": "Edit",
"delete": "Delete",
"licenseAllocation": "License Allocation", "licenseAllocation": "License Allocation",
"name": "Name", "name": "Name",
"role": "Role", "role": "Role",
@ -236,8 +238,7 @@
"changeTranscriptionist": "Change Transcriptionist", "changeTranscriptionist": "Change Transcriptionist",
"deleteDictation": "Delete Dictation", "deleteDictation": "Delete Dictation",
"selectedTranscriptionist": "Selected", "selectedTranscriptionist": "Selected",
"poolTranscriptionist": "Pool", "poolTranscriptionist": "Pool"
"saveChanges": "Save"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -294,7 +295,6 @@
"label": { "label": {
"title": "License", "title": "License",
"subTitle": "License for partners", "subTitle": "License for partners",
"returnButton": "Return",
"orderLicenseButton": "Order License", "orderLicenseButton": "Order License",
"orderHistoryButton": "Order History", "orderHistoryButton": "Order History",
"IssueLicenseCardButton": "License Card", "IssueLicenseCardButton": "License Card",
@ -314,7 +314,6 @@
"label": { "label": {
"title": "License", "title": "License",
"orderHistory": "Order History", "orderHistory": "Order History",
"return": "Return",
"orderDate": "Order date", "orderDate": "Order date",
"issueDate": "Issue date", "issueDate": "Issue date",
"numberOfOrder": "Number of Order", "numberOfOrder": "Number of Order",
@ -367,22 +366,52 @@
"typistGroupSetting": { "typistGroupSetting": {
"label": { "label": {
"title": "Transctiprionist Group", "title": "Transctiprionist Group",
"return": "Return",
"addGroup": "Add Group", "addGroup": "Add Group",
"groupName": "Group Name", "groupName": "Group Name",
"edit": "Edit",
"addTypistGroup": "Add Transcriptionist Group", "addTypistGroup": "Add Transcriptionist Group",
"transcriptionist": "Transcriptionist", "transcriptionist": "Transcriptionist",
"selected": "Selected", "selected": "Selected",
"pool": "Pool", "pool": "Pool",
"add": "Add", "add": "Add",
"remove": "Remove", "remove": "Remove",
"editTypistGroup": "Edit Transcriptionist Group", "editTypistGroup": "Edit Transcriptionist Group"
"save": "Save"
}, },
"message": { "message": {
"selectedTypistEmptyError": "TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。", "selectedTypistEmptyError": "TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "TypistGroupの保存に失敗しました。画面を更新し、再度実行してください" "groupSaveFailedError": "TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
} }
},
"worktypeIdSetting": {
"label": {
"title": "Worktype ID Setting",
"activeWorktypeId": "Active Worktype ID",
"addWorktypeId": "Add Worktype ID",
"worktypeId": "Worktype ID",
"description": "Description",
"descriptionOptional": "Description (Optional)",
"optionItem": "Option Item",
"worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .",
"addWorktype": "Add Worktype"
},
"message": {
"worktypeIdIncorrectError": "入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",
"alreadyWorktypeIdExistError": "このWorktype IDは既に登録されています。他のWorktype IDで登録してください。",
"worktypeIDLimitError": "Worktype IDが登録件数の上限に達しているため追加できません。"
}
},
"partnerPage": {
"label": {
"title": "Partners",
"addAccount": "Add Account",
"name": "Name",
"category": "Category",
"accountId": "Account ID",
"country": "Country",
"primaryAdmin": "Primary Admin",
"email": "E-mail",
"dealerManagement": "Dealer Management",
"partners": "partners",
"deleteAccount": "Delete Account"
}
} }
} }

View File

@ -5,15 +5,19 @@
"passwordIncorrectError": "(es)Error Message", "passwordIncorrectError": "(es)Error Message",
"emailIncorrectError": "(es)Error Message", "emailIncorrectError": "(es)Error Message",
"internalServerError": "(es)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "internalServerError": "(es)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"permissionDeniedError": "(es)操作を実行する権限がありません。",
"listEmpty": "(es)検索結果が0件です。", "listEmpty": "(es)検索結果が0件です。",
"dialogConfirm": "(es)操作を実行しますか?", "dialogConfirm": "(es)操作を実行しますか?",
"success": "(es)処理に成功しました。", "success": "(es)処理に成功しました。"
"permissionDeniedError": "(es)操作を実行する権限がありません。"
}, },
"label": { "label": {
"cancel": "(es)Cancel", "cancel": "(es)Cancel",
"headerTitle": "(es)ODMS Cloud", "headerTitle": "(es)ODMS Cloud",
"copyRight": "(es)OM Digital Solutions 2023", "copyRight": "(es)OM Digital Solutions 2023",
"edit": "(es)Edit",
"save": "(es)Save",
"delete": "(es)Delete",
"return": "(es)Return",
"tier1": "(es)Admin", "tier1": "(es)Admin",
"tier2": "(es)BC", "tier2": "(es)BC",
"tier3": "(es)Distributor", "tier3": "(es)Distributor",
@ -120,8 +124,6 @@
"label": { "label": {
"title": "(es)User", "title": "(es)User",
"addUser": "(es)Add User", "addUser": "(es)Add User",
"edit": "(es)Edit",
"delete": "(es)Delete",
"licenseAllocation": "(es)License Allocation", "licenseAllocation": "(es)License Allocation",
"name": "(es)Name", "name": "(es)Name",
"role": "(es)Role", "role": "(es)Role",
@ -236,8 +238,7 @@
"changeTranscriptionist": "(es)Change Transcriptionist", "changeTranscriptionist": "(es)Change Transcriptionist",
"deleteDictation": "(es)Delete Dictation", "deleteDictation": "(es)Delete Dictation",
"selectedTranscriptionist": "(es)Selected", "selectedTranscriptionist": "(es)Selected",
"poolTranscriptionist": "(es)Pool", "poolTranscriptionist": "(es)Pool"
"saveChanges": "(es)Save"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -294,7 +295,6 @@
"label": { "label": {
"title": "(es)License", "title": "(es)License",
"subTitle": "(es)License for partners", "subTitle": "(es)License for partners",
"returnButton": "(es)Return",
"orderLicenseButton": "(es)Order License", "orderLicenseButton": "(es)Order License",
"orderHistoryButton": "(es)Order History", "orderHistoryButton": "(es)Order History",
"IssueLicenseCardButton": "(es)License Card", "IssueLicenseCardButton": "(es)License Card",
@ -314,7 +314,6 @@
"label": { "label": {
"title": "(es)License", "title": "(es)License",
"orderHistory": "(es)Order History", "orderHistory": "(es)Order History",
"return": "(es)Return",
"orderDate": "(es)Order date", "orderDate": "(es)Order date",
"issueDate": "(es)Issue date", "issueDate": "(es)Issue date",
"numberOfOrder": "(es)Number of Order", "numberOfOrder": "(es)Number of Order",
@ -367,22 +366,52 @@
"typistGroupSetting": { "typistGroupSetting": {
"label": { "label": {
"title": "(es)Transctiprionist Group", "title": "(es)Transctiprionist Group",
"return": "(es)Return",
"addGroup": "(es)Add Group", "addGroup": "(es)Add Group",
"groupName": "(es)Group Name", "groupName": "(es)Group Name",
"edit": "(es)Edit",
"addTypistGroup": "(es)Add Transcriptionist Group", "addTypistGroup": "(es)Add Transcriptionist Group",
"transcriptionist": "(es)Transcriptionist", "transcriptionist": "(es)Transcriptionist",
"selected": "(es)Selected", "selected": "(es)Selected",
"pool": "(es)Pool", "pool": "(es)Pool",
"add": "(es)Add", "add": "(es)Add",
"remove": "(es)Remove", "remove": "(es)Remove",
"editTypistGroup": "(es)Edit Transcriptionist Group", "editTypistGroup": "(es)Edit Transcriptionist Group"
"save": "(es)Save"
}, },
"message": { "message": {
"selectedTypistEmptyError": "(es)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。", "selectedTypistEmptyError": "(es)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(es)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください" "groupSaveFailedError": "(es)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
} }
},
"worktypeIdSetting": {
"label": {
"title": "(es)Worktype ID Setting",
"activeWorktypeId": "(es)Active Worktype ID",
"addWorktypeId": "(es)Add Worktype ID",
"worktypeId": "(es)Worktype ID",
"description": "(es)Description",
"descriptionOptional": "(es)Description (Optional)",
"optionItem": "(es)Option Item",
"worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .",
"addWorktype": "(es)Add Worktype"
},
"message": {
"worktypeIdIncorrectError": "(es)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",
"alreadyWorktypeIdExistError": "(es)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。",
"worktypeIDLimitError": "(es)Worktype IDが登録件数の上限に達しているため追加できません。"
}
},
"partnerPage": {
"label": {
"title": "(es)Partners",
"addAccount": "(es)Add Account",
"name": "(es)Name",
"category": "(es)Category",
"accountId": "(es)Account ID",
"country": "(es)Country",
"primaryAdmin": "(es)Primary Admin",
"email": "(es)E-mail",
"dealerManagement": "(es)Dealer Management",
"partners": "(es)partners",
"deleteAccount": "(es)Delete Account"
}
} }
} }

View File

@ -5,15 +5,19 @@
"passwordIncorrectError": "(fr)Error Message", "passwordIncorrectError": "(fr)Error Message",
"emailIncorrectError": "(fr)Error Message", "emailIncorrectError": "(fr)Error Message",
"internalServerError": "(fr)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "internalServerError": "(fr)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
"permissionDeniedError": "(fr)操作を実行する権限がありません。",
"listEmpty": "(fr)検索結果が0件です。", "listEmpty": "(fr)検索結果が0件です。",
"dialogConfirm": "(fr)操作を実行しますか?", "dialogConfirm": "(fr)操作を実行しますか?",
"success": "(fr)処理に成功しました。", "success": "(fr)処理に成功しました。"
"permissionDeniedError": "(fr)操作を実行する権限がありません。"
}, },
"label": { "label": {
"cancel": "(fr)Cancel", "cancel": "(fr)Cancel",
"headerTitle": "(fr)ODMS Cloud", "headerTitle": "(fr)ODMS Cloud",
"copyRight": "(fr)OM Digital Solutions 2023", "copyRight": "(fr)OM Digital Solutions 2023",
"edit": "(fr)Edit",
"save": "(fr)Save",
"delete": "(fr)Delete",
"return": "(fr)Return",
"tier1": "(fr)Admin", "tier1": "(fr)Admin",
"tier2": "(fr)BC", "tier2": "(fr)BC",
"tier3": "(fr)Distributor", "tier3": "(fr)Distributor",
@ -120,8 +124,6 @@
"label": { "label": {
"title": "(fr)User", "title": "(fr)User",
"addUser": "(fr)Add User", "addUser": "(fr)Add User",
"edit": "(fr)Edit",
"delete": "(fr)Delete",
"licenseAllocation": "(fr)License Allocation", "licenseAllocation": "(fr)License Allocation",
"name": "(fr)Name", "name": "(fr)Name",
"role": "(fr)Role", "role": "(fr)Role",
@ -236,8 +238,7 @@
"changeTranscriptionist": "(fr)Change Transcriptionist", "changeTranscriptionist": "(fr)Change Transcriptionist",
"deleteDictation": "(fr)Delete Dictation", "deleteDictation": "(fr)Delete Dictation",
"selectedTranscriptionist": "(fr)Selected", "selectedTranscriptionist": "(fr)Selected",
"poolTranscriptionist": "(fr)Pool", "poolTranscriptionist": "(fr)Pool"
"saveChanges": "(fr)Save"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -294,7 +295,6 @@
"label": { "label": {
"title": "(fr)License", "title": "(fr)License",
"subTitle": "(fr)License for partners", "subTitle": "(fr)License for partners",
"returnButton": "(fr)Return",
"orderLicenseButton": "(fr)Order License", "orderLicenseButton": "(fr)Order License",
"orderHistoryButton": "(fr)Order History", "orderHistoryButton": "(fr)Order History",
"IssueLicenseCardButton": "(fr)License Card", "IssueLicenseCardButton": "(fr)License Card",
@ -314,7 +314,6 @@
"label": { "label": {
"title": "(fr)License", "title": "(fr)License",
"orderHistory": "(fr)Order History", "orderHistory": "(fr)Order History",
"return": "(fr)Return",
"orderDate": "(fr)Order date", "orderDate": "(fr)Order date",
"issueDate": "(fr)Issue date", "issueDate": "(fr)Issue date",
"numberOfOrder": "(fr)Number of Order", "numberOfOrder": "(fr)Number of Order",
@ -367,22 +366,52 @@
"typistGroupSetting": { "typistGroupSetting": {
"label": { "label": {
"title": "(fr)Transctiprionist Group", "title": "(fr)Transctiprionist Group",
"return": "(fr)Return",
"addGroup": "(fr)Add Group", "addGroup": "(fr)Add Group",
"groupName": "(fr)Group Name", "groupName": "(fr)Group Name",
"edit": "(fr)Edit",
"addTypistGroup": "(fr)Add Transcriptionist Group", "addTypistGroup": "(fr)Add Transcriptionist Group",
"transcriptionist": "(fr)Transcriptionist", "transcriptionist": "(fr)Transcriptionist",
"selected": "(fr)Selected", "selected": "(fr)Selected",
"pool": "(fr)Pool", "pool": "(fr)Pool",
"add": "(fr)Add", "add": "(fr)Add",
"remove": "(fr)Remove", "remove": "(fr)Remove",
"editTypistGroup": "(fr)Edit Transcriptionist Group", "editTypistGroup": "(fr)Edit Transcriptionist Group"
"save": "(fr)Save"
}, },
"message": { "message": {
"selectedTypistEmptyError": "(fr)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。", "selectedTypistEmptyError": "(fr)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(fr)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください" "groupSaveFailedError": "(fr)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
} }
},
"worktypeIdSetting": {
"label": {
"title": "(fr)Worktype ID Setting",
"activeWorktypeId": "(fr)Active Worktype ID",
"addWorktypeId": "(fr)Add Worktype ID",
"worktypeId": "(fr)Worktype ID",
"description": "(fr)Description",
"descriptionOptional": "(fr)Description (Optional)",
"optionItem": "(fr)Option Item",
"worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .",
"addWorktype": "(fr)Add Worktype"
},
"message": {
"worktypeIdIncorrectError": "(fr)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",
"alreadyWorktypeIdExistError": "(fr)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。",
"worktypeIDLimitError": "(fr)Worktype IDが登録件数の上限に達しているため追加できません。"
}
},
"partnerPage": {
"label": {
"title": "(fr)Partners",
"addAccount": "(fr)Add Account",
"name": "(fr)Name",
"category": "(fr)Category",
"accountId": "(fr)Account ID",
"country": "(fr)Country",
"primaryAdmin": "(fr)Primary Admin",
"email": "(fr)E-mail",
"dealerManagement": "(fr)Dealer Management",
"partners": "(fr)partners",
"deleteAccount": "(fr)Delete Account"
}
} }
} }

View File

@ -24,4 +24,4 @@ STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA

View File

@ -0,0 +1,15 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS `option_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'オプションアイテムID',
`worktype_id` BIGINT UNSIGNED NOT NULL COMMENT 'worktypeの内部ID',
`item_label` VARCHAR(50) NOT NULL COMMENT 'アイテムラベル',
`default_value_type` VARCHAR(16) NOT NULL COMMENT 'デフォルト値種別Default/Blank/LastInput',
`initial_value` VARCHAR(50) NOT NULL 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 '更新時刻'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-- +migrate Down
DROP TABLE `option_items`;

View File

@ -26,6 +26,8 @@
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"axios": "^1.3.4", "axios": "^1.3.4",
"cache-manager": "^5.2.0",
"cache-manager-redis-store": "^2.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
@ -47,6 +49,7 @@
"@nestjs/schematics": "^8.0.0", "@nestjs/schematics": "^8.0.0",
"@nestjs/swagger": "^6.3.0", "@nestjs/swagger": "^6.3.0",
"@nestjs/testing": "^9.3.12", "@nestjs/testing": "^9.3.12",
"@types/cache-manager-redis-store": "^2.0.1",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-session": "^1.17.5", "@types/express-session": "^1.17.5",
@ -2859,6 +2862,22 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cache-manager": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.2.tgz",
"integrity": "sha512-fT5FMdzsiSX0AbgnS5gDvHl2Nco0h5zYyjwDQy4yPC7Ww6DeGMVKPRqIZtg9HOXDV2kkc18SL1B0N8f0BecrCA==",
"dev": true
},
"node_modules/@types/cache-manager-redis-store": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.1.tgz",
"integrity": "sha512-8QuccvcPieh1xM/5kReE76SfdcIdEB0ePc+54ah/NBuK2eG+6O50SX4WKoJX81UxGdW3sh/WlDaDNqjnqxWNsA==",
"dev": true,
"dependencies": {
"@types/cache-manager": "*",
"@types/redis": "^2.8.0"
}
},
"node_modules/@types/connect": { "node_modules/@types/connect": {
"version": "3.4.35", "version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -3062,6 +3081,15 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true "dev": true
}, },
"node_modules/@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.3.13", "version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@ -4235,6 +4263,60 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/cache-manager": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.2.3.tgz",
"integrity": "sha512-9OErI8fksFkxAMJ8Mco0aiZSdphyd90HcKiOMJQncSlU1yq/9lHHxrT8PDayxrmr9IIIZPOAEfXuGSD7g29uog==",
"dependencies": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "^9.1.2"
}
},
"node_modules/cache-manager-redis-store": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
"dependencies": {
"redis": "^3.0.2"
},
"engines": {
"node": ">= 8.3"
}
},
"node_modules/cache-manager-redis-store/node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/cache-manager-redis-store/node_modules/redis": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"dependencies": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/cache-manager/node_modules/lru-cache": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz",
"integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -8130,6 +8212,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -9802,6 +9889,30 @@
"@redis/time-series": "1.0.4" "@redis/time-series": "1.0.4"
} }
}, },
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",

View File

@ -45,6 +45,8 @@
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"axios": "^1.3.4", "axios": "^1.3.4",
"cache-manager": "^5.2.0",
"cache-manager-redis-store": "^2.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
@ -66,6 +68,7 @@
"@nestjs/schematics": "^8.0.0", "@nestjs/schematics": "^8.0.0",
"@nestjs/swagger": "^6.3.0", "@nestjs/swagger": "^6.3.0",
"@nestjs/testing": "^9.3.12", "@nestjs/testing": "^9.3.12",
"@types/cache-manager-redis-store": "^2.0.1",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-session": "^1.17.5", "@types/express-session": "^1.17.5",

View File

@ -838,6 +838,127 @@
"security": [{ "bearer": [] }] "security": [{ "bearer": [] }]
} }
}, },
"/accounts/worktypes/{id}": {
"post": {
"operationId": "updateWorktype",
"summary": "",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"description": "Worktypeの内部ID",
"schema": { "type": "number" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWorktypesRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWorktypeResponse"
}
}
}
},
"400": {
"description": "WorktypeIDが重複 / WorktypeIDが空",
"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/partners": {
"get": {
"operationId": "getPartners",
"summary": "",
"parameters": [
{
"name": "limit",
"required": true,
"in": "query",
"description": "取得件数",
"schema": { "type": "number" }
},
{
"name": "offset",
"required": true,
"in": "query",
"description": "開始位置",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/GetPartnersResponse" }
}
}
},
"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": [] }]
}
},
"/users/confirm": { "/users/confirm": {
"post": { "post": {
"operationId": "confirmUser", "operationId": "confirmUser",
@ -2767,6 +2888,60 @@
"required": ["worktypeId"] "required": ["worktypeId"]
}, },
"CreateWorktypeResponse": { "type": "object", "properties": {} }, "CreateWorktypeResponse": { "type": "object", "properties": {} },
"UpdateWorktypesRequest": {
"type": "object",
"properties": {
"worktypeId": {
"type": "string",
"minLength": 1,
"description": "WorktypeID"
},
"description": { "type": "string", "description": "Worktypeの説明" }
},
"required": ["worktypeId"]
},
"UpdateWorktypeResponse": { "type": "object", "properties": {} },
"Partner": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "会社名" },
"tier": { "type": "number", "description": "階層" },
"accountId": { "type": "number", "description": "アカウントID" },
"country": { "type": "string", "description": "国" },
"primaryAdmin": {
"type": "string",
"description": "プライマリ管理者"
},
"email": {
"type": "string",
"description": "プライマリ管理者メールアドレス"
},
"dealerManagement": {
"type": "boolean",
"description": "代行操作許可"
}
},
"required": [
"name",
"tier",
"accountId",
"country",
"primaryAdmin",
"email",
"dealerManagement"
]
},
"GetPartnersResponse": {
"type": "object",
"properties": {
"total": { "type": "number", "description": "合計件数" },
"partners": {
"type": "array",
"items": { "$ref": "#/components/schemas/Partner" }
}
},
"required": ["total", "partners"]
},
"ConfirmRequest": { "ConfirmRequest": {
"type": "object", "type": "object",
"properties": { "token": { "type": "string" } }, "properties": { "token": { "type": "string" } },

View File

@ -41,6 +41,7 @@ import { UserGroupsRepositoryModule } from './repositories/user_groups/user_grou
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module';
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module'; import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module'; import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
import { OptionItemsRepositoryModule } from './repositories/option_items/option_items.repository.module';
@Module({ @Module({
imports: [ imports: [
@ -96,6 +97,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re
AuthGuardsModule, AuthGuardsModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
WorktypesRepositoryModule, WorktypesRepositoryModule,
OptionItemsRepositoryModule,
], ],
controllers: [ controllers: [
HealthController, HealthController,

View File

@ -48,5 +48,11 @@ export const ErrorCodes = [
'E010806', // ライセンス割り当て不可エラー 'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー 'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー 'E010808', // ライセンス注文キャンセル不可エラー
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010908', // タイピストグループ不在エラー 'E010908', // タイピストグループ不在エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
] as const; ] as const;

View File

@ -37,5 +37,11 @@ export const errors: Errors = {
E010806: 'License is unavailable Error', E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error', E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error', E010808: 'Order cancel failed Error',
E010809: 'Already license order status changed Error',
E010810: 'Cancellation period expired error',
E010811: 'Already license allocated Error',
E010908: 'Typist Group not exist Error', E010908: 'Typist Group not exist Error',
E011001: 'Thiw WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
}; };

View File

@ -30,6 +30,7 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat
import { FilesService } from '../../features/files/files.service'; import { FilesService } from '../../features/files/files.service';
import { LicensesService } from '../../features/licenses/licenses.service'; import { LicensesService } from '../../features/licenses/licenses.service';
import { TasksService } from '../../features/tasks/tasks.service'; import { TasksService } from '../../features/tasks/tasks.service';
import { OptionItemsRepositoryModule } from '../../repositories/option_items/option_items.repository.module';
export const makeTestingModule = async ( export const makeTestingModule = async (
datasource: DataSource, datasource: DataSource,
@ -65,6 +66,7 @@ export const makeTestingModule = async (
AuthGuardsModule, AuthGuardsModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
WorktypesRepositoryModule, WorktypesRepositoryModule,
OptionItemsRepositoryModule,
], ],
providers: [ providers: [
AuthService, AuthService,

View File

@ -8,8 +8,7 @@ import {
@ValidatorConstraint() @ValidatorConstraint()
export class IsUniqueArray implements ValidatorConstraintInterface { export class IsUniqueArray implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars validate(arr: any[]) {
validate(arr: any[], args: ValidationArguments) {
return arr.length === new Set(arr).size; return arr.length === new Set(arr).size;
} }

View File

@ -1,6 +1,30 @@
import { registerDecorator, ValidationOptions } from 'class-validator'; import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
@ValidatorConstraint()
export class IsAdminPassword implements ValidatorConstraintInterface {
validate(value: string): boolean {
// 8文字64文字でなければ早期に不合格
const minLength = 8;
const maxLength = 64;
if (value.length < minLength || value.length > maxLength) {
return false;
}
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!から2種類以上組み合わせ
const charaTypePattern =
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
return new RegExp(charaTypePattern).test(value);
}
defaultMessage(): string {
return 'Admin password rule not satisfied';
}
}
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => { export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => { return (object: any, propertyName: string) => {
registerDecorator({ registerDecorator({
@ -9,24 +33,7 @@ export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
propertyName: propertyName, propertyName: propertyName,
constraints: [], constraints: [],
options: validationOptions, options: validationOptions,
validator: { validator: IsAdminPassword,
validate: (value: string) => {
// 8文字64文字でなければ早期に不合格
const minLength = 8;
const maxLength = 64;
if (value.length < minLength || value.length > maxLength) {
return false;
}
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!から2種類以上組み合わせ
const charaTypePattern =
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
return new RegExp(charaTypePattern).test(value);
},
defaultMessage: () => {
return 'Admin password rule not satisfied';
},
},
}); });
}; };
}; };

View File

@ -1,10 +1,33 @@
import { import {
registerDecorator, registerDecorator,
ValidationOptions, ValidationOptions,
ValidationArguments, ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator'; } from 'class-validator';
import { Assignee } from '../../features/tasks/types/types'; import { Assignee } from '../../features/tasks/types/types';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
@ValidatorConstraint()
export class IsTypist implements ValidatorConstraintInterface {
validate(values: Assignee[]): boolean {
return values.every((value) => {
const { typistUserId, typistGroupId, typistName } = value;
if (typistUserId === undefined && typistGroupId === undefined) {
return false;
}
if (typistUserId !== undefined && typistGroupId !== undefined) {
return false;
}
if (!typistName) {
return false;
}
return true;
});
}
defaultMessage(): string {
return 'Request body is invalid format';
}
}
/** /**
* Validations options * Validations options
* @param [validationOptions] * @param [validationOptions]
@ -17,28 +40,7 @@ export const IsAssignees = (validationOptions?: ValidationOptions) => {
target: object.constructor, target: object.constructor,
propertyName: propertyName, propertyName: propertyName,
options: validationOptions, options: validationOptions,
validator: { validator: IsTypist,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate: (values: Assignee[], args: ValidationArguments) => {
return values.every((value) => {
const { typistUserId, typistGroupId, typistName } = value;
if (typistUserId === undefined && typistGroupId === undefined) {
return false;
}
if (typistUserId !== undefined && typistGroupId !== undefined) {
return false;
}
if (!typistName) {
return false;
}
return true;
});
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defaultMessage: (args?: ValidationArguments): string => {
return 'Request body is invalid format';
},
},
}); });
}; };
}; };

View File

@ -2,9 +2,46 @@ import {
registerDecorator, registerDecorator,
ValidationArguments, ValidationArguments,
ValidationOptions, ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator'; } from 'class-validator';
import { SignupRequest } from '../../features/users/types/types'; import { SignupRequest } from '../../features/users/types/types';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
@ValidatorConstraint()
export class IsPassword implements ValidatorConstraintInterface {
validate(value: string | undefined): boolean {
// passwordが設定されていない場合はチェックしない
if (value === undefined) {
return true;
}
// 正規表現でパスワードのチェックを行う
// 416文字の半角英数字と記号のみ
const regex = /^[!-~]{4,16}$/;
if (!regex.test(value)) {
return false;
}
return true;
}
defaultMessage(): string {
return 'EncryptionPassword rule not satisfied';
}
}
@ValidatorConstraint()
export class IsEncryptionPassword implements ValidatorConstraintInterface {
validate(value: string | undefined, args: ValidationArguments): boolean {
const { encryption } = args.object as SignupRequest;
if (encryption === true && !value) {
return false;
}
return true;
}
defaultMessage(): string {
return 'Encryption password is required when encryption is enabled';
}
}
export const IsPasswordvalid = (validationOptions?: ValidationOptions) => { export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => { return (object: any, propertyName: string) => {
registerDecorator({ registerDecorator({
@ -13,29 +50,10 @@ export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
propertyName: propertyName, propertyName: propertyName,
constraints: [], constraints: [],
options: validationOptions, options: validationOptions,
validator: { validator: IsPassword,
validate: (value: string | undefined) => {
// passwordが設定されていない場合はチェックしない
if (value === undefined) {
return true;
}
// 正規表現でパスワードのチェックを行う
// 416文字の半角英数字と記号のみ
const regex = /^[!-~]{4,16}$/;
if (!regex.test(value)) {
return false;
}
return true;
},
defaultMessage: () => {
return 'EncryptionPassword rule not satisfied';
},
},
}); });
}; };
}; };
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する
export const IsEncryptionPasswordPresent = ( export const IsEncryptionPasswordPresent = (
validationOptions?: ValidationOptions, validationOptions?: ValidationOptions,
) => { ) => {
@ -46,18 +64,7 @@ export const IsEncryptionPasswordPresent = (
propertyName: propertyName, propertyName: propertyName,
constraints: [], constraints: [],
options: validationOptions, options: validationOptions,
validator: { validator: IsEncryptionPassword,
validate: (value: string | undefined, args: ValidationArguments) => {
const { encryption } = args.object as SignupRequest;
if (encryption === true && !value) {
return false;
}
return true;
},
defaultMessage: () => {
return 'Encryption password is required when encryption is enabled';
},
},
}); });
}; };
}; };

View File

@ -2,6 +2,8 @@ import {
registerDecorator, registerDecorator,
ValidationArguments, ValidationArguments,
ValidationOptions, ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator'; } from 'class-validator';
import { import {
PostUpdateUserRequest, PostUpdateUserRequest,
@ -9,7 +11,26 @@ import {
} from '../../features/users/types/types'; } from '../../features/users/types/types';
import { USER_ROLES } from '../../constants'; import { USER_ROLES } from '../../constants';
// TODO タスク 2502: バリデータをクラスを使用した記述に統一するで修正する @ValidatorConstraint()
export class IsRoleAuthorData implements ValidatorConstraintInterface {
propertyName: string;
constructor(propertyName: string) {
this.propertyName = propertyName;
}
validate(value: any, args: ValidationArguments): boolean {
const request = args.object as SignupRequest | PostUpdateUserRequest;
const { role } = request;
if (role === USER_ROLES.AUTHOR && value === undefined) {
return false;
}
return true;
}
defaultMessage(): string {
return `When role is author, ${this.propertyName} cannot be undefined`;
}
}
export const IsRoleAuthorDataValid = < export const IsRoleAuthorDataValid = <
T extends SignupRequest | PostUpdateUserRequest, T extends SignupRequest | PostUpdateUserRequest,
>( >(
@ -22,19 +43,7 @@ export const IsRoleAuthorDataValid = <
propertyName: propertyName, propertyName: propertyName,
constraints: [], constraints: [],
options: validationOptions, options: validationOptions,
validator: { validator: new IsRoleAuthorData(propertyName),
validate: (value: any, args: ValidationArguments) => {
const request = args.object as T;
const { role } = request;
if (role === USER_ROLES.AUTHOR && value === undefined) {
return false;
}
return true;
},
defaultMessage: () => {
return `When role is author, ${propertyName} cannot be undefined`;
},
},
}); });
}; };
}; };

View File

@ -10,8 +10,7 @@ import { TASK_STATUS } from '../../constants';
@ValidatorConstraint() @ValidatorConstraint()
export class IsStatusConstraint implements ValidatorConstraintInterface { export class IsStatusConstraint implements ValidatorConstraintInterface {
private readonly STATUS: string[] = Object.values(TASK_STATUS); private readonly STATUS: string[] = Object.values(TASK_STATUS);
// eslint-disable-next-line @typescript-eslint/no-unused-vars validate(value: string): boolean {
validate(value: string, args: ValidationArguments): boolean {
if (value) { if (value) {
// ,で分割した文字列のすべてがTASK_STATUSのプロパティに存在する値であった場合のみtrue // ,で分割した文字列のすべてがTASK_STATUSのプロパティに存在する値であった場合のみtrue
return value.split(',').every((state) => this.STATUS.includes(state)); return value.split(',').every((state) => this.STATUS.includes(state));
@ -20,8 +19,7 @@ export class IsStatusConstraint implements ValidatorConstraintInterface {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars defaultMessage(): string {
defaultMessage(validationArguments?: ValidationArguments): string {
return `invalid status string`; return `invalid status string`;
} }
} }

View File

@ -0,0 +1,50 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
ValidationOptions,
registerDecorator,
} from 'class-validator';
@ValidatorConstraint()
export class IsWorktypeIdCharacters implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate(value: string) {
// 正規表現でWorktypeIDのチェックを行う
// 以下の禁則文字を除く半角英数記号
// \ (backslash)
// / (forward slash)
// : (colon)
// * (asterisk)
// ? (question mark)
// " (double quotation mark)
// < (less-than symbol)
// > (greater-than symbol)
// | (vertical bar)
// . (period)
const regex =
/^(?!.*\\)(?!.*\/)(?!.*:)(?!.*\*)(?!.*\?)(?!.*")(?!.*<)(?!.*>)(?!.*\|)(?!.*\.)[ -~]+$/;
return regex.test(value);
}
defaultMessage(args: ValidationArguments) {
return `WorktypeID rule not satisfied`;
}
}
/**
* WorktypeIDで使用できる文字列かをチェックする
* @param [validationOptions]
* @returns
*/
export function IsWorktypeId(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'IsWorktypeId',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: IsWorktypeIdCharacters,
});
};
}

View File

@ -223,3 +223,18 @@ export const TRIAL_LICENSE_EXPIRATION_DAYS = 30;
* @const {number} * @const {number}
*/ */
export const TRIAL_LICENSE_ISSUE_NUM = 100; export const TRIAL_LICENSE_ISSUE_NUM = 100;
/**
* worktypeの最大登録数
* @const {number}
*/
export const WORKTYPE_MAX_COUNT = 20;
/**
* worktypeのDefault値の取りうる値
**/
export const OPTION_ITEM_VALUE_TYPE = {
DEFAULT: 'Default',
BLANK: 'Blank',
LAST_INPUT: 'LastInput',
} as const;

View File

@ -7,6 +7,7 @@ import {
Req, Req,
UseGuards, UseGuards,
Param, Param,
Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
ApiOperation, ApiOperation,
@ -45,6 +46,11 @@ import {
GetWorktypesResponse, GetWorktypesResponse,
CreateWorktypeResponse, CreateWorktypeResponse,
CreateWorktypesRequest, CreateWorktypesRequest,
GetPartnersRequest,
GetPartnersResponse,
UpdateWorktypeRequestParam,
UpdateWorktypeResponse,
UpdateWorktypesRequest,
} from './types/types'; } from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards'; import { AuthGuard } from '../../common/guards/auth/authguards';
@ -633,12 +639,12 @@ export class AccountsController {
const context = makeContext(payload.userId); const context = makeContext(payload.userId);
// TODO: 発行キャンセル処理。API実装のタスク2498で本実装 await this.accountService.cancelIssue(
// await this.accountService.cancelIssue( context,
// context, payload.userId,
// body.poNumber, body.poNumber,
// body.orderedAccountId, body.orderedAccountId,
// ); );
return {}; return {};
} }
@ -706,10 +712,142 @@ export class AccountsController {
const { userId } = jwt.decode(token, { json: true }) as AccessToken; const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId); const context = makeContext(userId);
console.log(context.trackingId); await this.accountService.createWorktype(
console.log(worktypeId); context,
console.log(description); userId,
worktypeId,
description,
);
return {}; return {};
} }
@Post('/worktypes/:id')
@ApiResponse({
status: HttpStatus.OK,
type: UpdateWorktypeResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'WorktypeIDが重複 / WorktypeIDが空',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'updateWorktype' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async updateWorktype(
@Req() req: Request,
@Param() param: UpdateWorktypeRequestParam,
@Body() body: UpdateWorktypesRequest,
): Promise<UpdateWorktypeResponse> {
const { worktypeId, description } = body;
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
await this.accountService.updateWorktype(
context,
userId,
id,
worktypeId,
description,
);
return {};
}
@Get('/partners')
@ApiResponse({
status: HttpStatus.OK,
type: GetPartnersResponse,
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: 'getPartners' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN],
tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4],
}),
)
async getPartners(
@Req() req: Request,
@Query() query: GetPartnersRequest,
): Promise<GetPartnersResponse> {
const { limit, offset } = query;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
// TODO: パートナー取得APIで実装
// await this.accountService.getPartners(
// context,
// body.limit,
// body.offset,
// );
// 仮のreturn
return {
total: 1,
partners: [
{
name: 'testA',
tier: 5,
accountId: 1,
country: 'US',
primaryAdmin: 'nameA',
email: 'aaa@example.com',
dealerManagement: true,
},
{
name: 'testB',
tier: 5,
accountId: 2,
country: 'US',
primaryAdmin: 'nameB',
email: 'bbb@example.com',
dealerManagement: false,
},
{
name: 'testC',
tier: 5,
accountId: 1,
country: 'US',
primaryAdmin: 'nothing',
email: 'nothing',
dealerManagement: false,
},
],
};
}
} }

View File

@ -17,6 +17,7 @@ import {
createLicenseOrder, createLicenseOrder,
createLicenseSetExpiryDateAndStatus, createLicenseSetExpiryDateAndStatus,
createWorktype, createWorktype,
getOptionItems,
getSortCriteria, getSortCriteria,
getTypistGroup, getTypistGroup,
getTypistGroupMember, getTypistGroupMember,
@ -31,10 +32,19 @@ import {
getUserFromExternalId, getUserFromExternalId,
getUsers, getUsers,
makeTestUser, makeTestUser,
makeHierarchicalAccounts,
} from '../../common/test/utility'; } from '../../common/test/utility';
import { AccountsService } from './accounts.service'; import { AccountsService } from './accounts.service';
import { Context, makeContext } from '../../common/log'; import { Context, makeContext } from '../../common/log';
import { TIERS, USER_ROLES } from '../../constants'; import {
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
OPTION_ITEM_VALUE_TYPE,
TIERS,
USER_ROLES,
WORKTYPE_MAX_COUNT,
} from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity'; import { License } from '../../repositories/licenses/entity/license.entity';
import { import {
overrideAccountsRepositoryService, overrideAccountsRepositoryService,
@ -45,7 +55,13 @@ import {
import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service'; import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import {
createOrder,
selectLicense,
selectOrderLicense,
} from '../licenses/test/utility';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service'; import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
describe('createAccount', () => { describe('createAccount', () => {
let source: DataSource = null; let source: DataSource = null;
@ -1820,14 +1836,79 @@ describe('getPartnerAccount', () => {
).account; ).account;
// 所有ライセンスを追加3、子11、子22 // 所有ライセンスを追加3、子11、子22
await createLicense(source, parentAccountId); await createLicense(
await createLicense(source, parentAccountId); source,
await createLicense(source, parentAccountId); 1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
4,
null,
childAccountId1,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
2,
null,
null,
);
await createLicense(source, childAccountId1); await createLicense(
source,
await createLicense(source, childAccountId2); 5,
await createLicense(source, childAccountId2); null,
childAccountId2,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
3,
null,
null,
);
await createLicense(
source,
6,
null,
childAccountId2,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
3,
null,
null,
);
// ライセンス注文を追加子1→親10ライセンス、子2→親5ライセンス // ライセンス注文を追加子1→親10ライセンス、子2→親5ライセンス
await createLicenseOrder( await createLicenseOrder(
@ -1973,7 +2054,12 @@ describe('getPartnerAccount', () => {
} }
// 有効期限未設定のライセンスを1件追加子1 // 有効期限未設定のライセンスを1件追加子1
await createLicense(source, childAccountId1); await createLicenseSetExpiryDateAndStatus(
source,
childAccountId1,
null,
'Unallocated',
);
const service = module.get<AccountsService>(AccountsService); const service = module.get<AccountsService>(AccountsService);
const accountId = parentAccountId; const accountId = parentAccountId;
@ -2165,9 +2251,42 @@ describe('issueLicense', () => {
}); });
// 親のライセンスを作成する3個 // 親のライセンスを作成する3個
await createLicense(source, parentAccountId); await createLicense(
await createLicense(source, parentAccountId); source,
await createLicense(source, parentAccountId); 1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する2個 // 子から親への注文を作成する2個
await createLicenseOrder( await createLicenseOrder(
source, source,
@ -2224,9 +2343,42 @@ describe('issueLicense', () => {
role: 'admin', role: 'admin',
}); });
// 親のライセンスを作成する3個 // 親のライセンスを作成する3個
await createLicense(source, parentAccountId); await createLicense(
await createLicense(source, parentAccountId); source,
await createLicense(source, parentAccountId); 1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する2個 // 子から親への注文を作成する2個
await createLicenseOrder( await createLicenseOrder(
source, source,
@ -2282,9 +2434,42 @@ describe('issueLicense', () => {
}); });
// 親のライセンスを作成する3個 // 親のライセンスを作成する3個
await createLicense(source, parentAccountId); await createLicense(
await createLicense(source, parentAccountId); source,
await createLicense(source, parentAccountId); 1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する4個 // 子から親への注文を作成する4個
await createLicenseOrder( await createLicenseOrder(
source, source,
@ -3263,3 +3448,710 @@ describe('getWorktypes', () => {
} }
}); });
}); });
describe('createWorktype', () => {
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('Worktypeを作成できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
// Worktypeが未登録であることを確認
{
const worktypes = await getWorktypes(source, account.id);
const optionItems = await getOptionItems(source);
expect(worktypes.length).toBe(0);
expect(optionItems.length).toBe(0);
}
await service.createWorktype(
context,
admin.external_id,
'worktype1',
'description1',
);
//実行結果を確認
{
const worktypes = await getWorktypes(source, account.id);
const optionItems = await getOptionItems(source, worktypes[0].id);
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe('worktype1');
expect(worktypes[0].description).toBe('description1');
expect(optionItems.length).toBe(10);
expect(optionItems[0].item_label).toBe('');
expect(optionItems[0].default_value_type).toBe(
OPTION_ITEM_VALUE_TYPE.DEFAULT,
);
expect(optionItems[0].initial_value).toBe('');
}
});
it('WorktypeIDが登録済みのWorktypeIDと重複した場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktypeId = 'worktype1';
await createWorktype(source, account.id, worktypeId);
//作成したデータを確認
{
const worktypes = await getWorktypes(source, account.id);
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktypeId);
}
try {
await service.createWorktype(context, admin.external_id, worktypeId);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E011001'));
} else {
fail();
}
}
});
it('WorktypeIDがすでに最大登録数20件まで登録されている場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
// あらかじめ最大登録数分のWorktypeを登録する
for (let i = 0; i < WORKTYPE_MAX_COUNT; i++) {
await createWorktype(source, account.id, `worktype${i + 1}`);
}
//作成したデータを確認
{
const worktypes = await getWorktypes(source, account.id);
expect(worktypes.length).toBe(WORKTYPE_MAX_COUNT);
}
try {
await service.createWorktype(context, admin.external_id, 'newWorktypeID');
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E011002'));
} else {
fail();
}
}
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
//DBアクセスに失敗するようにする
const worktypeService = module.get<WorktypesRepositoryService>(
WorktypesRepositoryService,
);
worktypeService.createWorktype = jest.fn().mockRejectedValue('DB failed');
try {
await service.createWorktype(context, admin.external_id, 'worktype1');
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});
describe('updateWorktype', () => {
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('Worktypeを更新できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = new Worktype();
worktype.custom_worktype_id = 'worktypeID1';
worktype.description = 'description1';
await createWorktype(
source,
account.id,
worktype.custom_worktype_id,
worktype.description,
);
// Worktypeを確認
const worktypes = await getWorktypes(source, account.id);
{
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktype.custom_worktype_id);
expect(worktypes[0].description).toBe(worktype.description);
}
const updateWorktypeId = 'updateWorktypeID';
const updateDescription = 'updateDescription';
await service.updateWorktype(
context,
admin.external_id,
worktypes[0].id,
updateWorktypeId,
updateDescription,
);
//実行結果を確認
{
const worktypes = await getWorktypes(source, account.id);
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(updateWorktypeId);
expect(worktypes[0].description).toBe(updateDescription);
}
});
it('指定したIDが登録されていない場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = new Worktype();
worktype.custom_worktype_id = 'worktypeID1';
worktype.description = 'description1';
await createWorktype(
source,
account.id,
worktype.custom_worktype_id,
worktype.description,
);
// Worktypeを確認
{
const worktypes = await getWorktypes(source, account.id);
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktype.custom_worktype_id);
expect(worktypes[0].description).toBe(worktype.description);
}
try {
await service.updateWorktype(
context,
admin.external_id,
999,
'newWorktypeID',
'newDescription',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E011003'));
} else {
fail();
}
}
});
it('WorktypeIDが登録済みのWorktypeIDと重複した場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype1 = new Worktype();
worktype1.custom_worktype_id = 'worktypeID1';
worktype1.description = 'description1';
const worktype2 = new Worktype();
worktype2.custom_worktype_id = 'worktypeID2';
worktype2.description = 'description2';
await createWorktype(
source,
account.id,
worktype1.custom_worktype_id,
worktype1.description,
);
await createWorktype(
source,
account.id,
worktype2.custom_worktype_id,
worktype2.description,
);
//作成したデータを確認
const worktypes = await getWorktypes(source, account.id);
{
expect(worktypes.length).toBe(2);
expect(worktypes[0].custom_worktype_id).toBe(
worktype1.custom_worktype_id,
);
expect(worktypes[0].description).toBe(worktype1.description);
expect(worktypes[1].custom_worktype_id).toBe(
worktype2.custom_worktype_id,
);
expect(worktypes[1].description).toBe(worktype2.description);
}
try {
await service.updateWorktype(
context,
admin.external_id,
worktypes[0].id,
worktype2.custom_worktype_id,
worktype2.description,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E011001'));
} else {
fail();
}
}
});
it('WorktypeIDが登録済みの指定IDのWorktypeIDと重複した場合でも更新できること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = new Worktype();
worktype.custom_worktype_id = 'worktypeID1';
worktype.description = 'description1';
await createWorktype(
source,
account.id,
worktype.custom_worktype_id,
worktype.description,
);
// Worktypeを確認
const worktypes = await getWorktypes(source, account.id);
{
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktype.custom_worktype_id);
expect(worktypes[0].description).toBe(worktype.description);
}
const updateDescription = 'updateDescription';
await service.updateWorktype(
context,
admin.external_id,
worktypes[0].id,
worktype.custom_worktype_id,
updateDescription,
);
//実行結果を確認
{
const worktypes = await getWorktypes(source, account.id);
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktype.custom_worktype_id);
expect(worktypes[0].description).toBe(updateDescription);
}
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = new Worktype();
worktype.custom_worktype_id = 'worktypeID1';
worktype.description = 'description1';
await createWorktype(
source,
account.id,
worktype.custom_worktype_id,
worktype.description,
);
// Worktypeを確認
const worktypes = await getWorktypes(source, account.id);
{
expect(worktypes.length).toBe(1);
expect(worktypes[0].custom_worktype_id).toBe(worktype.custom_worktype_id);
expect(worktypes[0].description).toBe(worktype.description);
}
//DBアクセスに失敗するようにする
const worktypeService = module.get<WorktypesRepositoryService>(
WorktypesRepositoryService,
);
worktypeService.updateWorktype = jest.fn().mockRejectedValue('DB failed');
try {
await service.updateWorktype(
context,
admin.external_id,
worktypes[0].id,
'newWorktype',
'newDescription',
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});
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 { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 10);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
date.setDate(date.getDate() + 10);
// 発行時に論理削除されたライセンス情報
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
date,
1,
);
const service = module.get<AccountsService>(AccountsService);
await service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
);
// 発行待ちに戻した注文の状態確認
const orderRecord = await selectOrderLicense(
source,
tier5Accounts.account.id,
poNumber,
);
expect(orderRecord.orderLicense.issued_at).toBe(null);
expect(orderRecord.orderLicense.status).toBe(
LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
);
// 未割当に戻したライセンスの状態確認
const licenseRecord = await selectLicense(source, 1);
expect(licenseRecord.license.status).toBe(
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
);
expect(licenseRecord.license.delete_order_id).toBe(null);
expect(licenseRecord.license.deleted_at).toBe(null);
});
it('ライセンス発行のキャンセルが完了する(第二階層で実行)', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 10);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
date.setDate(date.getDate() + 10);
// 発行時に論理削除されたライセンス情報
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
date,
1,
);
const service = module.get<AccountsService>(AccountsService);
await service.cancelIssue(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
);
// 発行待ちに戻した注文の状態確認
const orderRecord = await selectOrderLicense(
source,
tier5Accounts.account.id,
poNumber,
);
expect(orderRecord.orderLicense.issued_at).toBe(null);
expect(orderRecord.orderLicense.status).toBe(
LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
);
// 未割当に戻したライセンスの状態確認
const licenseRecord = await selectLicense(source, 1);
expect(licenseRecord.license.status).toBe(
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
);
expect(licenseRecord.license.delete_order_id).toBe(null);
expect(licenseRecord.license.deleted_at).toBe(null);
});
it('キャンセル対象の発行が存在しない場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010809'), HttpStatus.BAD_REQUEST),
);
});
it('キャンセル対象の発行が14日より経過していた場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 15);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010810'), HttpStatus.BAD_REQUEST),
);
});
it('キャンセル対象の発行のライセンスが使われていた場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 14);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010811'), HttpStatus.BAD_REQUEST),
);
});
it('自身のパートナー以外の発行をキャンセルしようとした場合、エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: 100,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 14);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E000108'), HttpStatus.UNAUTHORIZED),
);
});
});

View File

@ -40,6 +40,9 @@ import {
LicensesShortageError, LicensesShortageError,
AlreadyIssuedError, AlreadyIssuedError,
OrderNotFoundError, OrderNotFoundError,
AlreadyLicenseStatusChangedError,
AlreadyLicenseAllocatedError,
CancellationPeriodExpiredError,
} from '../../repositories/licenses/errors/types'; } from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { import {
@ -47,6 +50,11 @@ import {
TypistIdInvalidError, TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types'; } from '../../repositories/user_groups/errors/types';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service'; import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
@Injectable() @Injectable()
export class AccountsService { export class AccountsService {
@ -1053,6 +1061,90 @@ export class AccountsService {
} }
} }
/**
*
* @param context
* @param extarnalId
* @param poNumber
* @param orderedAccountId
*/
async cancelIssue(
context: Context,
extarnalId: string,
poNumber: string,
orderedAccountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.cancelIssue.name} | params: { ` +
`extarnalId: ${extarnalId}, ` +
`poNumber: ${poNumber}, ` +
`orderedAccountId: ${orderedAccountId}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(extarnalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
}
// 注文元アカウントIDの親世代を取得
const parentAccountIds = await this.accountRepository.getHierarchyParents(
orderedAccountId,
);
// 自身が存在しない場合、エラー
if (!parentAccountIds.includes(myAccountId)) {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
try {
// 発行キャンセル処理
await this.accountRepository.cancelIssue(orderedAccountId, poNumber);
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
case AlreadyLicenseStatusChangedError:
throw new HttpException(
makeErrorResponse('E010809'),
HttpStatus.BAD_REQUEST,
);
case CancellationPeriodExpiredError:
throw new HttpException(
makeErrorResponse('E010810'),
HttpStatus.BAD_REQUEST,
);
case AlreadyLicenseAllocatedError:
throw new HttpException(
makeErrorResponse('E010811'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
}
}
/** /**
* *
* @param context * @param context
@ -1091,4 +1183,133 @@ export class AccountsService {
); );
} }
} }
/**
*
* @param context
* @param externalId
* @param worktypeId
* @param [description]
* @returns worktype
*/
async createWorktype(
context: Context,
externalId: string,
worktypeId: string,
description?: string,
): Promise<void> {
this.logger.log(`[IN] [${context.trackingId}] ${this.createWorktype.name}`);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.worktypesRepository.createWorktype(
accountId,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// WorktypeIDが既に存在する場合は400エラーを返す
case WorktypeIdAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E011001'),
HttpStatus.BAD_REQUEST,
);
// WorktypeIDが登録上限以上の場合は400エラーを返す
case WorktypeIdMaxCountError:
throw new HttpException(
makeErrorResponse('E011002'),
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.createWorktype.name}`,
);
}
}
/**
*
* @param context
* @param externalId
* @param id ID
* @param worktypeId
* @param [description]
* @returns worktype
*/
async updateWorktype(
context: Context,
externalId: string,
id: number,
worktypeId: string,
description?: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id}, ` +
`worktypeId: ${worktypeId}, ` +
`description: ${description} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ワークタイプを更新する
await this.worktypesRepository.updateWorktype(
accountId,
id,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// ユーザーが設定したWorktypeIDが既存WorktypeのWorktypeIDと重複する場合は400エラーを返す
case WorktypeIdAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E011001'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
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.updateWorktype.name}`,
);
}
}
} }

View File

@ -7,6 +7,7 @@ import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_cr
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity'; import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
import { OptionItem } from '../../../repositories/option_items/entity/option_item.entity';
/** /**
* ユーティリティ: すべてのソート条件を取得する * ユーティリティ: すべてのソート条件を取得する
@ -19,17 +20,26 @@ export const getSortCriteria = async (dataSource: DataSource) => {
export const createLicense = async ( export const createLicense = async (
datasource: DataSource, datasource: DataSource,
licenseId: number,
expiry_date: Date,
accountId: number, accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
): Promise<void> => { ): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({ const { identifiers } = await datasource.getRepository(License).insert({
expiry_date: null, id: licenseId,
expiry_date: expiry_date,
account_id: accountId, account_id: accountId,
type: 'NORMAL', type: type,
status: 'Unallocated', status: status,
allocated_user_id: null, allocated_user_id: allocated_user_id,
order_id: null, order_id: order_id,
deleted_at: null, deleted_at: deleted_at,
delete_order_id: null, delete_order_id: delete_order_id,
created_by: 'test_runner', created_by: 'test_runner',
created_at: new Date(), created_at: new Date(),
updated_by: 'updater', updated_by: 'updater',
@ -142,3 +152,17 @@ export const getWorktypes = async (
}, },
}); });
}; };
// オプションアイテムを取得する
export const getOptionItems = async (
datasource: DataSource,
worktypeId?: number,
): Promise<OptionItem[]> => {
return worktypeId
? await datasource.getRepository(OptionItem).find({
where: {
worktype_id: worktypeId,
},
})
: await datasource.getRepository(OptionItem).find();
};

View File

@ -13,6 +13,7 @@ import {
import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator';
import { IsUnique } from '../../../common/validators/IsUnique.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsWorktypeId } from '../../../common/validators/worktype.validator';
export class CreateAccountRequest { export class CreateAccountRequest {
@ApiProperty() @ApiProperty()
@ -350,9 +351,72 @@ export class GetWorktypesResponse {
export class CreateWorktypesRequest { export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' }) @ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1) @MinLength(1)
@MaxLength(255)
@IsWorktypeId()
worktypeId: string; worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false }) @ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string; description?: string;
} }
export class CreateWorktypeResponse {} export class CreateWorktypeResponse {}
export class UpdateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1)
@MaxLength(255)
@IsWorktypeId()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string;
}
export class UpdateWorktypeResponse {}
export class UpdateWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
id: number;
}
export class GetPartnersRequest {
@ApiProperty({ description: '取得件数' })
@IsInt()
@Min(0)
@Type(() => Number)
limit: number;
@ApiProperty({ description: '開始位置' })
@IsInt()
@Min(0)
@Type(() => Number)
offset: number;
}
export class Partner {
@ApiProperty({ description: '会社名' })
name: string;
@ApiProperty({ description: '階層' })
tier: number;
@ApiProperty({ description: 'アカウントID' })
accountId: number;
@ApiProperty({ description: '国' })
country: string;
@ApiProperty({ description: 'プライマリ管理者' })
primaryAdmin: string;
@ApiProperty({ description: 'プライマリ管理者メールアドレス' })
email: string;
@ApiProperty({ description: '代行操作許可' })
dealerManagement: boolean;
}
export class GetPartnersResponse {
@ApiProperty({ description: '合計件数' })
total: number;
@ApiProperty({ type: [Partner] })
partners: Partner[];
}

View File

@ -27,7 +27,11 @@ import { v4 as uuidv4 } from 'uuid';
@ApiTags('auth') @ApiTags('auth')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) {} constructor(
// TODO「タスク 1828: IDトークンを一度しか使えないようにする」で使用する予定
// private readonly redisService: RedisService,
private readonly authService: AuthService,
) {}
@Post('token') @Post('token')
@ApiResponse({ @ApiResponse({

View File

@ -4,7 +4,6 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Module({ @Module({
imports: [ConfigModule, AdB2cModule, UsersRepositoryModule], imports: [ConfigModule, AdB2cModule, UsersRepositoryModule],
controllers: [AuthController], controllers: [AuthController],

View File

@ -346,6 +346,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createCardLicense(source, license_id, issueId, cardLicenseKey); await createCardLicense(source, license_id, issueId, cardLicenseKey);
await createCardLicenseIssue(source, issueId); await createCardLicenseIssue(source, issueId);
@ -386,6 +389,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 2件目、expiry_dateがnull(OneYear) // 2件目、expiry_dateがnull(OneYear)
await createLicense( await createLicense(
@ -396,6 +402,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 3件目、1件目と同じ有効期限 // 3件目、1件目と同じ有効期限
await createLicense( await createLicense(
@ -406,6 +415,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 4件目、expiry_dateが一番遠いデータ // 4件目、expiry_dateが一番遠いデータ
await createLicense( await createLicense(
@ -416,6 +428,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 5件目、expiry_dateがnull(OneYear) // 5件目、expiry_dateがnull(OneYear)
await createLicense( await createLicense(
@ -426,6 +441,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 6件目、ライセンス状態が割当済 // 6件目、ライセンス状態が割当済
await createLicense( await createLicense(
@ -436,6 +454,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
null, null,
null,
null,
null,
); );
// 7件目、ライセンス状態が削除済 // 7件目、ライセンス状態が削除済
await createLicense( await createLicense(
@ -446,6 +467,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.DELETED, LICENSE_ALLOCATED_STATUS.DELETED,
null, null,
null,
null,
null,
); );
// 8件目、別アカウントの未割当のライセンス // 8件目、別アカウントの未割当のライセンス
await createLicense( await createLicense(
@ -456,6 +480,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
// 9件目、有効期限切れのライセンス // 9件目、有効期限切れのライセンス
await createLicense( await createLicense(
@ -466,6 +493,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
const service = module.get<LicensesService>(LicensesService); const service = module.get<LicensesService>(LicensesService);
const context = makeContext('userId'); const context = makeContext('userId');
@ -518,6 +548,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -570,6 +603,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE, LICENSE_ALLOCATED_STATUS.REUSABLE,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -616,6 +652,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId, userId,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -625,6 +664,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -697,6 +739,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId, userId,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -706,6 +751,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -742,6 +790,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId, userId,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -751,6 +802,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'CARD'); await createLicenseAllocationHistory(source, 1, userId, 1, 'CARD');
@ -787,6 +841,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.TRIAL, LICENSE_TYPE.TRIAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId, userId,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -796,6 +853,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD, LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null, null,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'TRIAL'); await createLicenseAllocationHistory(source, 1, userId, 1, 'TRIAL');
@ -832,6 +892,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE, LICENSE_ALLOCATED_STATUS.REUSABLE,
null, null,
null,
null,
null,
); );
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
@ -863,6 +926,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
null, null,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -872,6 +938,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.DELETED, LICENSE_ALLOCATED_STATUS.DELETED,
null, null,
null,
null,
null,
); );
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
@ -927,6 +996,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId, userId,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -987,6 +1059,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED, LICENSE_ALLOCATED_STATUS.ALLOCATED,
2, 2,
null,
null,
null,
); );
await createLicense( await createLicense(
source, source,
@ -996,6 +1071,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL, LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE, LICENSE_ALLOCATED_STATUS.REUSABLE,
userId, userId,
null,
null,
null,
); );
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -1037,6 +1115,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber, poNumber,
tier2Accounts[0].account.id, tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id, tier2Accounts[0].account.parent_account_id,
null,
10, 10,
'Issue Requesting', 'Issue Requesting',
); );
@ -1046,6 +1125,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber, poNumber,
tier2Accounts[0].account.id, tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id, tier2Accounts[0].account.parent_account_id,
null,
10, 10,
'Order Canceled', 'Order Canceled',
); );
@ -1078,6 +1158,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber, poNumber,
tier2Accounts[0].account.id, tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id, tier2Accounts[0].account.parent_account_id,
null,
10, 10,
'Issued', 'Issued',
); );
@ -1106,6 +1187,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber, poNumber,
tier2Accounts[0].account.id, tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id, tier2Accounts[0].account.parent_account_id,
null,
10, 10,
'Order Canceled', 'Order Canceled',
); );

View File

@ -15,6 +15,9 @@ export const createLicense = async (
type: string, type: string,
status: string, status: string,
allocated_user_id: number, allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
): Promise<void> => { ): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({ const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId, id: licenseId,
@ -23,9 +26,9 @@ export const createLicense = async (
type: type, type: type,
status: status, status: status,
allocated_user_id: allocated_user_id, allocated_user_id: allocated_user_id,
order_id: null, order_id: order_id,
deleted_at: null, deleted_at: deleted_at,
delete_order_id: null, delete_order_id: delete_order_id,
created_by: 'test_runner', created_by: 'test_runner',
created_at: new Date(), created_at: new Date(),
updated_by: 'updater', updated_by: 'updater',
@ -100,6 +103,7 @@ export const createOrder = async (
poNumber: string, poNumber: string,
fromId: number, fromId: number,
toId: number, toId: number,
issuedAt: Date,
quantity: number, quantity: number,
status: string, status: string,
): Promise<void> => { ): Promise<void> => {
@ -108,7 +112,7 @@ export const createOrder = async (
from_account_id: fromId, from_account_id: fromId,
to_account_id: toId, to_account_id: toId,
ordered_at: new Date(), ordered_at: new Date(),
issued_at: null, issued_at: issuedAt,
quantity: quantity, quantity: quantity,
status: status, status: status,
canceled_at: null, canceled_at: null,

View File

@ -20,6 +20,7 @@ import {
} from '../../common/types/sort/util'; } from '../../common/types/sort/util';
import { import {
LICENSE_ALLOCATED_STATUS, LICENSE_ALLOCATED_STATUS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_ISSUE_STATUS, LICENSE_ISSUE_STATUS,
TIERS, TIERS,
} from '../../constants'; } from '../../constants';
@ -28,6 +29,12 @@ import {
PartnerLicenseInfoForRepository, PartnerLicenseInfoForRepository,
} from '../../features/accounts/types/types'; } from '../../features/accounts/types/types';
import { AccountNotFoundError } from './errors/types'; import { AccountNotFoundError } from './errors/types';
import {
AlreadyLicenseAllocatedError,
AlreadyLicenseStatusChangedError,
CancellationPeriodExpiredError,
} from '../licenses/errors/types';
import { DateWithZeroTime } from '../../features/licenses/types/types';
@Injectable() @Injectable()
export class AccountsRepositoryService { export class AccountsRepositoryService {
@ -572,4 +579,110 @@ export class AccountsRepositoryService {
return accounts; return accounts;
} }
/**
* IDの親世代のアカウントIDをすべて取得する
* (tier)
* @param targetAccountId
* @returns accountIds
*/
async getHierarchyParents(targetAccountId: number): Promise<number[]> {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepository = entityManager.getRepository(Account);
const maxTierDifference = TIERS.TIER5 - TIERS.TIER1;
const parentAccountIds = [];
let currentAccountId = targetAccountId;
// システム的な最大の階層差異分、親を参照する
for (let i = 0; i < maxTierDifference; i++) {
const account = await accountRepository.findOne({
where: {
id: currentAccountId,
},
});
if (!account) {
break;
}
parentAccountIds.push(account.parent_account_id);
currentAccountId = account.parent_account_id;
}
return parentAccountIds;
});
}
/**
* IDとPOナンバーに紐づくライセンス発行をキャンセルする
* @param orderedAccountId:キャンセルしたい発行の注文元アカウントID
* @param poNumber:POナンバー
*/
async cancelIssue(orderedAccountId: 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: orderedAccountId,
po_number: poNumber,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
});
// キャンセル対象の発行が存在しない場合エラー
if (!targetOrder) {
throw new AlreadyLicenseStatusChangedError(
`Cancel issue is failed. Already license order status changed. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// キャンセル可能な日付発行日から14日経過かどうかに判定する時刻を取得する
const currentDateWithoutTime = new DateWithZeroTime();
const issuedDateWithoutTime = new DateWithZeroTime(targetOrder.issued_at);
const timeDifference =
currentDateWithoutTime.getTime() - issuedDateWithoutTime.getTime();
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
// 発行日から14日経過しているかをチェック
if (daysDifference > LICENSE_EXPIRATION_THRESHOLD_DAYS) {
throw new CancellationPeriodExpiredError(
`Cancel issue is failed. Cancellation period expired. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// すでに割り当て済みライセンスを含む注文か確認する
const licenseRepo = entityManager.getRepository(License);
const allocatedLicense = await licenseRepo.findOne({
where: {
order_id: targetOrder.id,
status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
},
});
// 存在した場合エラー
if (allocatedLicense) {
throw new AlreadyLicenseAllocatedError(
`Cancel issue is failed. Already license allocated. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// 更新用の変数に値をコピー
const updatedOrder = { ...targetOrder };
// 注文を発行待ちに戻す
updatedOrder.issued_at = null;
updatedOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
await orderRepo.save(updatedOrder);
// 発行時に削除したライセンスを未割当に戻す
await licenseRepo.update(
{ delete_order_id: targetOrder.id },
{
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
deleted_at: null,
delete_order_id: null,
},
);
// 発行時に発行されたライセンスを削除する
await licenseRepo.delete({ order_id: targetOrder.id });
});
}
} }

View File

@ -24,3 +24,12 @@ export class LicenseAlreadyDeallocatedError extends Error {}
// 注文キャンセル失敗エラー // 注文キャンセル失敗エラー
export class CancelOrderFailedError extends Error {} export class CancelOrderFailedError extends Error {}
// ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
export class AlreadyLicenseStatusChangedError extends Error {}
// ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
export class CancellationPeriodExpiredError extends Error {}
// ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
export class AlreadyLicenseAllocatedError extends Error {}

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm'; import { DataSource, In } from 'typeorm';
import { import {
LicenseOrder, LicenseOrder,
License, License,

View File

@ -0,0 +1,29 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
UpdateDateColumn,
CreateDateColumn,
} from 'typeorm';
@Entity({ name: 'option_items' })
export class OptionItem {
@PrimaryGeneratedColumn()
id: number;
@Column()
worktype_id: number;
@Column()
item_label: string;
@Column()
default_value_type: string;
@Column()
initial_value: string;
@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,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OptionItem } from './entity/option_item.entity';
import { OptionItemsRepositoryService } from './option_items.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([OptionItem])],
providers: [OptionItemsRepositoryService],
exports: [OptionItemsRepositoryService],
})
export class OptionItemsRepositoryModule {}

View File

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

View File

@ -1 +1,6 @@
// WorktypeID重複エラー
export class WorktypeIdAlreadyExistsError extends Error {}
// WorktypeID登録上限エラー
export class WorktypeIdMaxCountError extends Error {}
// WorktypeID不在エラー
export class WorktypeIdNotFoundError extends Error {}

View File

@ -1,6 +1,17 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm'; import { DataSource, Not } from 'typeorm';
import { Worktype } from './entity/worktype.entity'; import { Worktype } from './entity/worktype.entity';
import {
OPTION_ITEM_NUM,
OPTION_ITEM_VALUE_TYPE,
WORKTYPE_MAX_COUNT,
} from '../../constants';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from './errors/types';
import { OptionItem } from '../option_items/entity/option_item.entity';
@Injectable() @Injectable()
export class WorktypesRepositoryService { export class WorktypesRepositoryService {
@ -19,4 +30,109 @@ export class WorktypesRepositoryService {
return worktypes; return worktypes;
}); });
} }
/**
*
* @param accountId
* @param worktypeId
* @param [description]
*/
async createWorktype(
accountId: number,
worktypeId: string,
description?: string,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const worktypeRepo = entityManager.getRepository(Worktype);
const optionItemRepo = entityManager.getRepository(OptionItem);
const duplicatedWorktype = await worktypeRepo.findOne({
where: { account_id: accountId, custom_worktype_id: worktypeId },
});
// ワークタイプIDが重複している場合はエラー
if (duplicatedWorktype) {
throw new WorktypeIdAlreadyExistsError(
`WorktypeID is already exists. WorktypeID: ${worktypeId}`,
);
}
const worktypeCount = await worktypeRepo.count({
where: { account_id: accountId },
});
// ワークタイプの登録数が上限に達している場合はエラー
if (worktypeCount >= WORKTYPE_MAX_COUNT) {
throw new WorktypeIdMaxCountError(
`Number of worktype is exceeded the limit. MAX_COUNT: ${WORKTYPE_MAX_COUNT}, currentCount: ${worktypeCount}`,
);
}
// ワークタイプを作成
const worktype = await worktypeRepo.save({
account_id: accountId,
custom_worktype_id: worktypeId,
description: description,
});
// ワークタイプに紐づくオプションアイテムを10件作成
const newOptionItems = Array.from({ length: OPTION_ITEM_NUM }, () => {
const optionItem = new OptionItem();
optionItem.worktype_id = worktype.id;
optionItem.item_label = '';
optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT;
optionItem.initial_value = '';
return optionItem;
});
await optionItemRepo.save(newOptionItems);
});
}
/**
*
* @param accountId
* @param id ID
* @param worktypeId
* @param [description]
*/
async updateWorktype(
accountId: number,
id: number,
worktypeId: string,
description?: string,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const worktypeRepo = entityManager.getRepository(Worktype);
const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id },
});
// ワークタイプが存在しない場合はエラー
if (!worktype) {
throw new WorktypeIdNotFoundError(`Worktype is not found. id: ${id}`);
}
const duplicatedWorktype = await worktypeRepo.findOne({
where: {
account_id: accountId,
custom_worktype_id: worktypeId,
id: Not(id),
},
});
// ワークタイプIDが重複している場合はエラー
if (duplicatedWorktype) {
throw new WorktypeIdAlreadyExistsError(
`WorktypeID is already exists. WorktypeID: ${worktypeId}`,
);
}
// ワークタイプを更新
worktype.custom_worktype_id = worktypeId;
worktype.description = description;
await worktypeRepo.save(worktype);
});
}
} }

View File

@ -26,10 +26,11 @@ services:
cache: cache:
image: redis:latest image: redis:latest
container_name: redis-cache container_name: redis-cache
command: [ "redis-server", "--requirepass ${REDIS_PASSWORD}" ]
ports: ports:
- 8000:8000 - 6379:6379
expose: expose:
- 8000 - 6379
networks: networks:
- network - network
volumes: volumes: