Compare commits

...

244 Commits

Author SHA1 Message Date
shimoda.m
3d9a254b63 2025/1/27 PH1エンハンス 本番リリース
メンテナンス告知文削除
2025-01-24 04:04:55 +00:00
shimoda.m
ff5533b647 Merged PR 1010: 2025/1/27 本番リリース 2025-01-21 05:23:54 +00:00
shimoda.m
b3845187f6 Merged PR 1009: Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"
Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"

Reverted commit `b5293888`.

デプロイミスによる切り戻し
2025-01-21 04:47:21 +00:00
shimoda.m
019c818a19 Merged PR 1008: Revert "2025/1/27 PH1エンハンス 本番リリース
Revert "2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除"

Reverted commit `1137d826`.

デプロイミスによる切り戻し。
2025-01-21 04:46:02 +00:00
shimoda.m
1137d826ae 2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除
2025-01-21 04:00:54 +00:00
shimoda.m
b529388871 Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース 2025-01-21 02:59:31 +00:00
shimoda.m
aef17893d9 Merged PR 1004: 2025/1/27 本番リリース メンテナンス文言追加
## 概要

2025/1/27の本番リリースに向けて、トップページにメンテナンス文言を追加

リテラルは以下で確認中。(日付を変えただけなので問題ない認識)
OMDS_IS-459 【リリース】エンハンスリリース(1/27)のスケジュールについて

## UIの変更内容
![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/1004/attachments/image.png)
2025-01-21 01:52:47 +00:00
nik.n
4f598b0017 Merged PR 988: pipelineエラー解消のためglobal npmアップデート削除
### 概要
Dockerfileから `RUN npm install -g npm` 削除。Node.js(v18.17.1)とnpm(11.0.0)のバージョン互換性の問題により、pipeline が失敗していたためです。
現在のNode.jsイメージに含まれている npmバージョン(9.6.7)でプロジェクトの要件は満たされているため、グローバルnpmのアップグレードは不要と判断した

### 参考リンク
失敗した Pipeline ビルド
- [#1931 • Merged PR 986: 保守対応の内容反映](https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_build/results?buildId=1931&view=results)
2025-01-08 04:36:06 +00:00
SAITO-PC-3\saito.k
b71ec627d7 特別な文字列をエスケープしてからreplaceAllするように修正 2024-12-11 14:07:39 +09:00
SAITO-PC-3\saito.k
af56f8ccad Supportページにリンク追加。
画面デザイン修正
2024-12-04 10:07:12 +09:00
SAITO-PC-3\saito.k
0b451ed62f NotificationHub登録方法修正 2024-11-27 11:16:46 +09:00
saito.k
11395279af Merged PR 954: NotificationHubへのデバイス登録方法を修正
## 概要
[Task4580: NotificationHubへのデバイス登録方法を修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4580)

- NotificationHubのデバイス登録時に付与するInstallationIDをuuidからuserIdに変更

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 修正範囲がNotificationHubのserviceに閉じているため、既存のテストが通ることを確認したのみ

## 補足
- 相談、参考資料などがあれば
2024-11-26 08:45:27 +00:00
x.itou.t
a07cfe51aa Merged PR 948: PH1エンハンス先行リリース対応
## 概要
[ユーザー ストーリー 4489: 【PH1エンハンス】Dictation Finishedになったファイルのステータスを変更したい](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4489)
[ユーザー ストーリー 4491: 【PH1エンハンス】通知にユーザーIDを付加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4491)
2024-11-11 05:23:38 +00:00
saito.k
ad397f6fe7 Merged PR 933: Functions修正
## 概要
[Task4557: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4557)

- 国情報を追加
- テスト修正

## レビューポイント
- 特になし

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

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

## 補足
- 相談、参考資料などがあれば
2024-10-16 07:40:27 +00:00
saito.k
85fdec2e5a Merged PR 922: Functions
## 概要
[Task4485: Functions](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4485)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-25 01:06:33 +00:00
saito.k
f1b75a7ff0 Merged PR 921: API修正
## 概要
[Task4478: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4478)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-18 01:35:28 +00:00
saito.k
6690302ac3 Merged PR 920: API修正
## 概要
[Task4336: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4336)

- 文字起こし完了時のメールを文字起こし担当のTypistに送信しないようにする

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- ほかのテストケースがすべて通ることを確認
- メール送信処理を確認するテストケースを追加

## 補足
- 相談、参考資料などがあれば
2024-08-09 07:47:33 +00:00
saito.k
1320222d79 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 06:13:40 +00:00
saito.k
baea8ce5e5 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:57:41 +00:00
saito.k
d69126a980 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:55:36 +00:00
SAITO-PC-3\saito.k
79a2b9f0a3 yml修正 2024-08-09 13:42:37 +09:00
SAITO-PC-3\saito.k
2a755f2bd3 使用するVM修正 2024-08-09 12:27:25 +09:00
SAITO-PC-3\saito.k
edb8a79f94 docker-composeをダウンロードするymlに修正 2024-08-09 12:24:00 +09:00
SAITO-PC-3\saito.k
4a5136deee yml修正3 2024-08-09 11:47:36 +09:00
SAITO-PC-3\saito.k
f2eaba7e5f yml修正2 2024-08-09 11:39:51 +09:00
SAITO-PC-3\saito.k
3b576c6a47 パイプラインのYamlを修正 2024-08-09 11:36:06 +09:00
saito.k
557fc48d05 Merged PR 919: 画面修正
## 概要
[Task4331: 画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4331)

- サポートページにアプリダウンロードリンクを追加

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- サポートページのみで他画面に影響なし

## 補足
- 相談、参考資料などがあれば
2024-08-09 01:23:21 +00:00
saito.k
353d5ad462 Merged PR 918: analysisLicenses修正
## 概要
[Task4313: analysisLicenses修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4313)

- ライセンス推移CSVの出力内容を修正
  - 移行ライセンスの利用状況も出力する
  - 出力する移行ライセンスの条件は以下
     - 第五階層
     - 割り当て済
     - 有効期限が当日以降
     - 年間ライセンス
- テスト修正
  - 移行ライセンスの取得・変換ロジックのテスト修正

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-07-23 03:35:55 +00:00
saito.k
df55be5e19 Merged PR 917: 画面遷移してもインターバルが初期化されないように修正
## 概要
[Task4248: 画面遷移してもインターバルが初期化されないように修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4248)

- UpdateTokenTimerを修正
  - F5キーリロードの時もトークンをチェックする
- UpdateTokenTimerを各画面ごとではなくApp.tsxに配置する
  - 画面遷移のたびにSetIntervalが初期化されるのを防ぐ
- 各画面のUpdateTokenTimerを削除
  - App.tsxで管理するため

## レビューポイント
- いかの問題が解消されるかを可能であれば確認してほしい。
  - Dictation画面でトークンのチェック~自動更新ができること
  - 画面遷移をしてもトークンチェックのインターバルが初期化されないこと
  - F5キーリロードをした時にトークンチェックが走ること

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 各画面で取得系の処理が正しく動作することを確認

## 補足
- 相談、参考資料などがあれば
2024-07-02 06:43:41 +00:00
SAITO-PC-3\saito.k
35425c576a ローカル環境のDBスキーマの参照先を変更 2024-06-14 16:14:11 +09:00
SAITO-PC-3\saito.k
d4abc0fcfb スキーマを変更 2024-06-14 11:57:28 +09:00
SAITO-PC-3\saito.k
11c0937214 メンテナンス告知を削除 2024-06-14 11:54:27 +09:00
SAITO-PC-3\saito.k
12fe7ac9bb 本番環境で参照するスキーマはomdsのため参照先を修正 2024-06-13 17:06:53 +09:00
SAITO-PC-3\saito.k
b997b928b8 Merge branch 'release-ccb' into hotfix-task4228 2024-06-13 16:45:30 +09:00
SAITO-PC-3\saito.k
10f57c6aeb Merge branch 'develop' into release-ccb 2024-06-12 14:50:18 +09:00
SAITO-PC-3\saito.k
254cbdf0d6 リテラル反映漏れ修正 2024-06-12 14:04:16 +09:00
SAITO-PC-3\saito.k
6b1286ba13 Merge branch 'develop' into release-ccb 2024-06-11 17:00:35 +09:00
saito.k
aea66f4616 Merged PR 914: ユーザー削除API修正
## 概要
[Task4223: ユーザー削除API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4223)

- Authorの削除条件の修正
  - Authorが作成したタスクがある場合は削除できない
- Typistの削除条件の修正
  - 文字起こし担当のタスクがある場合は削除できないように修正
    - 挙動は変わらない(記述を修正した)
- リテラル反映
- テストケース追加

## レビューポイント
- テストケースは足りているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- 64行目のクエリ(Author/Typistともに)
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/Task4223?csf=1&web=1&e=UsjxWP

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - Authorが作成したタスクがある場合にそのAuthorは削除できないことを確認(タスクのステータスはFinished・Backup)
    - AuthorのAuthorIDが設定されているタスクがある場合はそのAuthorは削除できないことを確認(すべてのステータス)
    - Typistが文字起こし担当のタスクがある場合そのTypistは削除できないことを確認(タスクのステータスはFinished・Backup)

## 補足
- 相談、参考資料などがあれば
2024-06-11 05:05:53 +00:00
SAITO-PC-3\saito.k
c73340ec51 Merge branch 'develop' into release-ccb 2024-06-11 11:20:34 +09:00
saito.k
07904c35e6 Merged PR 885: メンテナンス通知画面実装
## 概要
[Task4148: メンテナンス通知画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4148)

- TOPページにメンテナンス通知を追加

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Hotfix4148?csf=1&web=1&e=aGsZP6

## 動作確認状況
- ローカルで確認
- TOPページに文言を追加したのみ
  - 画面サイズを小さくしてもデザインが崩れないことを確認

## 補足
- メンテナンスモードに入る日時については未定のため、OMDS定例で決まり次第修正し、決められた日時にmainにマージする
2024-06-11 01:24:52 +00:00
saito.k
474cdd56c6 Merged PR 913: ライセンス取得APIの修正
## 概要
[Task4220: ライセンス取得APIの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4220)

- 割り当て可能ライセンス取得APIを修正
  - 有効期限が当日のライセンスは割り当て可能としないように修正
    - 有効期限が翌日以降のライセンスを取得
- テスト修正
- ライセンス割り当ての処理にコメント追加
  - 結果として有効期限が当日のライセンスは割り当てられないので問題はないが、記述方法が紛らわしいのでコメントを追加した。

## レビューポイント
- テストケースは足りているか
- 追加したコメントの意図は伝わるか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- クエリ部分を修正したが、基準となる日付を変更したのみなので、クエリ自体に変更はなし
  - Before/Afterで確認したが、変更はなかった

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - テストを追加して、デグレがないことを検証した

## 補足
- 相談、参考資料などがあれば
2024-06-10 02:31:35 +00:00
saito.k
83732fa1b5 Merged PR 911: 画面修正(ファイル名変更ボタンのデザイン)
## 概要
[Task4213: 画面修正(ファイル名変更ボタンのデザイン)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4213)

- ファイル名変更ボタンのデザイン修正
  - 言語を英語以外にすると、ファイル名の入力欄と重なってしまうのを修正
    - ボタンの位置を入力欄の下に修正
- リテラル修正も含めて対応
  - https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2036-1?workitem=4214

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4213?csf=1&web=1&e=ftBwWm

## 動作確認状況
- ローカルで確認
- デザインの修正のみのためデグレは発生しない

## 補足
- 修正したデザインに対してはOMDS様に確認済み
2024-06-06 07:36:17 +00:00
saito.k
6b1650a634 Merged PR 910: メール文面修正&ユーザー一括登録のテンプレートファイル修正
## 概要
[Task4190: メール文面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4190)
[Task4191: ユーザー一括登録のテンプレートファイル修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4191)

- ユーザー一括追加時に送信するメールの内容を修正
  - SCVとなっていたところをCSVに修正
- ユーザー一括追加用のエクセルの項目名を画面上の項目名と合わせる
  - auto_renewとなっていたところをauto_assignに修正

## レビューポイント
- 修正内容の認識は合っているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - CSV変換のテストが通ることを確認
    - メール内容はdev動作確認で確認

## 補足
- 相談、参考資料などがあれば
2024-06-04 06:54:42 +00:00
makabe.t
4f7d65f0e8 Merged PR 905: POST /accounts/worktypes/{id}
## 概要
[Task3986: POST /accounts/worktypes/{id}](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3986)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-06-04 04:25:54 +00:00
SAITO-PC-3\saito.k
1f10a3f096 Merge branch 'develop' into release-ccb 2024-06-04 13:07:39 +09:00
saito.k
909c2a6d55 Merged PR 909: 画面修正(CSVファイルの変換処理)
## 概要
[Task4202: 画面修正(CSVファイルの変換処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4202)

- CSVデータの変換関数を修正
  - author_id,encryption_passwordが数値のみの場合でも、文字列として扱うようにする

## レビューポイント
- ほかに確認したほうが良いテストケースはあるか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存のテストはすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-06-03 06:36:05 +00:00
saito.k
37666a00c7 Merged PR 908: API修正(タスク削除条件のステータスチェックを修正)
## 概要
[Task4201: API修正(タスク削除条件のステータスチェックを修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4201)

- タスク削除不可条件を修正
  - Pendingの時も削除できないようにする

## レビューポイント
- 修正の認識があっているか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - タスクの各ステータスで削除可否を確かめられるように修正

## 補足
- 相談、参考資料などがあれば
2024-06-03 05:39:53 +00:00
saito.k
b7925f311b Merged PR 907: 画面修正(タスク削除ボタンの活性条件を修正)
## 概要
[Task4200: 画面修正(タスク削除ボタンの活性条件を修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4200)

- タスク削除ボタンの活性条件を修正
  - Pendingの時は削除できないようにする

## レビューポイント
- 特になし

## UIの変更
- https://ndstokyo.sharepoint.com/:i:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4200/Pending%E5%89%8A%E9%99%A4%E4%B8%8D%E5%8F%AF.png?csf=1&web=1&e=ATzEZ8

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - Inprogressのステータスで削除できないこと
  - Uploaded・Finished・backupで削除できる

## 補足
- 相談、参考資料などがあれば
2024-06-03 05:07:08 +00:00
SAITO-PC-3\saito.k
97620881d3 Merge branch 'develop' into release-ccb 2024-05-31 14:05:54 +09:00
saito.k
0e68f26c57 Merged PR 906: ユーザー認証API修正
## 概要
[Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182)

- 認証済みチェックをパスワード変更より先に行うように修正
- パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加
  - リカバリに失敗したら手動復旧ログを出力
- メール送信に失敗したらエラーを返すように修正
  - メール送信に失敗したらリカバリ処理を行うように修正
    - リカバリに失敗したら手動復旧ログを出力
- テスト修正
  - リカバリ処理を考慮したケースを追加

## レビューポイント
- リカバリ処理の記述
- メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか
  - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる
    - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。

## クエリの変更
- クエリの変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストケースをDBを使うテストに置き換え
    - 結果は変えずに通ることを確認
  - テストケースを追加し、新たな観点でテストを作成

## 補足
- 相談、参考資料などがあれば
2024-05-30 00:18:59 +00:00
makabe.t
9736d23653 Merged PR 904: POST /accounts/worktypes
## 概要
[Task3985: POST /accounts/worktypes](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3985)

- `POST /accounts/worktypes`のリクエストバリデータにテストを追加しました。

## レビューポイント
- テストケースに不足はないか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの追加のみなので影響なし
2024-05-28 05:58:17 +00:00
makabe.t
261a5afbdd Merged PR 903: POST /notification/register
## 概要
[Task3983: POST /notification/register](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3983)

- `POST /notification/register`APIのバリデータに対するテストを追加しました。

## レビューポイント
- テストケースに不足はないか。

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの追加のみなのでほかに影響なし。
2024-05-28 05:38:33 +00:00
SAITO-PC-3\saito.k
3a7c1765ee Merge branch 'main' into develop
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-05-24 17:26:50 +09:00
SAITO-PC-3\saito.k
c16a8b023c Merge branch 'develop' into release-ccb 2024-05-24 16:17:03 +09:00
saito.k
4b16e6a004 Merged PR 900: 画面修正(フッターに規約リンク追加)
## 概要
[Task4084: 画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4084)

- フッターに規約リンクを追加
  - デザインはOMDS様に確認済み
- サポートページにフッターを追加

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Hotfix4084?csf=1&web=1&e=bQBkQI

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-24 05:23:26 +00:00
saito.k
bd8f035c46 Merged PR 902: POST /files/audio/upload-finished
## 概要
[Task3982: POST /files/audio/upload-finished](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3982)

- /files/audio/upload-finished のバリデーションテスト作成

## レビューポイント
- テストケースで抜けているところはないか
  - IsNotEmpty()は一つに省略

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - テスト追加のため既存機能に影響なし

## 補足
- 相談、参考資料などがあれば
2024-05-23 07:26:04 +00:00
makabe.t
bdcce37b5a Merged PR 898: 対応
## 概要
[Task4174: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4174)

- アカウント情報画面からディーラーを変更する際に、同名ディーラーがいてもそれぞれ別個に指定できるようにIDで検索するように修正しました。

## レビューポイント
- 対応方針は認識通りでしょうか?

## UIの変更
- なし

## クエリの変更
- クライアントのみ変更なのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカルで同名ディーラーでそれぞれを指定して変更できることを確認
2024-05-23 01:12:31 +00:00
SAITO-PC-3\saito.k
7af5c50a27 Merge branch 'develop' into release-ccb 2024-05-20 13:57:26 +09:00
saito.k
5860da285e Merged PR 899: 階層構造変更API修正
## 概要
[Task4179: 階層構造変更API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4179)

- 階層構造変更API修正
  - 第四⇔第五の切り替え処理の時は国・リージョンの一致を確認しない
  - 不要なエラーコード・エラーハンドリングを削除
- テスト修正

## レビューポイント
- 修正箇所の認識は合っているか
- テストケースは足りているか

## 動作確認状況
- ローカルで確認
  - 国の違うDealerに付け替える処理を行い成功した
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 国の違う第四⇔第五間の付け替えのテストが成功することを確認
    - ほかのテストが成功することを確認

## 補足
- 相談、参考資料などがあれば
2024-05-17 06:49:26 +00:00
SAITO-PC-3\saito.k
3ece576476 Merge branch 'develop' into release-ccb 2024-05-16 14:06:54 +09:00
SAITO-PC-3\saito.k
4dac420bed typeORMのモジュール設定が漏れていたので修正 2024-05-15 17:00:20 +09:00
makabe.t
ddf338bc72 Merged PR 890: 翻訳情報反映
## 概要
[Task4058: 翻訳情報反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4058)

- 6月末リリース分の翻訳をクライアントに適用しました。
  - いただいた翻訳内容は開発の管理ファイルに反映しています。(メンテナンスについては未反映です。)
[ラベル・メッセージ管理](https://sonyjpn.sharepoint.com//r/sites/S127-OMDS-EXTERNAL-NDS/_layouts/15/doc2.aspx?sourcedoc=%7BCBFBB1F9-3AB4-4E2E-A21D-572684D20B29%7D&file=%E3%83%A9%E3%83%99%E3%83%AB%E3%83%BB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E7%AE%A1%E7%90%86_dictation.xlsx&action=default&mobileredirect=true)

## レビューポイント
- 翻訳の適用は適切か?

## UIの変更
- 翻訳の適用

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - リテラルの内容のみなので影響なし
2024-05-15 07:15:35 +00:00
saito.k
0cca61517c Merged PR 896: バージョンアップ用SQLを作成
## 概要
[Task4044: バージョンアップ用SQLを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4044)

- jobNumberの初期値を設定するマイグレーションファイル作成
  - タスクテーブルにレコードがある(=タスクを作成したことがある)アカウントに対しては最新のJobNumberで初期値をセットする
  - タスクテーブルにレコードがない(=タスク作成をしたことがない)アカウントに対しては`00000000`をセットする

## レビューポイント
- セットする初期値は認識あっているか
- migrate downの処理は問題ないか

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - マイグレーションファイルの作成のみなのでほかに影響はない想定

## 補足
- 相談、参考資料などがあれば
2024-05-15 06:15:22 +00:00
saito.k
fe5e8b8e1c Merged PR 895: API修正(アカウント作成系)
## 概要
[Task4043: API修正(アカウント作成系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4043)

- アカウント作成時にJobNumberの初期値を設定するように修正
- パートナーアカウント作成時にJobNumberの初期値を設定するように修正
- リカバリ処理にJobNumberのレコード削除を追加
- テスト修正

## レビューポイント
- JobNumber作成処理の追加する箇所に問題はないか
- テストケースに不足はないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- 既存のクエリに修正はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存テストが通ることを確認
    - パートナーアカウント作成のテストにメール送信内容のチェックを追加
    - ソート条件が作成・削除されていることを確認するテストを追加

## 補足
- 相談、参考資料などがあれば
2024-05-14 07:18:57 +00:00
saito.k
dfdc6a33ad Merged PR 894: API修正(アカウント削除系)
## 概要
[Task4034: API修正(アカウント削除系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4034)

- アカウント削除時にJobNumberテーブルのレコードも削除するように修正
- パートナー削除時にJobNumberテーブルのレコードも削除するように修正
- テスト修正

## レビューポイント
- テストケースに不足はないか
- ジョブナンバーテーブルの削除順に問題はないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- JobNumberテーブルのレコードを削除する処理を追加した
  - 既存のクエリに影響はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストが通ることを確認
  - テストしていなかった観点(ソート条件も削除されているか等)も確認するように修正

## 補足
- 相談、参考資料などがあれば
2024-05-14 02:12:41 +00:00
saito.k
35e2d626a0 Merged PR 893: API修正(upload-finished)
## 概要
[Task4033: API修正(upload-finished)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4033)

- JobNumberテーブルから取得したJOBNUMBERを使用してタスクを作成する。
- テスト追加

## レビューポイント
- テストケースは足りているか
- JOBNUMBERの採番ロジックに誤りはないか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
-  JobNumberテーブルからの取得と更新クエリを追加した
  - 既存のクエリを変更はしていない

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストケースを修正し、既存テストがすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-05-13 05:04:16 +00:00
saito.k
228e21ba78 Merged PR 892: migration修正
## 概要
[Task4035: migration修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4035)

- job_numberテーブル作成
- マイグレーションのコマンド修正

## レビューポイント
- インデックス・ユニーク制約・外部キー制約の認識は合っているか
- マイグレーションのコマンドは基本的にccbで認識あっているか

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - マイグレーションファイル作成のみでほかに影響なし

## 補足
- 相談、参考資料などがあれば
2024-05-10 03:56:48 +00:00
makabe.t
68df7cd728 Merged PR 887: POST /auth/token
## 概要
[Task3981: POST /auth/token](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3981)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- `POST /auth/token` のバリデータのUTを追加しました。

## レビューポイント
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの修正のみなので影響なし
2024-05-09 05:28:03 +00:00
makabe.t
ffd6eb4e68 Merged PR 886: GET /tasks
## 概要
[Task3980: GET /tasks](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3980)

- `GET /tasks` のバリデータのUTを追加しました。

## レビューポイント
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストの修正のみなので影響なし
2024-05-09 01:20:57 +00:00
SAITO-PC-3\saito.k
e205209b12 Merge branch 'develop' into release-ccb 2024-05-08 16:15:14 +09:00
SAITO-PC-3\saito.k
3ea84a3597 Merge branch 'main' into develop 2024-05-08 15:55:30 +09:00
saito.k
b71c4398d2 Merged PR 891: 不具合対応
## 概要
[Task4163: 不具合対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4163)

- ビルド成果物にメール用のテンプレートも含めるように修正

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - コードの修正はなし

## 補足
- 相談、参考資料などがあれば
2024-05-08 05:41:20 +00:00
makabe
c26ad130ed Merge branch 'develop' into release-ccb 2024-05-07 16:49:42 +09:00
makabe.t
279a9ab037 Merged PR 889: function修正
## 概要
[Task4164: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4164)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-07 07:11:39 +00:00
SAITO-PC-3\saito.k
4e00c03ef2 Merge branch 'main' into develop 2024-05-07 15:42:39 +09:00
saito.k
c813ddc0ac Merged PR 888: 対応
## 概要
[Task4160: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4160)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-05-07 05:37:16 +00:00
makabe
23f8b54011 Merge branch 'main' into develop 2024-05-07 12:04:03 +09:00
makabe.t
8122f6f4e1 Merged PR 884: FunctionにX-Requested-Withヘッダを適用
## 概要
[Task4142: FunctionにX-Requested-Withヘッダを適用](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4142)

- Functionのユーザー一括登録の処理中でOMDS CloudのAPIを呼び出す処理があるので、X-Requested-Withヘッダを適用しました。
- 一括登録失敗時のメール文面の翻訳でエラーがない場合のメッセージが日本語のままになっていたので各言語に対応しました。

## レビューポイント
- ヘッダの適用は適切でしょうか?
- 翻訳の適用方法で、言語ごとに割り当てる内容を定数としていますが、文面の置き換え方法に問題はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカルからAPIを叩い検証証
2024-05-07 00:05:17 +00:00
SAITO-PC-3\saito.k
e76242bddd Functionsのテストで使用する環境変数を修正 2024-04-25 18:19:48 +09:00
saito.k
af0ba78ae9 Merged PR 883: Functions修正
## 概要
[Task4132: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4132)

- 自動割り当てを行うライセンスの取得条件を変更
  - 有効期限が近いライセンスまたは有効期限が設定されていないライセンス(新規ライセンス)を取得する
  - 有効期限が近いものから割り当てを行うので、ソートはサーバー側で行うようにした
- メール送信処理を追加

## レビューポイント
- テンプレート取得からメール送信までの実装で漏れはないか
- テストケースは足りているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- 2行目で割り当てるライセンスを取得しているが、その条件を修正した
- https://ndstokyo.sharepoint.com/:u:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/task4132/after.log?csf=1&web=1&e=jh49c3

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 追加したメール送信処理を確認するように各テストを修正し、テストが通っている
  - テストの観点を拡充したうえでテストが通っていることを確認
    - ライセンス割り当て履歴の内容をより詳細に確認するようにした

## 補足
- 相談、参考資料などがあれば
2024-04-25 08:56:03 +00:00
makabe
1904b95adf Merge branch 'develop' into release-ccb 2024-04-24 11:35:58 +09:00
makabe.t
d43cece48a Merged PR 882: 追加開発分をステージングに反映するPipelineを作成
## 概要
[Task4016: 追加開発分をステージングに反映するPipelineを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4016)

- 追加開発分CCBをステージングに反映するPipeline定義のYamlファイルを追加しました。

## レビューポイント
- 参照元ブランチを変えた以外はSTGデプロイそのままにしていますが、対応の不足はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- マージ後に確認
2024-04-22 08:01:37 +00:00
SAITO-PC-3\saito.k
ba7196cac1 本番デプロイようにコメントアウトした実装をもとに戻す 2024-04-22 10:44:56 +09:00
SAITO-PC-3\saito.k
7eecb001c6 4/22の本番環境デプロイのために確認未実施の修正をコメントアウト 2024-04-21 22:41:44 +09:00
SAITO-PC-3\saito.k
0b01da936d CacheModule作成時にTTLを設定 2024-04-19 19:15:23 +09:00
saito.k
b88c0d9b96 Merged PR 877: ディーラー取得APIの修正
## 概要
[Task4104: ディーラー取得APIの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4104)

- メールの文面を各言語版に置き換えました。
  - 環境変数に設定されたアカウントIDのDealerはResponseに含めないように修正

## レビューポイント
- 環境変数からインスタンス変数に代入するときの処理に問題はあるか
- 環境変数のフォーマットはこれで良いか
  - もっとよいやり方があれば指摘いただきたいです
- テストケースに不足はないか

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ほかのテストに影響が出ていない
2024-04-19 04:47:32 +00:00
makabe
b7554e30ff CCBからDBの向き先を変更 2024-04-19 13:38:24 +09:00
makabe
b03cda3ccc Merge branch 'ccb' into develop 2024-04-19 11:17:17 +09:00
makabe.t
7bfd424a64 Merged PR 875: メールの多言語対応
## 概要
[Task3859: メールの多言語対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3859)

- メールの文面を各言語版に置き換えました。
  - アカウント情報消去完了通知 [U-111] 
  - ユーザー一括登録 受付通知 [U-120] 
  - ユーザー一括登録 完了通知 [U-121] 
  - ユーザー一括登録 失敗通知 [U-122] 
  - パートナーアカウント情報消去完了通知 [U-123] 

## レビューポイント
- 対応メールは適切でしょうか?
- メール文面は適切でしょうか?
- 反映内容は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカル確認、マージ後にdevelop動作確認
2024-04-19 02:14:05 +00:00
makabe
9ee29e91ba マイグレーションコマンド修正 2024-04-19 09:10:48 +09:00
SAITO-PC-3\saito.k
b24059b538 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
#	dictation_function/src/test/common/utility.ts
#	dictation_server/src/features/files/test/utility.ts
2024-04-18 09:57:37 +09:00
SAITO-PC-3\saito.k
a7b18d8151 Merge branch 'develop' 2024-04-17 14:02:05 +09:00
makabe.t
23862ad3ac Merged PR 874: 不具合修正
## 概要
[Task4137: 不具合修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4137)

- ファイル名変更時にダイアログを追加

## レビューポイント
- 共有
## UIの変更
- ダイアログを追加

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ローカル確認
2024-04-17 01:49:34 +00:00
saito.k
566da623bf Merged PR 873: dev動作確認不具合修正
## 概要
[Task4131: dev動作確認不具合修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4131)

- メール送信対象のアカウント取得条件を修正
  - 第五階層はDPAの規約に同意することがないため、accepted_dpa_versionは常にNULLとなる
    - 取得条件にaccepted_dpa_versionがNOTNULLを追加するとカラムに値が入る契機がないのでアカウントを取得できなくなってしまっていた。

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ほかのテストケースで使用しているユーザーデータをaccepted_dpa_versionはNULLの状態で作成するようにし、テストがすべて通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-04-17 01:01:31 +00:00
makabe.t
69241ed36c Merged PR 871: Function(音声ファイル自動削除)修正
## 概要
[Task4060: Function(音声ファイル自動削除)修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4060)

- 自動ファイル削除で生ファイル名を利用するように修正しUTを追従しました。

## レビューポイント
- 生ファイル名を使う方法は適切でしょうか?
- テストの修正は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- select対象をraw_file_nameに修正

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - テストが通ることを確認
2024-04-17 00:23:08 +00:00
makabe.t
c469f943f1 Merged PR 872: 生ファイル名の初期値を追加するマイグレーションファイル作成
## 概要
[Task4125: 生ファイル名の初期値を追加するマイグレーションファイル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4125)

- 生ファイル名と表示ファイル名をそれぞれ修正して初期値を入れるスクリプトを追加しました。
  - 生ファイル名:これまでfile_nameカラムに入っていた値
  - file_nameの値から.zipを除いたもの

## レビューポイント
- この値の操作で問題はないか

## UIの変更
- なし
## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - migrate up/downで想定通りになることを確認
2024-04-17 00:18:48 +00:00
makabe.t
0a714f8484 Merged PR 870: ファイル情報ポップアップ画面修正
## 概要
[Task4051: ファイル情報ポップアップ画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4051)

- ファイル情報ポップアップを修正し、ファイル名を変更できるように修正しました。
- 音声ファイル名変更APIのファイル名の文字数を修正しました。
  - フロントの入力欄で64文字にしたので、プラス拡張子で68文字としました。

## レビューポイント
- 画面イメージは認識通りでしょうか?
- 表示では拡張子を取って、APIに渡す際にはつけているのですが処理として不自然ではないでしょうか?

## UIの変更
- [Task4051](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task4051?csf=1&web=1&e=nbYIE0)

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - タスク一覧、ファイル情報ポップアップが正常に見えることを確認
2024-04-16 10:12:44 +00:00
makabe.t
1d2089b0c4 Merged PR 868: 音声ファイル名変更のに伴うAPI修正
## 概要
[Task4053: 音声ファイル名変更のに伴うAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4053)

- 音声ファイル名について生ファイル名を利用するように修正しました。
  - ファイルアップロード完了(タスク登録)
    - パラメータのファイル名で生ファイル名も登録
  - 音声ファイルダウンロード先取得
  - タスク削除
    - blobストレージのファイル名に生ファイル名を利用

## レビューポイント
- 対象APIは認識通りか

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - UTが通ることを確認
    - 各APIで生ファイル名を使って実行できることを確認
2024-04-16 05:24:18 +00:00
saito.k
f975ecf551 Merged PR 869: Functions修正
## 概要
[Task4085: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4085)

- メール送信対象のアカウント取得条件を修正
  - プライマリー管理者の規約同意用のカラムがNULLであった場合は、そのアカウントにはメール送信しない。
- Entityの`@Column`で設定する型が実際のパラメータの型と異なっていたため修正
  - 文字列のところをDateTimeとしていた

## レビューポイント
- 修正内容の認識あっているか
- テストケースは足りているか

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/4085?csf=1&web=1&e=WRec5O
  - 35行目に変更あり(規約系のカラムがNULLではないという条件を追加)
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテスト結果に影響なし

## 補足
- 相談、参考資料などがあれば
2024-04-16 05:08:04 +00:00
makabe.t
e6d27d7810 Merged PR 867: 音声ファイル名変更API実装
## 概要
[Task4052: 音声ファイル名変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4052)

- ファイル名変更APIとそのUTを実装しました。

## レビューポイント
- リポジトリ実装のチェック内容とその順序は適切でしょうか?
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - UT実行
    - ローカル実行

## 補足
- 相談、参考資料などがあれば
2024-04-15 06:52:07 +00:00
makabe.t
f209c7359e Merged PR 865: IF実装・修正
## 概要
[Task4049: IF実装・修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4049)

- 音声ファイル名変更APIのIFを実装してリクエストパラメータのテストを実装しました。
- タスク一覧取得APIのレスポンスに生ファイル名を追加しました。
- OpenAPIの更新

## レビューポイント
- ファイル名変更APIのパスは適切でしょうか?
- バリデータのチェックは適切でしょうか?

## UIの変更
- なし

## クエリの変更
- IFなのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 既存テストを実施して、タスク一覧についてはレスポンス期待値を修正。
    - タスク一覧画面が正常に見えることを確認
2024-04-12 01:36:49 +00:00
makabe.t
33d4ab3d2f Merged PR 863: (Sprint31対応)一部のentityでMySQL上の型がbigintのものに対してbigintTransformerで変換する処理が入っていない
## 概要
[Task3928: (Sprint31対応)一部のentityでMySQL上の型がbigintのものに対してbigintTransformerで変換する処理が入っていない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3928)

- 現在、bigint型で変換を適用していない部分へのTransformerの適用はうまくいかないため保留。
  - 既存処理については問題ありません
- Functionのテストで日付に依存している箇所があったので修正しています。

## レビューポイント
- 共有

## UIの変更
- なし
## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - テストの修正のみ
2024-04-11 04:29:16 +00:00
makabe.t
07bca1d638 Merged PR 866: DBマイグレーションエラー修正
## 概要
[Task4120: DBマイグレーションエラー修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4120)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-04-10 09:48:40 +00:00
makabe.t
09c21eafa7 Merged PR 864: DBにカラム追加
## 概要
[Task4057: DBにカラム追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4057)

- DBマイグレーションファイルとして音声ファイルテーブルに生ファイル名カラムを追加しました。

## レビューポイント
- カラムの要件は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルでmigrate:up/downできることを確認
2024-04-10 06:15:54 +00:00
makabe.t
e6d6e477d9 Merged PR 862: パートナー一覧画面&パートナー編集ポップアップ実装
## 概要
[Task3935: パートナー一覧画面&パートナー編集ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3935)

- パートナー一覧画面からパートナー編集ポップアップを表示して情報を変更できる画面実装をしています。

## レビューポイント
- エラーの表示は適切でしょうか?
- 画面イメージは認識通りでしょうか?

## UIの変更
- [Task3935](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3935?csf=1&web=1&e=FdaUMT)

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規機能なので問題なし
2024-04-08 07:41:05 +00:00
maruyama.t
915483c109 Merged PR 860: パートナー情報更新API実装
## 概要
[Task3937: パートナー情報更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3937)

- パートナーアカウント情報更新APIとUTを実装しました。

## レビューポイント
- エラーケースの出し分けは適切でしょうか?
- テストケースは過不足ないでしょうか?

## UIの変更
- なし

## クエリの変更
- 新規追加のため変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規追加なので問題なし。
2024-04-05 02:37:58 +00:00
SAITO-PC-3\saito.k
c600d9f818 Merge branch 'develop' 2024-04-03 18:51:31 +09:00
saito.k
5147f853ae Merged PR 861: API修正
## 概要
[Task4038: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4038)

- タスク作成時のジョブナンバーの採番ルールを修正
  - 常に最新タスクのジョブナンバー+1を採番する
- テストケース追加
- 非同期処理の呼び出しでawaitが抜けている箇所を修正

## レビューポイント
- 修正内容の認識は合っているか
- テストケースに不足はないか
- 修正箇所がほかの機能に影響していないか
  - taskRepositoryのcreateはupload-finished以外では使用されていない

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/Task4038?csf=1&web=1&e=WTIk2l
  - L107辺りが変更箇所

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - 修正したcreateがほかで使用していないことを確認
    - テストケースを追加し確認
    - 既存のテスト含め成功することを確認

## 補足
- 相談、参考資料などがあれば
2024-04-03 05:35:30 +00:00
maruyama.t
0288292058 Merged PR 859: パートナーユーザー取得API実装
## 概要
[Task3936: パートナーユーザー取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3936)
- このPull Requestでの対象/対象外
パートナー変更APIの修正は別タスクで対応
- 影響範囲(他の機能にも影響があるか)
新規APIのため他の機能に影響はない

## レビューポイント
- パートナーのアカウントIDからユーザー一覧を取得する際に、Repository層ではEmai認証状態を意識した取得は行わない
 →service層でフィルタリングする実装にしたが
 (アカウントIDからユーザー一覧を取得する処理がいままでなかったので、あったほうがいいかなと思い)

## クエリの変更
新規APIのためクエリの変更はない

## 動作確認状況
- ローカルで確認
UT+POSTMAN
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
既存機能には手を入れていない
## 補足
- 相談、参考資料などがあれば
2024-04-03 00:50:53 +00:00
maruyama.t
8752448eed Merged PR 858: エラーメッセージ修正
## 概要
[Task4013: エラーメッセージ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4013)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)
なし

## レビューポイント
- とくになし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
UIなのでなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
メッセージの修正のみなのでデグレなし

## 補足
- 相談、参考資料などがあれば
2024-03-26 11:30:30 +00:00
湯本 開
1d71bef7aa Merged PR 857: テスト追加
## 概要
[Task3977: テスト追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3977)

- エラー時にアラーム発報ログが出力されているかを確認するテストを追加
- contextを継承して、ログに吐き出す内容をstring型の配列として溜め込むテスト用のクラスを追加

## レビューポイント
- テストの内容は妥当か

## クエリの変更
- テストのみ追加なので変更なし

## 動作確認状況
- npm run testが通過したこと
2024-03-26 07:05:39 +00:00
maruyama.t
114ded790e Merged PR 855: API IF実装(パートナーを編集したい)
## 概要
[Task3930: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3930)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 新規追加API2本のIFを作成、controllerの返却値は仮実装(別タスクで実装)
- 影響範囲(他の機能にも影響があるか)
 新規追加のみなので影響はなし

## レビューポイント
- 特筆する点はありません

## UIの変更
なし

## クエリの変更
なし

## 動作確認状況
- ローカルで確認
 バリデーションテストとPOSTMANからの起動の確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
完全新規のIFの実装のみなのでデグレはない想定

## 補足
- 相談、参考資料などがあれば
2024-03-26 06:22:07 +00:00
Kentaro Fukunaga
ae638b16be Merge remote-tracking branch 'origin/develop' 2024-03-25 18:03:40 +09:00
湯本 開
133db833ee Merged PR 850: staging-pipelineのテスト実行修正
## 概要
[Task3948: staging-pipelineのテスト実行修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3948)

- docker-composeを使用したテストは `ubuntu-latest` で実行する必要があったため、pipelineを修正

## レビューポイント
- 問題がありそうな記述はあるか
- 実行順の依存関係等が壊れていないか

## 動作確認状況
- 未動作確認。4月以降、CCB対応時に動かして確認を想定。
2024-03-25 08:09:16 +00:00
Kentaro Fukunaga
1f0cf50166 Merged PR 856: Clientビルド時に埋め込んでいるキーやURLの更新
## 概要
[Task3963: Clientビルド時に埋め込んでいるキーやURLの更新](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3963)

- クライアント本番用のACB2C関連の環境変数を作り直したACB2Cの値に差し替えました。

## レビューポイント
- 共有

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- 未実施
- デプロイ後に確認

## 補足
- 相談、参考資料などがあれば
2024-03-25 07:54:43 +00:00
masaaki
eb6b413adb Merged PR 848: パートナー一覧画面修正
## 概要
[Task3833: パートナー一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3833)

- パートナー一覧画面のパートナーにカーソルを合わせた際に、Delete Partnerリンクが表示される。
押下した場合は、本当に削除するのかを確認するメッセージが表示され、どのPartnerを削除するかメッセージ欄に表示されること。

## レビューポイント
- 特筆する点はなし

## UIの変更
https://ndstokyo.sharepoint.com/sites/Piranha/Shared%20Documents/Forms/AllItems.aspx?csf=1&web=1&e=hzPw9b&cid=e8e0702d%2D3730%2D4295%2Dbb9d%2D40e6b1998906&FolderCTID=0x012000C0DCEE65AC2177479C3C761CD137C9C9&id=%2Fsites%2FPiranha%2FShared%20Documents%2FGeneral%2FOMDS%2F%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%2FTask3833&viewid=786a81cf%2Dd15f%2D4dc2%2D9e55%2Dc7a729fbc72f

## クエリの変更
なし

## 動作確認状況
- ローカルで確認(APIからの返却値を直接指定する方式で確認)
- 行った修正がデグレを発生させていないことを確認できるか
  新規機能追加のみなので問題なし

## 補足
- 相談、参考資料などがあれば
2024-03-22 07:50:58 +00:00
makabe.t
6e93a5be79 Merged PR 846: パートナーアカウント削除API実装
## 概要
[Task3834: パートナーアカウント削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3834)

- パートナーアカウント削除APIとUTを実装しました。

## レビューポイント
- 削除対象データは適切でしょうか?
- テストケースに不足はないでしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
  - テストとローカルで実行確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存処理への変更なし
2024-03-22 06:12:47 +00:00
湯本 開
ac3d523c0e Merged PR 826: Azure Functions実装(音声ファイル削除)
## 概要
[Task3880: Azure Functions実装(音声ファイル削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3880)

- 自動音声ファイル削除を実装
- 上記のテストを実装
- テストにMySQLを使用する仕組みを導入

## レビューポイント
- テストケースは十分か
- テスト内容は妥当か
- developにデプロイする前の動作確認・ユニットテストとして十分か

## クエリの変更
- 新規処理のため、既存からの変更はなし

## 動作確認状況
- DBが空の状態でローカル環境で実行し、0件削除のログが出ることを確認
  - 削除対象が正しいか等はdevelopでチェック予定
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存処理の変更はなし
2024-03-19 07:36:03 +00:00
Kentaro Fukunaga
75f0a49fc1 Merged PR 831: 親アカウント変更API実装
## 概要
[Task3853: 親アカウント変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3853)

- 親アカウント切り替えAPIを実装しました。

## レビューポイント
- Service層の関数の分け方に改善点ないか?
- テストケースで他にあったほうがいいものや観点などあるか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで全テスト通ることを確認
- 行った修正がデグレを発生させていないことを確認できるか
    - 新規APIの実装のため既存実装に変更なし
2024-03-18 05:47:24 +00:00
maruyama.t
cab7a75ec1 Merged PR 849: 有効なアカウント内の削除されたユーザーの割り当て履歴が集計されない
## 概要
[Task3929: 有効なアカウント内の削除されたユーザーの割り当て履歴が集計されない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3929)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-03-15 12:19:16 +00:00
SAITO-PC-3\saito.k
b8af6fb6b2 Merge branch 'develop' 2024-03-15 19:01:42 +09:00
saito.k
13e0793219 Merged PR 847: リテラルの修正
## 概要
[Task3927: リテラルの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3927)

- 翻訳反映
  - Auto Renew → Auto Assign に変更
  - Transcriptionist List→ Transcription List に変更

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - リテラルの反映のみのためほか機能に影響なし

## 補足
- 相談、参考資料などがあれば
2024-03-15 08:21:24 +00:00
masaaki
f80912c617 Merged PR 834: API IF実装
## 概要
[Task3904: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3904)

- 「プロダクト バックログ項目 1242: パートナーを削除したい」のAPI IFを作成しました
- 影響範囲(他の機能にも影響があるか)
  - 新規IFのため影響はなし

## レビューポイント
- controllerの試験実装が初なので、テストケース過不足ないか確認いただきたいです。

## UIの変更
- 無し

## クエリの変更
- 無し

## 動作確認状況
- ユニットテストが通ることを確認、ローカル環境でpostmanで呼び出せることを確認、SWAGGER UI上で追加されていることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ユニットテストが通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-15 07:41:56 +00:00
makabe.t
66c643677d Merged PR 829: 音声ファイルバックアップポップアップ修正
## 概要
[Task3882: 音声ファイルバックアップポップアップ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3882)

- 音声ファイルバックアップ中にファイル削除された場合の処理を追加しました。
  - ダウンロードで対象ファイルが削除されていた場合に特別なエラーとなるようにしています。
  - タスクバックアップで対象ファイルが削除されていた場合でも成功となるようにしています。

## レビューポイント
- 対応するエラーは適切でしょうか?

## UIの変更
- [Task3882](https://ndstokyo.sharepoint.com/:i:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3882/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%89%8A%E9%99%A4%E6%B8%88%E3%81%BF.png?csf=1&web=1&e=1BKVh8)

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - ダウンロード実行中にファイル削除
    - backupAPIからタスク不在エラーを返却して成功するか確認
2024-03-15 06:53:41 +00:00
makabe.t
1451d6f584 Merged PR 835: 本番環境に対する移行データの投入後の修正
## 概要
[Task3580: 本番環境に対する移行データの投入後の修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3580)

- 検証ツールの日時比較について、一度Date型に変換してから比較するように修正しました。
  - 時刻フォーマットで秒が入っていなくても比較できるようにするため

## レビューポイント
- 日時の変換は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- 本番踏み台で確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ツールの変更のみなので影響なし

## 補足
- 相談、参考資料などがあれば
2024-03-15 01:02:33 +00:00
Kentaro Fukunaga
5f4a05044b Merged PR 845: クライアントメッセージ変更(アカウント階層構造変更)
## 概要
[Task3917: クライアントメッセージ変更](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3917)

- メッセージレビューで変更あったため修正して自動生成しました。

## レビューポイント
- 情報共有
2024-03-15 00:53:58 +00:00
saito.k
02a4784e58 Merged PR 836: 翻訳情報の反映
## 概要
[Task3908: 翻訳情報の反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3908)

- 翻訳情報の反映
 - 修正したのは、licenseNotAssignedErrorのピリオドの後にスペースを入れた部分です。

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認、develop環境で確認など
- リテラルの修正だけのためほか機能への影響はなし

## 補足
- 相談、参考資料などがあれば
2024-03-15 00:15:01 +00:00
maruyama.t
9256566f89 Merged PR 839: DEV動作確認のバグ対応
## 概要
[Task3918: DEV動作確認のバグ対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3918)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-03-14 15:03:17 +00:00
makabe
9b61443b2f Merge branch 'develop' into ccb 2024-03-14 11:08:35 +09:00
makabe.t
5169092892 Merged PR 833: 本番環境に対する移行データの投入後の修正
## 概要
[Task3580: 本番環境に対する移行データの投入後の修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3580)

- デモライセンスの判定を日時から日付だけ見るように修正しました。
  - 9999/12/31で始まるかを見ています。
- メールアドレス重複のチェックは大文字小文字を区別せずに実行するようにしています。

## レビューポイント
- 対応箇所は適切でしょうか?
- 対応として先頭文字列を見ていますが適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- 本番踏み台で変換できることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ツールの変更だけなので問題なし
2024-03-14 01:46:57 +00:00
makabe
017276c94a Merge branch 'develop' into ccb 2024-03-14 09:10:06 +09:00
makabe.t
e96e8ea54a Merged PR 832: 本番環境に対する移行データの投入後の修正
## 概要
[Task3580: 本番環境に対する移行データの投入後の修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3580)

- メールアドレスの重複チェックについて、大文字小文字を区別せずに実行するように変換ツールを修正しました。

## レビューポイント
- メールアドレスチェックの対応箇所は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- 本番踏み台で確認
- 行った修正がデグレを発生させていないことを確認できるか
  - ツールのみの変更なので影響なし
2024-03-14 00:08:37 +00:00
maruyama.t
2b68a9f054 Merged PR 824: AzureFunctions実装3(CSVをストレージアカウントに配置する)
## 概要
[Task3846: AzureFunctions実装3(CSVをストレージアカウントに配置する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3846)

outputDataを追加
→outputAnalysisLicensesDataに変更(アラートルールなどでログを見るので、何の処理か理解できるように)
blobstorageService.tsに以下を追加
 - uploadFileAnalysisLicensesCSV
(ライセンスCSVを配置する)
- createContainerAnalysisを追加
(コンテナーを作成する)
環境変数の追加

## レビューポイント
- 今回追加されたJP-EASTのストレージアカウントのコンテナーが、第一階層のアカウントのものであるかどうかはソース上は特に意識していないが問題ないでしょうか。

## 動作確認状況
- ローカルで確認(モックでソース上処理が通ることのみ確認のみ)
詳細なテストは別タスクで行う。

## 補足
- 相談、参考資料などがあれば
2024-03-13 07:54:10 +00:00
Kentaro Fukunaga
83e297cc9b Merged PR 821: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)
## 概要
[Task3854: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3854)

- パートナーライセンス一覧に「Change Owner」ボタンを配置し、表示制御およびクリック時にポップアップ表示する処理の実装
- アカウント階層構造変更ポップアップの処理全体実装
- サーバー側のエラーコード定義

## レビューポイント
- 「一括」を表現するためのドロップダウンの構築や処理周りで改善点ないか(to:斎藤くん)
- コンポーネントでの状態管理でお作法に違反しているところないか(to:斎藤くん)
- 修正箇所がほかの機能に影響していないか
    - パートナーライセンス一覧の画面表示に何らか悪影響ないか?(to:ガンさん)

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3854?csf=1&web=1&e=jBGQrR

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認しました
    - 第一階層でログインしてかつ第三または第四視点での一覧を確認しているときにChangeOwnerボタンが表示される
    - ボタン押下すると、仕様通りにポップアップの表示が行われる
    - ポップアップにて入力項目に入力できる&バリデーション効いている
    - ポップアップにて実行ボタン押下するとAPI実行できる&処理結果に応じて仕様通りの挙動をすること
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - パートナーライセンス画面に新規ボタンを配置した&新規ポップアップの実装のみのため、
ポップアップでの処理が正常終了/失敗/何もせず閉じた場合に元の画面の表示が今まで通り動くことを確認済み。
2024-03-13 07:41:25 +00:00
makabe.t
9f5ccabb0c Merged PR 830: アカウント削除修正
## 概要
[Task3896: アカウント削除修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3896)

- アカウント削除を実行すると失敗するので、対応を実施しました。
  - `AccountsRepositoryModule`のインポートに`AccountArchive`を追加しました。

## レビューポイント
- 対応内容は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
  - npm run test
  - 削除実行
- 行った修正がデグレを発生させていないことを確認できるか
  - リポジトリのインポートへの追加のみなので対象の処理ができることとtestが通ることで確認
2024-03-13 02:01:28 +00:00
maruyama.t
415fd2eb58 Merged PR 825: AzureFunctions実装2(取得したデータをCSV用に変換する)
## 概要
[Task3844: AzureFunctions実装2(取得したデータをCSV用に変換する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3844)

transferData()の実装
ccbの最新よりマージした状態での引数に変更
各ライセンス数をCSV配列に出力できるところまでを実装
(実際にローカルにCSV出力して中身を確認済み)

![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/825/attachments/image.png)
※UIの変更ではない為、ここにそのまま張り付けさせていただきます。

## レビューポイント
- 関数化の範囲は適切か。
テストコードは最低限の記述になるが、問題ないか
(33行*7項目の突合せをコード上に実装するのは時間的余裕がないためやってない)
詳細な動作確認は、別タスク
https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2029-2?workitem=3861
で行います。
## 補足
- 相談、参考資料などがあれば
2024-03-12 05:43:49 +00:00
masaaki
340aa73bde Merged PR 827: CSV出力が失敗したときに手動で起動するためのFunctionsを作成
## 概要
[Task3860: CSV出力が失敗したときに手動で起動するためのFunctionsを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3860)

- analysisLicensesについて手動で起動できる処理(analysisLicensesManualRetry)を追加しました
- データベース接続の初期化処理について共通化しました

## レビューポイント
- 特筆する部分はありません

## UIの変更
- 無し

## クエリの変更
- 無し

## 動作確認状況
- ローカルでpostmanからリクエストを行うことで起動できることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - unit testが通ることを確認
    - タイマを暫定的に1分にして、ローカル環境で各タイマ処理が正常終了することを確認(DBアクセスが全処理行われることを確認)

## 補足
- 相談、参考資料などがあれば
2024-03-12 04:55:04 +00:00
湯本 開
43561f237e Merged PR 822: クエリ比較用ログ出力の仕組みを改良
## 概要
[Task3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3889)

- SQLクエリを比較可能とするための仕組みを導入
  - UUIDや日付等の実行の度に変更される要素を出力段階で置き換えてしまうロガーを追加
  - テストで上記ロガーを使うよう修正
- テストで使用する環境変数が必要がないのにlocalを指定するようになっていたため、production想定でテストが実施されるようテスト用環境変数ファイルを変更

## レビューポイント
- この仕組みを使ってデグレを防ぐための「クエリの変更内容を確認する」事が問題なく出来そうか

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- npm run test が正常に実施されることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - testファイルにしか参照されない変更なので、デグレは原理的に発生しないはず
  - testも正常に成功したことを確認したので、デグレは発生していないはず
2024-03-12 03:57:29 +00:00
SAITO-PC-3\saito.k
311eb98236 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
#	dictation_server/src/app.module.ts
2024-03-12 11:48:25 +09:00
SAITO-PC-3\saito.k
8b2613b9f6 Merge branch 'develop' 2024-03-11 19:37:37 +09:00
saito.k
071bd2b85e Merged PR 828: ヘルプページのリンクを差し替える+ヘルプページへの遷移リンクを一つにする
## 概要
[Task3887: ヘルプページのリンクを差し替える+ヘルプページへの遷移リンクを一つにする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3887)

- ヘルプページのリンクを修正
- リンクを一つにまとめる
- リンクの翻訳情報を修正

## レビューポイント
- 修正に漏れはないか

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3887?csf=1&web=1&e=NtVNCO

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - リンクの張替えと翻訳情報の変更のみで他機能に影響はない

## 補足
- 相談、参考資料などがあれば
2024-03-11 10:18:25 +00:00
saito.k
1ef696efe8 Merged PR 814: リンク差し替える(ヘルプページ以外)
## 概要
[Task3874: リンク差し替える(3/8Staging・本番デプロイ分)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3874)

- 以下のリンクを本物に差し替え
  - 利用規約
    - EULA
    - DPA
    - Privacy Notice
  - デスクトップアプリダウンロード

## レビューポイント
- 差し替える対象に漏れはないか

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3874?csf=1&web=1&e=eQE4hq

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - リンクの差し替えのみなのでほか機能に影響はない

## 補足
- ヘルプページのリンクはまだ受領していないのでほかタスクで対応
2024-03-11 07:54:35 +00:00
saito.k
c059a2eabd Merged PR 815: XMLHttpRequestにてリクエストヘッダー「X-Requested-With: XMLHttpRequest」をコメントアウトしていればコメントアウトはずす
## 概要
[Task1806: XMLHttpRequestにてリクエストヘッダー「X-Requested-With: XMLHttpRequest」をコメントアウトしていればコメントアウトはずす](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1806)

- 以下の開発規約に準拠するように修正
  - 可能な限り、JSONの場合(JSONPではない場合)は、XMLHttpRequestにてリクエストヘッダー「X-Requested-With: XMLHttpRequest」を設定し、サーバー側でチェックすること。

- Client
  - APIリクエスト時に、ヘッダーに「X-Requested-With: XMLHttpRequest」を付ける

- server
  - ヘッダーをチェックするミドルウェアを実装
    - /healthは画面からのリクエストではないので除外している
  - ミドルウェアをローカル環境以外で使用するように実装
    - ローカル環境ではサーバーから静的ファイルを配信しているから
       - APIリクエスト以外のリクエストにもmiddlewareが適用されてしまうのでローカル環境は除外している

## レビューポイント
- この修正で開発規約に準拠しているといえるか
- ローカル環境は除外したが、問題ないか
  - ローカルとdev,stg,prodで差異があることで、ローカルだけ発生しない問題が生じる可能性がある(その逆も)
  - 基本的に特定のヘッダーがあるかというチェックを追加しただけなので、大きな問題が発生するとは考えづらい

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 事前にdev環境でAPI呼び出しができることを確認
  - すべてのAPIの呼び出しを確認したわけではないが、ログイン等の基本的な操作はできることを確認した

## 補足
- 相談、参考資料などがあれば
2024-03-11 07:26:01 +00:00
masaaki
84fc89071a Merged PR 811: AzureFunctions実装1(DBから必要な情報を取得する)
## 概要
[Task3842: AzureFunctions実装1(DBから必要な情報を取得する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3842)

- ライセンス数推移情報CSV出力機能のAzureFuntion用関数を追加しました
- DBから必要な情報を取得する処理を実装しました

## レビューポイント
- 特にレビューしてほしい箇所
  - DBアクセス時の結合・検索条件はラフスケッチの条件に対して過不足ないか
  - テストケースに不足はないか
- 関数名、構造は分かりづらくないか

## UIの変更
- 無し

## クエリの変更
- 新規のため無し

## 動作確認状況
- ローカルで処理が正常終了することを確認
- ユニットテストが通ることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - Function全体のunittestを実施し通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-11 06:04:02 +00:00
makabe.t
ccc03da62d Merged PR 823: 環境変数の追従
## 概要
[Task3891: 環境変数の追従](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3891)

- `server`に追加した環境変数の追従が`.env.local.example`から漏れていたので追加

## レビューポイント
- 共有

## UIの変更
- なし

## クエリの変更
- なし
## 動作確認状況
- ローカルで確認
  - exampleを追加したのみなので動作に影響はなし
2024-03-11 02:52:55 +00:00
湯本 開
ff4cd35ed3 Merged PR 819: [3848]アカウント削除処理修正
## 概要
[Task3847: [3848]アカウント削除処理修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3847)

- AccountArchiveエンティティを追加
- アカウント削除時、Accountをアーカイブする処理を追加
- アーカイブ関連テストを追加

## レビューポイント
- 実装の修正内容は問題なさそうか
- テストケースの修正内容は問題なさそうか
- クエリの変更内容の確認方法は問題なさそうか to 斎藤さん

## レビュー対象外
- テスト用ロガーに以下の比較用前処理を追加するべきだが、別タスクを作って対応予定
- 下記クエリの変更点にて、CommentOut判定に環境変数STAGEを使用している部分にRequestIdが表示されていない部分が存在するが、テスト用環境変数の変更は上記と同じく別タスクを作って対応予定
[タスク 3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/3889)

## クエリの変更
- ロガーを有効にした状態でテストを実行し、ログのUUIDと日付を処理して比較できるよう加工した
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/3847?csf=1&web=1&e=xlK011

## 動作確認状況
- npm run testで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - アカウント削除テストで発行されるクエリを比較し、AccountArchiveする対象を特定するためのAccountのSELECT、AccountArchiveのINSERTとSELECTのみが追加されている事が確認できたので、デグレはないと判断
2024-03-11 02:08:29 +00:00
Kentaro Fukunaga
f386a8f7e0 Merged PR 817: API IF実装(親アカウント変更API)
## 概要
[Task3852: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3852)

- 親アカウント変更APIのIFを実装し、OpenAPIの生成もしました。
- 影響範囲(他の機能にも影響があるか)
    - なし

## レビューポイント
- controllerのメソッド名にほか良い案ないか?
- validationに過不足や間違いないか?
- ~~controllerのテストは正常系ひとつだけ追加しているが、他にあったほうがいいものあるか?~~
    - ~~個人的には、テスト追加してもnpmライブラリのvalidatorのテストになるだけな気がするため不要では?と思っています。~~
    - 「npmライブラリのvalidatorを正しいパラメータで正しく利用しているか」が目的であるとの認識を得たため異常系も追加しました。

## 動作確認状況
- apigenを実行してOpenAPI生成できることを確認、controllerテスト通ることを確認。
- 行った修正がデグレを発生させていないことを確認できるか
  - 新規APIのため無し
2024-03-11 01:29:55 +00:00
makabe.t
2e6b7c8ab5 Merged PR 818: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 行番号をExcelの表記通りとなるように修正
- 途中から始められるようにforループを修正

## レビューポイント
- 共有

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-08 05:45:53 +00:00
Kentaro Fukunaga
85edd2296e Merged PR 820: DBマイグレーションファイル追加(アカウント階層構造変更PBI)
## 概要
[Task3863: DBマイグレーションファイル追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3863)

- licenseOrdersテーブルに、from_account_idとstatus検索のマルチカラムインデックスを追加しました。
- 影響範囲(他の機能にも影響があるか)
    - なし

## レビューポイント
- 気になる点あれば

## 動作確認状況
- ローカルでmigrate:up/downをして、想定通りindex作成/削除されることを確認
- 行った修正がデグレを発生させていないことを確認できるか
  - インデックス追加のみのため無し
2024-03-08 05:21:22 +00:00
湯本 開
869cbd43e0 Merged PR 810: アカウント退避テーブル作成
## 概要
[Task3848: アカウント退避テーブル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3848)

- アカウントテーブルと同様の構造をしたアカウント退避テーブルを作成
  - ※差分:  company_nameを削除、active_worktype_idの外部キー制約を削除

## レビューポイント
- 上記要素以外はaccountsテーブルの要素と同等であるか
- 張られたインデックスに不足はないか
- 想定と違う構造になっていないか

## 動作確認状況
- npm run migrate:up/downを実施
- EXPLAINでindexが機能していそうな事を確認
```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1;
---------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '100.00', NULL
```

```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where tier=2;
---------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_tier', 'idx_accounts_archive_tier', '4', 'const', '2', '100.00', NULL
```
```
EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1 AND tier=1;
----------------------------------------------------
# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id,idx_accounts_archive_tier', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '50.00', 'Using where'
```
2024-03-08 05:20:00 +00:00
saito.k
aca9bcf496 Merged PR 782: 翻訳反映+α
## 概要
[Task3781: 翻訳反映+α](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3781)

- 翻訳情報の反映
- タスクのチェックアウト時(/tasks/{audioFileId}/checkout)にライセンスをチェックするようになったため、それに対応するエラーを追加
  - ライセンスが未割当の時とライセンスが有効期限切れの時にそれぞれ専用のエラーメッセージを表示するように修正する

## レビューポイント
- エラーのハンドリングに誤りはないか

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3781?csf=1&web=1&e=dCIGih

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

## 補足
- 相談、参考資料などがあれば
2024-03-08 05:11:20 +00:00
makabe.t
146b8a6e40 Merged PR 816: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-08 01:09:14 +00:00
SAITO-PC-3\saito.k
9dc80b4965 Merge branch 'develop' into ccb 2024-03-07 20:55:44 +09:00
makabe.t
04c726e964 Merged PR 808: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-07 11:47:53 +00:00
maruyama.t
f265d0ff45 Merged PR 812: 「oderLicense」や「CardLicense」を開いて閉じるとパンくずリストがおかしくなる
## 概要
[Task3771: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3771)

ポップアップを×で閉じた際に、パンくずリストを保持したまま、`dispatch(getMyAccountAsync()`しており画面を初期化していた。
画面を更新するのかしないのかあいまいな挙動になっていた。

対応としては、他のポップアップと同様、×で閉じた場合は画面は更新しないように修正。
追加でパンくずリストを初期化する処理を作成し、ポップアップ側でAPIを呼び出した場合のみそれを実行する。

## レビューポイント
- 特筆する点はなし

## UIの変更
- スクショ置き場
https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3771?csf=1&web=1&e=USf40m

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

## 補足
- 相談、参考資料などがあれば
2024-03-07 10:41:30 +00:00
saito.k
e448e8d249 Merged PR 806: メール文面の修正反映
## 概要
[Task3792: メール文面の修正反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3792)

- メール文面を修正
  - ドイツ語の部分

## レビューポイント
- タスクに添付しているエクセルの内容と修正した内容を照らし合わせて確認していただきたいです。

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカル環境でメールを確認
- メール文面のみの修正のため、他機能に影響はない

## 補足
- 相談、参考資料などがあれば
2024-03-07 02:26:15 +00:00
SAITO-PC-3\saito.k
eda88aa048 Merge branch 'develop' into ccb 2024-03-06 11:23:50 +09:00
makabe.t
da40e8f09c Merged PR 800: 画面実装(一括追加ボタン&ポップアップ画面)
## 概要
[Task3753: 画面実装(一括追加ボタン&ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3753)

- ユーザー一括登録画面を実装しました。
  - 一括登録ポップアップ
    - テンプレートCSVダウンロード
    - ファイルインポート
      - エラー行表示

## レビューポイント
- 行エラーの条件、内容は認識通りでしょうか?
- 画面の表示内容は認識通りでしょうか?
- CSV変換時にworkerを有効にしているとエラーとなりうまくいかないのでOFFにしてしまいましたが問題ないでしょうか?
  - @<湯本 開> さん

## UIの変更
- [Task3753](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3753?csf=1&web=1&e=x5M6hr)

## 動作確認状況
- ローカルで確認
 -  ファイルチェックするところまで
2024-03-06 01:58:32 +00:00
makabe.t
d6a47932e7 Merged PR 786: Azure Functions実装(一括登録)
## 概要
[Task3756: Azure Functions実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3756)

- ユーザー一括登録用のAzure Functionを実装しました。

## レビューポイント
- 処理の流れがラフスケッチと認識通りでしょうか?
- JSONファイルの内容はイメージ通りでしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
  - テスト実行

実際の詳細な動作についてはdevelop環境で確認します。
2024-03-06 01:48:02 +00:00
saito.k
7160e0ee2e Merged PR 804: デグレ再発防止のため、修正をチェックするテストを作成
## 概要
[Task3830: デグレ再発防止のため、修正をチェックするテストを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3830)

- タスクを100件取得できることを確認するテストを追加

## レビューポイント
- テストでかくにんする項目は足りているか

## UIの変更
- 特になし

## クエリの変更
- 特になし

## 動作確認状況
- ローカルでテストが通ることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-06 01:31:10 +00:00
masaaki
2220e2560f Merged PR 799: makepasswordで条件に合致しないパスワードを生成した際無限ループになる
## 概要
[Task3840: makepasswordで条件に合致しないパスワードを生成した際無限ループになる](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3840)

- makepasswordで条件に合致しないパスワードを生成した場合、再度生成するループ処理としているが、初期化が行われていないため常に同じパスワードで条件合致のチェックが行われていました。結果、一度条件に合致しないパスワードを生成した場合無限ループとなっていました。
- ループ内で変数を初期化するよう対応。

## レビューポイント
- 特にありません

## UIの変更
- 無し

## 動作確認状況
- ユニットテストが通ることを確認
- ローカルでユーザー作成を実施し、これまで同様作成できることを確認

## 補足
- 相談、参考資料などがあれば
2024-03-06 01:19:23 +00:00
makabe.t
31de71f743 Merged PR 797: API実装(一括登録)
## 概要
[Task3752: API実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3752)

- ユーザー一括登録APIとテストを実装しました。
  - メール文面はまだ翻訳が来ていないので日本語のものを使用しています。別タスクで多言語対応します。

## レビューポイント
- ファイル名は認識通りでしょうか?
- ファイルの内容は認識通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-03-05 10:27:50 +00:00
makabe.t
7ff563f644 Merged PR 795: API実装(一括登録完了)
## 概要
[Task3763: API実装(一括登録完了)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3763)

- 一括登録完了APIとテストを実装しました。
  - メール文面は多言語対応がまだですのですべて日本語の文面にしています。

## レビューポイント
- 送信メールの内容は認識通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-03-04 09:15:53 +00:00
masaaki
a47ebaa9df Merged PR 798: [4回目実行][フルデータ]develop環境での移行実施後の修正作業
## 概要
[Task3821: [4回目実行][フルデータ]develop環境での移行実施後の修正作業](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3821)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 移行ツールに対して以下の修正を実施しました。
  - アカウントとユーザ間でAuthorIDが重複する際、通番を付与して重複を避けるようにしました
  - AADB2Cのエラー発生時、リトライ処理を行うように対応しました

## レビューポイント
- 特にありません

## UIの変更
- 無し

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

## 補足
- 相談、参考資料などがあれば
2024-03-02 02:21:19 +00:00
masaaki
88ce6a2c9e Merged PR 796: [3回目実行][フルデータ]develop環境での移行実施後の修正作業
## 概要
[Task3802: [3回目実行][フルデータ]develop環境での移行実施後の修正作業](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3802)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-03-01 12:00:42 +00:00
maruyama.t
cad3a99f70 Merged PR 794: 登録ツールにログを仕込む
## 概要
[Task3839: 登録ツールにログを仕込む](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3839)

登録ツールが途中で動かなくなってしまう原因調査のために各関数にログを仕込みました。

## レビューポイント
- 特になし

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

## 補足
- 相談、参考資料などがあれば
2024-02-29 12:50:17 +00:00
maruyama.t
0ebd2ab17e Merged PR 793: accountに名前がないデータが存在する
## 概要
[Task3831: accountに名前がないデータが存在する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3831)

移行元データのアカウントユーザーにfirtst_nameおよびlast_nameが存在しない行が存在しており、
変換ツール側で対応していなかったためAdminNameが空のアカウントユーザーを作成しようとして登録ツール側でエラーになってしまっていた。
→バックログに起票しOMDSさんに確認中

■暫定対応
first_name\last_nameが存在しない場合はユーザーと同様にメールアドレスをAdminNameとするように修正。

## レビューポイント
- とくになし

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

## 補足
- 相談、参考資料などがあれば
2024-02-29 06:55:34 +00:00
Kentaro Fukunaga
9ca9b7a144 Merged PR 790: AuthorのNotificationフラグを見てタスク完了メールの送信先を変更するよう修正
## 概要
[Task3818: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3818)

- AuthorのNotificationフラグがOFFのときには、Authorに対してタスク完了通知メールが送信されないよう修正しました。

## レビューポイント
- 動作確認項目に不足はないか?

## 動作確認状況
- ローカルで確認しました
    - AuthorのNotificationON時にはメール宛先に入っており、OFF時には宛先から外れること
    - TypistはNotificationON/OFF関わらずメール宛先に入っていること
2024-02-29 06:36:23 +00:00
SAITO-PC-3\saito.k
fd3e584fac Merge branch 'develop' 2024-02-29 10:55:41 +09:00
saito.k
ce6e09a7d0 Merged PR 791: タスク一覧画面の取得件数が10件となっているバグの対応
## 概要
[Task3815: タスク一覧画面の取得件数が10件となっているバグの対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3815)

- OptionItemのソート順を変換処理の中で行うように修正
  - OrderByでソートするのはfindメソッドの作り的に無理そうなので
  - 調査にも時間がかかるため

## レビューポイント
- タスクの中にこのバグが発生した原因を記載し、なぜこの修正にしたのか記述したのでそちらを確認していただいて変なところがあれば指摘していただきたいです。
  - 書いている内容がよくわからない場合は、ハドルでの説明をさせてください。

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

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

## 補足
- 相談、参考資料などがあれば
2024-02-29 01:16:15 +00:00
maruyama.t
6d56255a5a Merged PR 792: parent_account_idが正しく設定されない
## 概要
[Task3804: parent_account_idが正しく設定されない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3804)

Map配列からaccountidをキーにdealerAccountIdを取る処理で、検索keyが逆になっていたため修正。

## レビューポイント
- とくになし

## 動作確認状況
- ローカルで確認(階層を付け替えたアカウントの親子階層が正しいことを確認)

## 補足
- 相談、参考資料などがあれば
2024-02-28 09:04:36 +00:00
湯本 開
363f12f86f Merged PR 774: 画面実装(csv読み込み部分切り出し)
## 概要
[タスク 3754: 画面実装(csv読み込み部分切り出し)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3754)

- CSVファイルの内容を入力として、JSONに変換する処理&テストを実装
- 画面実装時の想定としては、以下の流れと想定しており、本Taskの範囲は1のみ
  1. csv->jsonへパースが出来るか?(csvの形式として合っていて読み込み可能か?)
  2. json内の各パラメータは問題ないか?(データの制限や組み合わせは問題ないか?)
- 使い方を示す & 動作確認のために、client側でもテストを実施できるよう修正(※pipelineでは実行されない)

## レビューポイント
- テストケースを見て、使い方は分かるか
- CSVの形式自体が想定とズレていた場合は入力を弾く必要がある想定だが、間違っていないか
- 利用ライブラリはメジャーかつ便利そうなものを選定したが、問題なさそうか

## 動作確認状況
- npm run testを通過
2024-02-28 09:03:27 +00:00
masaaki
0be9c26f09 Merged PR 781: データ検証ツール作成+動作確認
## 概要
[Task3573: データ検証ツール作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3573)

- データ検証ツールを作成しました

## レビューポイント
- 特にレビューしてほしい箇所
 詳細情報の突き合わせについて、ラフスケッチと対応しているか第三者目線でも確認してほしいです
 verification.serviceのcompareCardLicenses、compareLicenses、compareAccountsになります。

## UIの変更
- 無し

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

## 補足
- 無し
2024-02-28 05:31:13 +00:00
湯本 開
71127a6db9 Merged PR 787: API I/F修正
## 概要
[Task3793: API I/F修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3793)

- OMDS様とのメール文面調整の結果、csvファイル名もAPIで受け渡す必要が出てきたためAPI I/Fを修正する
  - 一括登録依頼API、一括登録完了APIの両方に「ファイル名」を追加
- 増えたプロパティのバリデーションをするテストを追加

## レビューポイント
- プロパティ名は妥当か
- テストの内容は十分か

## 動作確認状況
- npm run testを通過
2024-02-28 05:30:09 +00:00
maruyama.t
f6d39a4c26 Merged PR 788: [2回目実行]実施後の動作確認
## 概要
[Task3577: [2回目実行]実施後の動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3577)

accountがCountryの場合に配下のDistributorの親アカウントIDを付け替える処理について、Typeの付け替えができていなかったのを修正。
ライセンスの有効期限が"9999/12/31 23:59:59.997"でフォーマットチェックをしているが、移行元は"9999/12/31 23:59:59"なので移行元に合わせた。
dealerAccountIdが設定されているが、そのdealerが存在しない場合もデータを作ってしまっている。
→該当レコードはエラーファイルを出力する。

## レビューポイント
- エラーファイルの出力処理だが簡素すぎるか?
JSONで出力する意味はないが、これまでの動作確認で動作担保できているのでJSONで出しています。

## 動作確認状況
- ローカルで確認
正常の場合データ変換が行われることを確認。
dealerAccountIdが設定されているが、そのdealerが存在しない場合もデータでテストした場合、error.jsonが作られることを確認。
## 補足
- 相談、参考資料などがあれば
2024-02-27 23:55:44 +00:00
SAITO-PC-3\saito.k
34d1dd5629 Merge branch 'develop' 2024-02-27 18:53:10 +09:00
maruyama.t
f0d71937e3 Merged PR 780: データ変換ツール(汚いデータ対応版)の作成+動作確認
## 概要
[Task3776: データ変換ツール(汚いデータ対応版)の作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3776)

綺麗なデータ対応版のレビュー指摘も合わせて修正。
一旦OMDS様よりいただいた1万件~のデータを処理できることは確認済みです。
実装コストとバグの入れ込みを懸念し、有効期限が"9999/12/31"のデータは最初にデータを積む段階で除外するようにしました。

## レビューポイント
- メールアドレス重複チェックについて、想定通りの重複対象を検索出来ているか。
- step3の1.アカウントとユーザが同じ場合
adminMainとuserEmailが重複していた場合に、重複していたユーザーは削除し、アカウントのみを残す(accountユーザーのroleとauthorIdは削除したuserに設定されていたものとする)処理は妥当か。
→accountのIFにroleとauthorIdを追加し、register側のcreateAccountで登録するようにしています。

## 動作確認状況
- ローカルで確認(Account_transition_2024.1.19.csvで実施)
4つのJSONファイルができていることを確認。
Countryの場合の付け替えができていることを確認。
adminMainとemailが重複している場合の重複削除ができていることを確認。

## 補足
- 登録ツールと共通のパラメータで動作するようにしました。
例)
POST:
localhost:8280/transfer
Body:
{
    "inputFilePath": "./data/"
}

 変換ツールの使い方としてはAccount_transition.jsonというファイルを見るようにしています。
2024-02-27 06:24:41 +00:00
Kentaro Fukunaga
dd8bddc971 Merged PR 771: 音声ファイルアップロード完了API実装(ストレージ使用量超過チェック)
## 概要
[Task3687: 音声ファイルアップロード完了API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3687)

- 音声ファイルアップロード完了API実行時に、ストレージの使用量チェックを行い、必要ならメール送信をする実装を追加しました。

## レビューポイント
- 使用量チェックメソッドで他にいい関数名ないか?
- なるべく既存実装をいじりたくなかったので自動ルーティング前にチェック機構を配置したが不都合ないか?
- テストケースに過不足ないか
- 自動テストの実行方法や確認方法として適切か?ほかに代替案ないか?

## 動作確認状況
- ローカルでUT通ることを確認。
   - 実際のメール送信はdeveop動作確認でやります。
2024-02-27 02:49:52 +00:00
SAITO-PC-3\saito.k
ddd4d31f25 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_server/src/features/users/users.service.spec.ts
2024-02-27 09:26:46 +09:00
Kentaro Fukunaga
5305984b1a Merged PR 764: 第五階層ライセンス情報画面実装
## 概要
[Task3709: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3709)

- ストレージ使用可否切り替えの画面実装をしました
- 動作確認中に、既存実装でライセンスオーダーするときとカードライセンスアクティベートするときの操作不能化処理に漏れがあったのを修正しました

## レビューポイント
- Redux周りの実装でお作法に違反しているところがないか。もしくは改善点ないか。
- ライセンス情報表示のAPI結果待ち部分のローディング処理で、最低限の改善にしたが現時点ではこれでよいか?(いつ修正するかも未定だけど、実害はないためひとまずこんな感じで。。。)
    - `licenseSummarySlice.ts` のコメント部分が該当箇所です

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3709?csf=1&web=1&e=bJVzss

## 動作確認状況
- ローカルで動作確認しました。
2024-02-27 00:01:02 +00:00
saito.k
0ab6488f58 Merged PR 779: タスク一覧画面のOptionItemがソート条件によって表示順がおかしくなる
## 概要
[Task3787: タスク一覧画面のOptionItemがソート条件によって表示順がおかしくなる](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3787)

- タスク一覧取得APIレスポンスにあるOptionItemの順番を固定する(idの昇順)
- テスト修正

## レビューポイント
- 特になし

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

## 補足
- 相談、参考資料などがあれば
2024-02-26 11:48:02 +00:00
saito.k
5a78a6668f Merged PR 769: /users/relationsレスポンスのWorkTypeIDをID名の昇順にする
## 概要
[Task3783: /users/relationsレスポンスのWorkTypeIDをID名の昇順にする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3783)

- relationsAPIレスポンスのWorkTypeList内の順番を指定する
- テスト修正

## レビューポイント
- 修正内容に不足はないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

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

## 補足
- 相談、参考資料などがあれば
2024-02-26 11:29:22 +00:00
saito.k
d0628caa05 Merged PR 767: パートナー追加成功時に一覧の更新が行われていない
## 概要
[Task3769: パートナー追加成功時に一覧の更新が行われていない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3769)

- パートナー追加成功時に一覧の更新を行う

## レビューポイント
- 特になし

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3769?csf=1&web=1&e=ajJOBd

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-02-26 10:42:26 +00:00
masaaki
68d1a1796b Merged PR 783: [1回目実行]実施後の修正実施
## 概要
[Task3790: [1回目実行]実施後の修正実施](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3790)

- 移行データ動作確認(1回目)で発生した不具合の対応を実施しました
 ・バリデータでエラーとなる(コメントアウトして実行したところ、成功)
 ・アカウント管理者のユーザーのメール認証がfalseで登録されるので、
  パスワード変更では認証をできない⇒強制敵にtrueで登録する
 ・(指摘外、検証ツール実装時に内部検出)カードライセンス登録時、カードライセンス発行・ライセンステーブルも登録する

- このPull Requestでの対象/対象外
 上記以外の指摘(下記)はタスク3772にて対応するため本プルリク対象外

 変換ツール
  ・ディーラーアカウントに登録されたアカウントがいないCSVで
   変換するとディーラーがundefined(パラメータがない)状態でJSON出力されてしまう。
  ・重複したメールアドレスの取り込みが未実装
  ・有効期限が9999/~は移行対象外とする
  ・ワークタイプの出力がされない

 登録ツール
  ・ファイルパスの取り扱いが変換ツールと異なる
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- カードライセンス登録時のライセンスのアカウントIDについて、第一階層アカウントのため「AUTO_INCREMENT_START: 853211」を設定しているが問題ないか?
 →移行データ上第一階層アカウントは最初に登場するため問題ない認識

## UIの変更
- 無し

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

## 補足
- 相談、参考資料などがあれば
2024-02-26 08:59:37 +00:00
湯本 開
c95fb1e1f6 Merged PR 784: テスト失敗修正(I/F実装)
## 概要
[Task3791: テスト失敗修正(I/F実装)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3791)

- AuthorIDのチェックを行うバリデータが特定リスクエストに対してしか利用できなかったので、もっと単純な同一ロジックのバリデータを追加して利用するよう変更
- 上記ケースに対するテストを追加

## レビューポイント
- 既存実装へは影響がなさそうか

## 動作確認状況
- npm run testを通過
2024-02-26 07:48:08 +00:00
湯本 開
c1f370faaf Merged PR 766: API I/F & system権限Token実装
## 概要
[Task3764: API I/F & system権限Token実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3764)

- システムが発行したトークンの型定義を追加
- AuthGuardと同等の、システムが発行したTokenである `SystemAccessToken` を検証する `SystemAccessGuard` を追加
- API I/Fを実装

## レビューポイント
- バリデーターは適切か
- システムが発行したトークンの型定義は適切か
- API I/Fの型は問題ないか

## 動作確認状況
- ローカルでswagger UI上で確認
2024-02-26 05:13:43 +00:00
SAITO-PC-3\saito.k
13d421c2bc Merge branch 'develop' into ccb 2024-02-23 11:10:38 +09:00
saito.k
b524fd5995 Merged PR 768: U-105メッセージのドイツ語部分が英語になっている
## 概要
[Task3770: U-105メッセージのドイツ語部分が英語になっている](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3770)

- U-105のメール文面を修正
 - ドイツ語の文章であるべき箇所が英語になっていた

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

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

## 補足
- 相談、参考資料などがあれば
2024-02-23 00:42:05 +00:00
makabe
a1b59de44d Merge branch 'develop' into ccb 2024-02-22 20:43:35 +09:00
makabe.t
fbdfeee73c Merged PR 778: 削除ツールの修正
## 概要
[Task3788: 削除ツールの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3788)

- ADB2Cの削除に失敗するので対応しました。
  - 最後のページの場合に削除より先に抜けていたので、削除処理を先にやるように修正しました。

## レビューポイント
- 共有

## UIの変更
- なし

## 動作確認状況
- developで確認
2024-02-22 11:42:31 +00:00
makabe.t
f03342bc55 Merged PR 777: データ削除ツール作成+動作確認
## 概要
[Task3569: データ削除ツール作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3569)

- クライアントに不要なフォルダが残っていたので削除しました。

## レビューポイント
- 共有

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-02-22 08:45:20 +00:00
maruyama.t
a65d6a2774 Merged PR 776: 階層の付け替えを誤っていたのを修正
## 概要
[Task3570: データ変換ツール(きれいなデータ版)作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3570)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-02-22 08:40:57 +00:00
maruyama.t
cb68c16eb8 Merged PR 775: 変換ツールのバリデーションチェックを修正
## 概要
[Task3570: データ変換ツール(きれいなデータ版)作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3570)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-02-22 08:23:31 +00:00
maruyama.t
c31bb47bb8 Merged PR 773: apiにtransferが存在しなかったのを修正
## 概要
[Task3570: データ変換ツール(きれいなデータ版)作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3570)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
2024-02-22 07:52:16 +00:00
makabe.t
dc52ec2022 Merged PR 765: データ削除ツール作成+動作確認
## 概要
[Task3569: データ削除ツール作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3569)

- ADB2Cからのユーザー削除が100件ごとにしか削除できていなかったので、修正しました。
  - 取得が100件まででそのユーザーに対して削除処理をしていたので100件までの削除になっていました。
  - 対応として、100件づつの削除をユーザーが全削除されるまで実行するようにしました。

## レビューポイント
- 対応方法として適切でしょうか?
- ループで制限を設けていますが、MAX値として適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで順に実行できることを確認
- 実際の削除は別途develop環境で実施します。
2024-02-22 07:33:55 +00:00
makabe
439ce7de63 Merge branch 'develop' into ccb 2024-02-21 09:16:05 +09:00
Kentaro Fukunaga
f8183399e2 Merged PR 762: アカウント利用制限更新API実装
## 概要
[Task3710: アカウント利用制限更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3710)

- タイトルの通りです。
- 既存実装でログが不足している箇所あったのでちょろ修正もしました。

## レビューポイント
- これと言ってみて欲しいポイントはないので、何か気になる点あれば

## 動作確認状況
- ローカルで全テストが通ることを確認済み
2024-02-20 11:23:04 +00:00
makabe
e44cb3b955 Merge branch 'develop' into ccb 2024-02-20 19:24:42 +09:00
SAITO-PC-3\saito.k
562db3def9 Merge branch 'develop' into ccb 2024-02-19 21:25:54 +09:00
Kentaro Fukunaga
ecb28b9328 Merged PR 759: API IF実装
## 概要
[Task3711: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3711)

- アカウント利用制限更新APIのIFを実装し、openapi生成しました。

## レビューポイント
- メソッド名で他に案あれば。

## 動作確認状況
- 特になし
2024-02-19 04:32:59 +00:00
SAITO-PC-3\saito.k
cdef84e269 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_server/src/features/accounts/accounts.service.spec.ts
2024-02-19 12:00:01 +09:00
makabe.t
fc7d271a29 Merged PR 749: 第五階層ライセンス情報画面実装
## 概要
[Task3654: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3654)

- 第五階層ライセンス情報画面でストレージ情報(Storage Available、Storage Used)を表示するように画面を修正しました。
  - 表示する値はGB単位となるようにし、小数点以下第三位までを表示するようにしています。

## レビューポイント
- 表示する値のフォーマットは適切でしょうか?
  - 小数点以下第三位までを固定で表示するようにしていますが問題ないでしょうか?

## UIの変更
- [Task3654](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3654?csf=1&web=1&e=1tCGoN)

## 動作確認状況
- ローカルで確認
2024-02-16 05:15:45 +00:00
Kentaro Fukunaga
aef30c8cbe Merged PR 748: 第五階層ライセンス情報取得API実装
## 概要
[Task3655: 第五階層ライセンス情報取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3655)

- 第五階層ライセンス情報取得APIに、ストレージ上限とストレージ使用量を取得する処理を追加しました。
- 既存の「割り当て済みライセンス取得処理」と「再利用可能ライセンス取得処理」に不要な条件があったため削除しました

## レビューポイント
- 上限計算方法、使用量取得条件に仕様との認識齟齬はないか?
    - もしくはテストケースで「これもあったほうがいいのでは?」などないか
- その他気になる点あれば

## 動作確認状況
- ローカルでテストが全部通ることを確認
2024-02-16 02:11:34 +00:00
x.yumoto.k
c0b99203da Merge branch 'develop' into ccb 2024-02-15 15:14:50 +09:00
SAITO-PC-3\saito.k
63191a3b61 OptionItemの作成が重複してしまっていたので片方を削除
タスク削除のテストをMysql使用するように修正
2024-02-14 12:06:19 +09:00
SAITO-PC-3\saito.k
3877a4670d Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/UserListPage/index.tsx
2024-02-14 11:33:11 +09:00
makabe.t
bbbd3e757b Merged PR 743: テンプレートファイル削除画面修正
## 概要
[Task3600: テンプレートファイル削除画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3600)

- テンプレートファイル削除の画面を実装しました。

## レビューポイント
- エラー処理は適切でしょうか?

## UIの変更
- [Task3600](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3600?csf=1&web=1&e=E25Kf7)

## 動作確認状況
- ローカルで確認
2024-02-13 02:32:17 +00:00
makabe.t
83efd97bdf Merged PR 739: テンプレートファイル削除API実装
## 概要
[Task3599: テンプレートファイル削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3599)

- テンプレートファイル削除APIとテストを実装しました。

## レビューポイント
- テンプレートファイル削除できないエラーの条件は適切でしょうか?
- テストケースは適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2024-02-13 00:22:38 +00:00
makabe
447b0e280c Merge branch 'develop' into ccb 2024-02-09 19:06:45 +09:00
x.yumoto.k
cd277f3f9a Merge branch 'develop' into ccb 2024-02-09 17:37:38 +09:00
makabe.t
270122b135 Merged PR 741: アカウント情報画面修正(保存日数の表示)
## 概要
[Task3691: アカウント情報画面修正(保存日数の表示)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3691)

- アカウント情報画面にファイルの保存日数を表示するよう修正しました。
  - ファイル削除が設定されている場合は日数、されていない場合はハイフン表示となるようにしています。

## レビューポイント
- 画面イメージは想定通りでしょうか?
- 表示する保存日数は想定通りの値でしょうか?

## UIの変更
- [Task3691](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3691?csf=1&web=1&e=Gthaa8)

## 動作確認状況
- ローカルで確認
2024-02-09 01:05:15 +00:00
masaaki
32d8c6b896 Merged PR 737: ファイル削除設定API作成
## 概要
[Task3546: ファイル削除設定API作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3546)

- プロダクト バックログ項目 1199: アカウント設定を変更したい(ファイル削除設定)
- ファイル削除設定API作成を実装しました
- 変数定義時に型指定が不要で、自動的に明示的な型宣言が削除されています。非テストコードについてはeslint-disable-lineによって無視設定を行っています。テストコードについては削除したままとしています。

## レビューポイント
- 特筆するものはありません

## UIの変更
- 無し

## 動作確認状況
- ローカル及びユニットテストで確認

## 補足
- 無し
2024-02-07 02:47:41 +00:00
Kentaro Fukunaga
91c27b7684 Merged PR 736: テンプレートファイル削除API IF実装
## 概要
[Task3598: テンプレートファイル削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3598)

- 掲題のとおり、IF実装しました。

## レビューポイント
- 認可処理に認識違いないか?
- パラメータに不足ないか?

## 動作確認状況
- ローカルでAPI実行できることを確認

## 補足
- とくになし
2024-02-06 23:53:00 +00:00
Kentaro Fukunaga
548c7a05e9 Merged PR 729: 画面実装(ファイル削除設定ポップアップ画面)
## 概要
[Task3547: 画面実装(ファイル削除設定ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3547)

- ファイル削除設定ポップアップの新規実装をしました。

## レビューポイント
- reduxとの連携部分の設計でNGな部分ないか?
- 処理の抜け漏れないか?

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3547?csf=1&web=1&e=0pu92Q

## 動作確認状況
- ローカルで一通りの動作確認しました。

## 補足
- とくになし
2024-02-06 12:58:55 +00:00
makabe.t
eaf1b3c8b8 Merged PR 738: develop動作確認不具合対応
## 概要
[Task3645: develop動作確認不具合対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3645)

- 削除完了メールのタイトルを修正しました。

## レビューポイント
- 共有

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-02-06 10:26:42 +00:00
makabe.t
19b544540e Merged PR 724: API実装
## 概要
[Task3535: API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3535)

- タイピストグループ削除APIとテストを実装しました。

accountのテストがうまくいっていないようなので別途見直します。
※タイピストグループ削除のテストはうまくいっています

## レビューポイント
- エラーケースと出力されるコードは適切でしょうか?
- テストケースは適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2024-02-06 07:46:57 +00:00
湯本 開
feeec9d1f5 Merged PR 714: API実装(ユーザー削除|Repository以外)
## 概要
[Task3594: API実装(ユーザー削除|Repository以外)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3594)

ユーザー削除API実装
ユニットテスト実装

## レビューポイント
- `'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった)`が用意されているが、
`'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)`とわけて実装する必要あるか。
管理者でしか削除処理は行えない&管理者ユーザは削除できない。

- `ExistsCheckoutPermissionDeleteFailedError`
削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラーは、ユーザ削除エラーの一つとして、`code.ts`にコードを用意してあげる必要があるか?
(引継ぎ時あえて用意していないように見えなくもなかったので)

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

## 補足
- 相談、参考資料などがあれば
2024-02-06 07:12:11 +00:00
x.yumoto.k
4442c7cbbe Merge branch 'develop' into ccb 2024-02-06 14:56:39 +09:00
SAITO-PC-3\saito.k
1e8bc39c7f Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-02-05 21:15:24 +09:00
makabe
ce3deecd1b Merge branch 'develop' into ccb 2024-02-05 19:39:37 +09:00
makabe
8e159a1c2a Merge branch 'develop' into ccb 2024-02-05 16:36:09 +09:00
maruyama.t
df74dc358c Merged PR 731: 既存API修正(アカウント情報取得API、アカウント作成API)
## 概要
[Task3555: 既存API修正(アカウント情報取得API、アカウント作成API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3555)

Accountにauto_file_delete、file_retention_daysを追加
既存のテストでアカウントを作成している部分に項目の値を追加。

## レビューポイント
-entityの

```
@Column({ default: 30 })
```
が必要かどうか。

## UIの変更
なし

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

## 補足
- 相談、参考資料などがあれば
2024-02-05 02:27:43 +00:00
makabe
186896da15 Merge branch 'ccb' of https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_git/ODMS%20Cloud into ccb 2024-02-05 09:46:49 +09:00
saito.k
4be13e002d Merged PR 710: 画面実装(削除操作)
## 概要
[Task3488: 画面実装(削除操作)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3488)

- ユーザー削除の画面実装
  - 確認ダイアログ
  - 削除API呼び出し
  - エラーハンドリング
  - 成功時のメッセージ
  - 成功時のユーザー一覧更新

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

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

## 補足
- API呼び出しのエラーハンドリング部分はエラーコードが採番されたら追従します
2024-02-05 00:39:53 +00:00
makabe
cf30b97a16 Merge branch 'ccb' of https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_git/ODMS%20Cloud into ccb 2024-02-05 09:37:06 +09:00
Kentaro Fukunaga
44759b1aac Merged PR 725: タイピストグループ削除 画面実装
## 概要
[Task3536: 画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3536)

- タイピストグループ画面に削除ボタン配置
- 削除ボタンクリック時に確認ダイアログ表示→OKで削除API実行&ぐるぐる表示
- API実行結果によってメッセージの出し分け&画面リスト更新
- 翻訳情報追加

## レビューポイント
- エラーコードに認識違いないか?
- 画面実装のお作法にそぐわないところがないか?
- なんか抜けてる実装などあれば

## UIの変更
大した変更ではないので、スクショ置き場に置く手間を省いてここに貼り付けます
![image (2).png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/725/attachments/image%20%282%29.png)

## 動作確認状況
- 期待される動作を一通りローカルで確認しました。
    - API実行中はぐるぐる表示されること
    - API成功または削除済みの場合は成功メッセージ表示され、画面更新されること
    - API失敗時、理由によってエラーメッセージが表示分けされること
2024-02-05 00:31:47 +00:00
makabe
5ab2f02c56 Merge branch 'develop' into ccb 2024-02-05 09:26:55 +09:00
Kentaro Fukunaga
f595cae1b8 Merged PR 728: ファイル削除設定更新API IF実装
## 概要
[Task3557: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3557)

- ファイル削除設定更新APIのIFを実装してOpenAPI生成しました。

## 補足
- 別PRでOKもらってるのでそのままマージします
2024-02-03 06:22:53 +00:00
Kentaro Fukunaga
48a2bddfd9 Merged PR 721: DBマイグレーション
## 概要
[Task3556: DBマイグレーション](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3556)

- アカウントテーブルに対して、以下の2カラムを追加しました。
    - 自動ファイル削除要否
    - 文字起こし完了してからファイル削除するまでのファイル保持日数

## レビューポイント
- カラム名としてほかに適切なものはないか?
- 初期値合ってますよね?

## 動作確認状況
- ローカルでmigrate up と downが出来ることを確認

## 補足
- もしこの値の扱いが、「Finishedになってから○○日後にファイルを削除する」のものであればそのとき修正します。
2024-02-02 04:57:25 +00:00
SAITO-PC-3\saito.k
fbcafd2014 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/DictationPage/index.tsx
2024-02-02 12:01:46 +09:00
makabe.t
9f7c8c99c0 Merged PR 720: API IF実装
## 概要
[Task3534: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3534)

- タイピストグループ削除APIのIFを実装し、OpenAPIを更新しました。

## レビューポイント
- パラメータとバリデータは想定通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-02-02 00:27:08 +00:00
makabe
92193d499a Merge branch 'develop' into ccb 2024-01-31 10:28:39 +09:00
湯本 開
c32b38b783 Merged PR 703: API実装(ユーザー削除)/Repository実装
## 概要
[Task3521: API実装(ユーザー削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3521)

- ユーザー削除を行うRepository(DB操作部分)を実装
  - 削除不可条件をチェックして削除できなければエラー
  - 削除可能だった場合、以下の処理を実行
    - ユーザーをアーカイブ
    - ユーザーを削除

## レビューポイント
- 「ライセンス割り当て解除」をせずにユーザーを削除するため、ライセンスがUserテーブルに存在しないIDを指したままになってしまうが問題ないか
  - ラフスケッチ時には、UserArchiveのidには紐づく & UserArchiveに紐づくことによって期限切れのライセンスが誰に割り当たっていたかを把握できるという話だったと思うが、これは"そういう必要がある"という仕様という認識でよいか
- ロック対象の指定は妥当であるか
  - デッドロックは発生しなさそうか
    - User -> UserGroup -> Workflow -> Task -> CheckoutPermission -> Licenseの順番

## 動作確認状況
- 動作確認なし
2024-01-30 07:10:11 +00:00
SAITO-PC-3\saito.k
08a37ff264 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/translation/de.json
#	dictation_client/src/translation/en.json
#	dictation_client/src/translation/es.json
#	dictation_client/src/translation/fr.json
2024-01-29 18:30:34 +09:00
makabe
f00861702a Merge branch 'develop' into ccb 2024-01-29 15:52:55 +09:00
湯本 開
8dfbcea0da Merged PR 702: API IF実装
## 概要
[Task3520: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3520)

- ユーザー削除APIのI/Fを実装

## レビューポイント
- バリデーターは適切に設定されているか
- 不要な処理が混入していないか
- 代行操作による実行を許可しているが、認識は間違っていないか
- マージ先ブランチは間違っていないか

## 動作確認状況
- openapi.jsonの生成成功を確認
2024-01-25 04:00:54 +00:00
makabe.t
8aa45baee8 Merged PR 697: DB関連コード修正
## 概要
[Task3509: DB関連コード修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3509)

- 参照するDBがCCB用のスキーマとなるようにserverの参照DB名を変更しました。
対象:
- server
  - .env
  - app.module.ts
- function
  - .env
  - functions/*

※マージの取り込み分も入ってしまったので他は無視してください。

## レビューポイント
- 対応箇所は適切でしょうか。

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
  - ローカルにスキーマを追加・マイグレーションしてserverを起動できることを確認
2024-01-24 02:52:13 +00:00
SAITO-PC-3\saito.k
f2f8728319 ccbの競合解消漏れを対応 2024-01-24 11:15:27 +09:00
SAITO-PC-3\saito.k
ded446de93 Merge branch 'develop' into ccb
# Conflicts:
#	dictation_client/src/pages/UserListPage/index.tsx
#	dictation_server/package.json
#	dictation_server/src/common/test/init.ts
#	dictation_server/src/features/auth/auth.service.spec.ts
#	dictation_server/src/features/files/files.service.spec.ts
#	dictation_server/src/features/licenses/licenses.service.spec.ts
#	dictation_server/src/features/tasks/tasks.service.spec.ts
#	dictation_server/src/features/terms/terms.service.spec.ts
#	dictation_server/src/features/users/users.service.spec.ts
#	dictation_server/src/features/workflows/workflows.service.spec.ts
2024-01-24 10:46:48 +09:00
makabe.t
1524ec2473 Merged PR 701: パイプラインエラー対応
## 概要
[Task3531: パイプラインエラー対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3531)

- パイプラインエラー解消

## レビューポイント
- 共有
2024-01-24 00:48:21 +00:00
makabe.t
a7bb32ec4a Merged PR 699: DBマイグレーションファイルにCCB用の設定を追加する
## 概要
[Task3510: DBマイグレーションファイルにCCB用の設定を追加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3510)

- DBマイグレーションファイルにCCB用の設定を追加し、コマンドを修正しました。

## レビューポイント
- 設定内容は認識通りでしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
  - CCB用のスキーマに対してmigrate:up/downできるところまで確認
2024-01-23 10:30:14 +00:00
makabe.t
d08c6c99af Merged PR 681: タスク削除API実装
## 概要
[Task3457: タスク削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3457)

- タスク削除APIとUTを実装しました。

## レビューポイント
- テストケースは適切でしょうか?
- リポジトリでの削除処理は適切でしょうか?
- エラー時のコード使い分けは適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-16 07:55:38 +00:00
makabe.t
8793606070 Merged PR 682: タスク一覧画面修正
## 概要
[Task3458: タスク一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3458)

- タスク一覧画面のタスク削除ボタンからタスクを削除する処理を実装しました。
  - タスクがInProgress、ユーザーがTypistの場合にはボタンを非活性となるようにしています。

## レビューポイント
- エラーごとの処理内容は適切でしょうか?
- ボタンの活性制御は適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-16 00:17:45 +00:00
makabe.t
81c299dd99 Merged PR 680: タスク削除API IF実装
## 概要
[Task3456: タスク削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3456)

- タスク削除APIのIFを実装しopenapi.jsonを更新しました。

## レビューポイント
- パス、バリデータは想定通りでしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-11 08:47:28 +00:00
394 changed files with 74283 additions and 3636 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
environment_building_tools/logfile.log

View File

@ -0,0 +1,312 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ccb
tags:
include:
- stage-*
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ccb:release-ccb
if git merge-base --is-ancestor $(Build.SourceVersion) release-ccb; then
echo "This commit is in the release-ccb branch."
else
echo "This commit is not in the release-ccb branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ccbブランチに存在するか確認'
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build And Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
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: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
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:stg
- 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: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_test
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
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: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: function_build
condition: succeeded('function_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--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: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
- job: swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
- job: migration
dependsOn: swap_slot
condition: succeeded('swap_slot')
displayName: DB migration
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: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME_CCB/$(db-name-ccb)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-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_ccb

View File

@ -0,0 +1,363 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConnectionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ph1-enhance
tags:
include:
- stage-*
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ph1-enhance:release-ph1-enhance
if git merge-base --is-ancestor $(Build.SourceVersion) release-ph1-enhance; then
echo "This commit is in the release-ph1-enhance branch."
else
echo "This commit is not in the release-ph1-enhance branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ph1-enhanceブランチに存在するか確認'
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build And Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
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: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
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:stg
- 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: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: frontend_build_production
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: Build Frontend Files(production)
variables:
storageAccountName: saomdspipeline
environment: production
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
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:prod
- 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: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_test
dependsOn: frontend_build_production
condition: succeeded('frontend_build_production')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
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: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: function_build
condition: succeeded('function_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--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: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
- job: swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
- job: migration
dependsOn: swap_slot
condition: succeeded('swap_slot')
displayName: DB migration
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: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name-ph1-enhance)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-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

@ -43,11 +43,14 @@ jobs:
targetType: inline targetType: inline
workingDirectory: dictation_server/.devcontainer workingDirectory: dictation_server/.devcontainer
script: | script: |
docker-compose -f pipeline-docker-compose.yml build sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
docker-compose -f pipeline-docker-compose.yml up -d sudo chmod +x /usr/local/bin/docker-compose
docker-compose exec -T dictation_server sudo npm ci docker-compose --version
docker-compose exec -T dictation_server sudo npm run migrate:up:test docker-compose -f pipeline-docker-compose.yml build
docker-compose exec -T dictation_server sudo npm run test docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build - job: backend_build
dependsOn: backend_test dependsOn: backend_test
condition: succeeded('backend_test') condition: succeeded('backend_test')
@ -170,9 +173,32 @@ jobs:
--type block \ --type block \
--overwrite \ --overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_build - job: function_test
dependsOn: frontend_build_production dependsOn: frontend_build_production
condition: succeeded('frontend_build_production') condition: succeeded('frontend_build_production')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image displayName: Build And Push Function Image
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline
@ -186,32 +212,6 @@ jobs:
command: ci command: ci
workingDir: dictation_function workingDir: dictation_function
verbose: false verbose: false
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
SecretsFilter: '*'
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
script: |
cd dictation_function
npm run test
env:
TENANT_NAME: xxxxxxxxxxxx
SIGNIN_FLOW_NAME: xxxxxxxxxxxx
ADB2C_TENANT_ID: $(adb2c-tenant-id)
ADB2C_CLIENT_ID: $(adb2c-client-id)
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
ADB2C_ORIGIN: xxxxxx
SENDGRID_API_KEY: $(sendgrid-api-key)
MAIL_FROM: xxxxxx
APP_DOMAIN: xxxxxxxxx
REDIS_HOST: xxxxxxxxxxxx
REDIS_PORT: 0
REDIS_PASSWORD: xxxxxxxxxxxx
- task: Docker@0 - task: Docker@0
displayName: build displayName: build
inputs: inputs:

View File

@ -1,70 +0,0 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const ErrorCodes = [
'E009999', // 汎用エラー
'E000101', // トークン形式不正エラー
'E000102', // トークン有効期限切れエラー
'E000103', // トークン非アクティブエラー
'E000104', // トークン署名エラー
'E000105', // トークン発行元エラー
'E000106', // トークンアルゴリズムエラー
'E000107', // トークン不足エラー
'E000108', // トークン権限エラー
'E000301', // ADB2Cへのリクエスト上限超過エラー
'E000401', // IPアドレス未設定エラー
'E000501', // リクエストID未設定エラー
'E010001', // パラメータ形式不正エラー
'E010201', // 未認証ユーザエラー
'E010202', // 認証済ユーザエラー
'E010203', // 管理ユーザ権限エラー
'E010204', // ユーザ不在エラー
'E010205', // DBのRoleが想定外の値エラー
'E010206', // DBのTierが想定外の値エラー
'E010207', // ユーザーのRole変更不可エラー
'E010208', // ユーザーの暗号化パスワード不足エラー
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
'E010301', // メールアドレス登録済みエラー
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
'E010501', // アカウント不在エラー
'E010502', // アカウント情報変更不可エラー
'E010503', // 代行操作不許可エラー
'E010504', // アカウントロックエラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー
'E010701', // Blobファイル不在エラー
'E010801', // ライセンス不在エラー
'E010802', // ライセンス取り込み済みエラー
'E010803', // ライセンス発行済みエラー
'E010804', // ライセンス不足エラー
'E010805', // ライセンス有効期限切れエラー
'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010812', // ライセンス未割当エラー
'E010908', // タイピストグループ不在エラー
'E010909', // タイピストグループ名重複エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
'E011004', // ワークタイプ使用中エラー
'E012001', // テンプレートファイル不在エラー
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
'E013002', // ワークフロー不在エラー
] as const;

View File

@ -1,10 +0,0 @@
import { errors } from './message';
import { ErrorCodeType, ErrorResponse } from './types/types';
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
const msg = errors[errorcode];
return {
code: errorcode,
message: msg,
};
};

View File

@ -1,59 +0,0 @@
import { Errors } from './types/types';
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: 'Internal Server Error.',
E000101: 'Token invalid format Error.',
E000102: 'Token expired Error.',
E000103: 'Token not before Error',
E000104: 'Token invalid signature Error.',
E000105: 'Token invalid issuer Error.',
E000106: 'Token invalid algorithm Error.',
E000107: 'Token is not exist Error.',
E000108: 'Token authority failed Error.',
E000301: 'ADB2C request limit exceeded Error',
E000401: 'IP address not found Error.',
E000501: 'Request ID not found Error.',
E010001: 'Param invalid format Error.',
E010201: 'Email not verified user Error.',
E010202: 'Email already verified user Error.',
E010203: 'Administrator Permissions Error.',
E010204: 'User not Found Error.',
E010205: 'Role from DB is unexpected value Error.',
E010206: 'Tier from DB is unexpected value Error.',
E010207: 'User role change not allowed Error.',
E010208: 'User encryption password not found Error.',
E010209: 'Accepted term not latest Error.',
E010301: 'This email user already created Error',
E010302: 'This AuthorId already used Error',
E010401: 'This PoNumber already used Error',
E010501: 'Account not Found Error.',
E010502: 'Account information cannot be changed Error.',
E010503: 'Delegation not allowed Error.',
E010504: 'Account is locked Error.',
E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',
E010701: 'File not found in Blob Storage Error.',
E010801: 'License not exist Error',
E010802: 'License already activated Error',
E010803: 'License already issued Error',
E010804: 'License shortage Error',
E010805: 'License is expired Error',
E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error',
E010809: 'Already license order status changed Error',
E010810: 'Cancellation period expired error',
E010811: 'Already license allocated Error',
E010812: 'License not allocated Error',
E010908: 'Typist Group not exist Error',
E010909: 'Typist Group name already exist Error',
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
E011004: 'WorkTypeID is in use Error',
E012001: 'Template file not found Error',
E013001: 'AuthorId and WorktypeId pair already exists Error',
E013002: 'Workflow not found Error',
};

View File

@ -1,15 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { ErrorCodes } from '../code';
export class ErrorResponse {
@ApiProperty()
message: string;
@ApiProperty()
code: string;
}
export type ErrorCodeType = (typeof ErrorCodes)[number];
export type Errors = {
[P in ErrorCodeType]: string;
};

View File

@ -26,6 +26,7 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"csv": "^6.3.6",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -3107,9 +3108,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.8.2", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"devOptional": true, "devOptional": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -4049,6 +4050,35 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/csv": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/csv/-/csv-6.3.6.tgz",
"integrity": "sha512-jsEsX2HhGp7xiwrJu5srQavKsh+HUJcCi78Ar3m4jlmFKRoTkkMy7ZZPP+LnQChmaztW+uj44oyfMb59daAs/Q==",
"dependencies": {
"csv-generate": "^4.3.1",
"csv-parse": "^5.5.3",
"csv-stringify": "^6.4.5",
"stream-transform": "^3.3.0"
},
"engines": {
"node": ">= 0.1.90"
}
},
"node_modules/csv-generate": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.1.tgz",
"integrity": "sha512-7YeeJq+44/I/O5N2sr2qBMcHZXhpfe38eh7DOFxyMtYO+Pir7kIfgFkW5MPksqKqqR6+/wX7UGoZm1Ot11151w=="
},
"node_modules/csv-parse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.3.tgz",
"integrity": "sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A=="
},
"node_modules/csv-stringify": {
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.5.tgz",
"integrity": "sha512-SPu1Vnh8U5EnzpNOi1NDBL5jU5Rx7DVHr15DNg9LXDTAbQlAVAmEbVt16wZvEW9Fu9Qt4Ji8kmeCJ2B1+4rFTQ=="
},
"node_modules/date-fns": { "node_modules/date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -8654,6 +8684,11 @@
"npm": ">=6" "npm": ">=6"
} }
}, },
"node_modules/stream-transform": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.0.tgz",
"integrity": "sha512-pG1NeDdmErNYKtvTpFayrEueAmL0xVU5wd22V7InGnatl4Ocq3HY7dcXIKj629kXvYQvglCC7CeDIGAlx1RNGA=="
},
"node_modules/streamsearch": { "node_modules/streamsearch": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -12333,9 +12368,9 @@
} }
}, },
"acorn": { "acorn": {
"version": "8.8.2", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"devOptional": true "devOptional": true
}, },
"acorn-import-assertions": { "acorn-import-assertions": {
@ -13027,6 +13062,32 @@
"which": "^2.0.1" "which": "^2.0.1"
} }
}, },
"csv": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/csv/-/csv-6.3.6.tgz",
"integrity": "sha512-jsEsX2HhGp7xiwrJu5srQavKsh+HUJcCi78Ar3m4jlmFKRoTkkMy7ZZPP+LnQChmaztW+uj44oyfMb59daAs/Q==",
"requires": {
"csv-generate": "^4.3.1",
"csv-parse": "^5.5.3",
"csv-stringify": "^6.4.5",
"stream-transform": "^3.3.0"
}
},
"csv-generate": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.1.tgz",
"integrity": "sha512-7YeeJq+44/I/O5N2sr2qBMcHZXhpfe38eh7DOFxyMtYO+Pir7kIfgFkW5MPksqKqqR6+/wX7UGoZm1Ot11151w=="
},
"csv-parse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.3.tgz",
"integrity": "sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A=="
},
"csv-stringify": {
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.5.tgz",
"integrity": "sha512-SPu1Vnh8U5EnzpNOi1NDBL5jU5Rx7DVHr15DNg9LXDTAbQlAVAmEbVt16wZvEW9Fu9Qt4Ji8kmeCJ2B1+4rFTQ=="
},
"date-fns": { "date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -16557,6 +16618,11 @@
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="
}, },
"stream-transform": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.0.tgz",
"integrity": "sha512-pG1NeDdmErNYKtvTpFayrEueAmL0xVU5wd22V7InGnatl4Ocq3HY7dcXIKj629kXvYQvglCC7CeDIGAlx1RNGA=="
},
"streamsearch": { "streamsearch": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",

View File

@ -45,7 +45,8 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"swagger-cli": "^4.0.4", "swagger-cli": "^4.0.4",
"typeorm": "^0.3.20" "typeorm": "^0.3.20",
"csv": "^6.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.17", "@types/express": "^4.17.17",

View File

@ -24,6 +24,12 @@ import { DeleteModule } from "./features/delete/delete.module";
import { DeleteRepositoryModule } from "./repositories/delete/delete.repository.module"; import { DeleteRepositoryModule } from "./repositories/delete/delete.repository.module";
import { DeleteController } from "./features/delete/delete.controller"; import { DeleteController } from "./features/delete/delete.controller";
import { DeleteService } from "./features/delete/delete.service"; import { DeleteService } from "./features/delete/delete.service";
import { TransferModule } from "./features/transfer/transfer.module";
import { TransferController } from "./features/transfer/transfer.controller";
import { TransferService } from "./features/transfer/transfer.service";
import { VerificationController } from "./features/verification/verification.controller";
import { VerificationService } from "./features/verification/verification.service";
import { VerificationModule } from "./features/verification/verification.module";
@Module({ @Module({
imports: [ imports: [
@ -37,7 +43,9 @@ import { DeleteService } from "./features/delete/delete.service";
AdB2cModule, AdB2cModule,
AccountsModule, AccountsModule,
UsersModule, UsersModule,
TransferModule,
RegisterModule, RegisterModule,
VerificationModule,
AccountsRepositoryModule, AccountsRepositoryModule,
UsersRepositoryModule, UsersRepositoryModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
@ -61,8 +69,22 @@ import { DeleteService } from "./features/delete/delete.service";
inject: [ConfigService], inject: [ConfigService],
}), }),
], ],
controllers: [RegisterController, AccountsController, UsersController, DeleteController], controllers: [
providers: [RegisterService, AccountsService, UsersService, DeleteService], RegisterController,
AccountsController,
UsersController,
DeleteController,
TransferController,
VerificationController,
],
providers: [
RegisterService,
AccountsService,
UsersService,
DeleteService,
TransferService,
VerificationService,
],
}) })
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {

View File

@ -18,7 +18,8 @@ export const makePassword = (): string => {
let autoGeneratedPassword: string = ""; let autoGeneratedPassword: string = "";
while (!valid) { while (!valid) {
// パスワードをランダムに決定 autoGeneratedPassword = "";
// パスワードをランダムに決定+
while (autoGeneratedPassword.length < passLength) { while (autoGeneratedPassword.length < passLength) {
// 上で決定したcharsの中からランダムに1文字ずつ追加 // 上で決定したcharsの中からランダムに1文字ずつ追加
const index = Math.floor(Math.random() * chars.length); const index = Math.floor(Math.random() * chars.length);
@ -30,6 +31,11 @@ export const makePassword = (): string => {
valid = valid =
autoGeneratedPassword.length == passLength && autoGeneratedPassword.length == passLength &&
charaTypePattern.test(autoGeneratedPassword); charaTypePattern.test(autoGeneratedPassword);
if (!valid) {
// autoGeneratedPasswordをログに出す
console.log("Password is not valid");
console.log(autoGeneratedPassword);
}
} }
return autoGeneratedPassword; return autoGeneratedPassword;
}; };

View File

@ -8,8 +8,8 @@ export class csvInputFile {
last_name: string; last_name: string;
country: string; country: string;
state: string; state: string;
start_date: Date; start_date: string;
expired_date: Date; expired_date: string;
user_email: string; user_email: string;
author_id: string; author_id: string;
recording_mode: string; recording_mode: string;
@ -34,7 +34,12 @@ export class csvInputFile {
wt19: string; wt19: string;
wt20: string; wt20: string;
} }
export class AccountsOutputFileStep1 {
export class csvInputFileWithRow extends csvInputFile {
row: number;
}
export class AccountsFileType {
accountId: number; accountId: number;
type: string; type: string;
companyName: string; companyName: string;
@ -43,9 +48,11 @@ export class AccountsOutputFileStep1 {
adminName: string; adminName: string;
adminMail: string; adminMail: string;
userId: number; userId: number;
role: string;
authorId: string;
} }
export class AccountsOutputFile { export class AccountsFile {
accountId: number; accountId: number;
type: number; type: number;
companyName: string; companyName: string;
@ -54,18 +61,11 @@ export class AccountsOutputFile {
adminName: string; adminName: string;
adminMail: string; adminMail: string;
userId: number; userId: number;
role: string;
authorId: string;
} }
export class AccountsInputFile {
accountId: number; export class UsersFile {
type: number;
companyName: string;
country: string;
dealerAccountId?: number;
adminName: string;
adminMail: string;
userId: number;
}
export class UsersOutputFile {
accountId: number; accountId: number;
userId: number; userId: number;
name: string; name: string;
@ -74,23 +74,7 @@ export class UsersOutputFile {
email: string; email: string;
} }
export class UsersInputFile { export class LicensesFile {
accountId: number;
userId: number;
name: string;
role: string;
authorId: string;
email: string;
}
export class LicensesOutputFile {
expiry_date: string;
account_id: number;
type: string;
status: string;
allocated_user_id?: number;
}
export class LicensesInputFile {
expiry_date: string; expiry_date: string;
account_id: number; account_id: number;
type: string; type: string;
@ -98,16 +82,12 @@ export class LicensesInputFile {
allocated_user_id?: number; allocated_user_id?: number;
} }
export class WorktypesOutputFile { export class WorktypesFile {
account_id: number;
custom_worktype_id: string;
}
export class WorktypesInputFile {
account_id: number; account_id: number;
custom_worktype_id: string; custom_worktype_id: string;
} }
export class CardLicensesInputFile { export class CardLicensesFile {
license_id: number; license_id: number;
issue_id: number; issue_id: number;
card_license_key: string; card_license_key: string;
@ -118,10 +98,26 @@ export class CardLicensesInputFile {
updated_by?: string; updated_by?: string;
} }
export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] { export class AccountsMappingFile {
return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item)); accountIdText: string;
accountIdNumber: number;
} }
export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
export class VerificationResultDetails {
input: string;
inputRow: number;
diffTargetTable: string;
columnName: string;
fileData: string;
databaseData: string;
reason: string;
}
export function isAccountsFileArray(obj: any): obj is AccountsFile[] {
return Array.isArray(obj) && obj.every((item) => isAccountsFile(item));
}
export function isAccountsFile(obj: any): obj is AccountsFile {
return ( return (
typeof obj === "object" && typeof obj === "object" &&
obj !== null && obj !== null &&
@ -134,21 +130,27 @@ export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
"country" in obj && "country" in obj &&
typeof obj.country === "string" && typeof obj.country === "string" &&
("dealerAccountId" in obj ("dealerAccountId" in obj
? typeof obj.dealerAccountId === "number" ? obj.dealerAccountId === null || typeof obj.dealerAccountId === "number"
: true) && : true) &&
"adminName" in obj && "adminName" in obj &&
typeof obj.adminName === "string" && typeof obj.adminName === "string" &&
"adminMail" in obj && "adminMail" in obj &&
typeof obj.adminMail === "string" && typeof obj.adminMail === "string" &&
"userId" in obj && "userId" in obj &&
typeof obj.userId === "number" typeof obj.userId === "number" &&
("role" in obj
? obj.role === null || typeof obj.role === "string"
: true) &&
("authorId" in obj
? obj.authorId === null || typeof obj.authorId === "string"
: true)
); );
} }
export function isUsersInputFileArray(obj: any): obj is UsersInputFile[] { export function isUsersFileArray(obj: any): obj is UsersFile[] {
return Array.isArray(obj) && obj.every((item) => isUsersInputFile(item)); return Array.isArray(obj) && obj.every((item) => isUsersFile(item));
} }
export function isUsersInputFile(obj: any): obj is UsersInputFile { export function isUsersFile(obj: any): obj is UsersFile {
return ( return (
typeof obj === "object" && typeof obj === "object" &&
obj !== null && obj !== null &&
@ -167,10 +169,10 @@ export function isUsersInputFile(obj: any): obj is UsersInputFile {
); );
} }
export function isLicensesInputFileArray(obj: any): obj is LicensesInputFile[] { export function isLicensesFileArray(obj: any): obj is LicensesFile[] {
return Array.isArray(obj) && obj.every((item) => isLicensesInputFile(item)); return Array.isArray(obj) && obj.every((item) => isLicensesFile(item));
} }
export function isLicensesInputFile(obj: any): obj is LicensesInputFile { export function isLicensesFile(obj: any): obj is LicensesFile {
return ( return (
typeof obj === "object" && typeof obj === "object" &&
obj !== null && obj !== null &&
@ -187,12 +189,10 @@ export function isLicensesInputFile(obj: any): obj is LicensesInputFile {
); );
} }
export function isWorktypesInputFileArray( export function isWorktypesFileArray(obj: any): obj is WorktypesFile[] {
obj: any return Array.isArray(obj) && obj.every((item) => isWorktypesFile(item));
): obj is WorktypesInputFile[] {
return Array.isArray(obj) && obj.every((item) => isWorktypesInputFile(item));
} }
export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile { export function isWorktypesFile(obj: any): obj is WorktypesFile {
return ( return (
typeof obj === "object" && typeof obj === "object" &&
obj !== null && obj !== null &&
@ -203,16 +203,10 @@ export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile {
); );
} }
export function isCardLicensesInputFileArray( export function isCardLicensesFileArray(obj: any): obj is CardLicensesFile[] {
obj: any return Array.isArray(obj) && obj.every((item) => isCardLicensesFile(item));
): obj is CardLicensesInputFile[] {
return (
Array.isArray(obj) && obj.every((item) => isCardLicensesInputFile(item))
);
} }
export function isCardLicensesInputFile( export function isCardLicensesFile(obj: any): obj is CardLicensesFile {
obj: any
): obj is CardLicensesInputFile {
return ( return (
typeof obj === "object" && typeof obj === "object" &&
obj !== null && obj !== null &&
@ -229,3 +223,65 @@ export function isCardLicensesInputFile(
(obj.updated_by === null || typeof obj.updated_by === "string") (obj.updated_by === null || typeof obj.updated_by === "string")
); );
} }
export function isAccountsMappingFileArray(
obj: any
): obj is AccountsMappingFile[] {
return Array.isArray(obj) && obj.every((item) => isAccountsMappingFile(item));
}
export function isAccountsMappingFile(obj: any): obj is AccountsMappingFile {
return (
typeof obj === "object" &&
obj !== null &&
"accountIdText" in obj &&
"accountIdNumber" in obj &&
typeof obj.accountIdText === "string" &&
typeof obj.accountIdNumber === "number"
);
}
export function isCsvInputFileForValidateArray(obj: any): obj is csvInputFile[] {
return (
Array.isArray(obj) && obj.every((item) => isCsvInputFileForValidate(item))
);
}
export function isCsvInputFileForValidate(obj: any): obj is csvInputFile {
return (
typeof obj === "object" &&
"type" in obj &&
"account_id" in obj &&
"parent_id" in obj &&
"email" in obj &&
"company_name" in obj &&
"first_name" in obj &&
"last_name" in obj &&
"country" in obj &&
"state" in obj &&
"start_date" in obj &&
"expired_date" in obj &&
"user_email" in obj &&
"author_id" in obj &&
"recording_mode" in obj &&
"wt1" in obj &&
"wt2" in obj &&
"wt3" in obj &&
"wt4" in obj &&
"wt5" in obj &&
"wt6" in obj &&
"wt7" in obj &&
"wt8" in obj &&
"wt9" in obj &&
"wt10" in obj &&
"wt11" in obj &&
"wt12" in obj &&
"wt13" in obj &&
"wt14" in obj &&
"wt15" in obj &&
"wt16" in obj &&
"wt17" in obj &&
"wt18" in obj &&
"wt19" in obj &&
"wt20" in obj
);
}

View File

@ -343,7 +343,7 @@ export const MIGRATION_TYPE = {
export const COUNTRY_LIST = [ export const COUNTRY_LIST = [
{ value: "CA", label: "Canada" }, { value: "CA", label: "Canada" },
{ value: "KY", label: "Cayman Islands" }, { value: "KY", label: "Cayman Islands" },
{ value: "US", label: "U.S.A." }, { value: "US", label: "United States" },
{ value: "AU", label: "Australia" }, { value: "AU", label: "Australia" },
{ value: "NZ", label: "New Zealand" }, { value: "NZ", label: "New Zealand" },
{ value: "AT", label: "Austria" }, { value: "AT", label: "Austria" },
@ -351,7 +351,7 @@ export const COUNTRY_LIST = [
{ value: "BG", label: "Bulgaria" }, { value: "BG", label: "Bulgaria" },
{ value: "HR", label: "Croatia" }, { value: "HR", label: "Croatia" },
{ value: "CY", label: "Cyprus" }, { value: "CY", label: "Cyprus" },
{ value: "CZ", label: "Czech Republic" }, { value: "CZ", label: "Czech" },
{ value: "DK", label: "Denmark" }, { value: "DK", label: "Denmark" },
{ value: "EE", label: "Estonia" }, { value: "EE", label: "Estonia" },
{ value: "FI", label: "Finland" }, { value: "FI", label: "Finland" },

View File

@ -37,6 +37,7 @@ export class AccountsService {
password: string, password: string,
username: string, username: string,
role: string, role: string,
authorId: string,
acceptedEulaVersion: string, acceptedEulaVersion: string,
acceptedPrivacyNoticeVersion: string, acceptedPrivacyNoticeVersion: string,
acceptedDpaVersion: string, acceptedDpaVersion: string,
@ -78,6 +79,7 @@ export class AccountsService {
HttpStatus.INTERNAL_SERVER_ERROR HttpStatus.INTERNAL_SERVER_ERROR
); );
} }
this.logger.log("idpにユーザーを作成成功");
// メールアドレス重複エラー // メールアドレス重複エラー
if (isConflictError(externalUser)) { if (isConflictError(externalUser)) {
@ -89,6 +91,7 @@ export class AccountsService {
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
); );
} }
this.logger.log("メールアドレスは重複していません");
let account: Account; let account: Account;
let user: User; let user: User;
@ -103,6 +106,7 @@ export class AccountsService {
type, type,
externalUser.sub, externalUser.sub,
role, role,
authorId,
accountId, accountId,
userId, userId,
acceptedEulaVersion, acceptedEulaVersion,
@ -136,6 +140,7 @@ export class AccountsService {
account.id, account.id,
country country
); );
this.logger.log("コンテナー作成成功");
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error( this.logger.error(

View File

@ -11,6 +11,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express"; import { Request } from "express";
import { DeleteService } from "./delete.service"; import { DeleteService } from "./delete.service";
import { DeleteResponse } from "./types/types"; import { DeleteResponse } from "./types/types";
import { makeContext } from "src/common/log";
@ApiTags("delete") @ApiTags("delete")
@Controller("delete") @Controller("delete")
@ -33,7 +34,9 @@ export class DeleteController {
}) })
@Post() @Post()
async deleteData(): Promise<{}> { async deleteData(): Promise<{}> {
await this.deleteService.deleteData(); const context = makeContext("tool", "delete");
await this.deleteService.deleteData(context);
return {}; return {};
} }
} }

View File

@ -3,6 +3,7 @@ import { DeleteRepositoryService } from "../../repositories/delete/delete.reposi
import { makeErrorResponse } from "../../common/errors/makeErrorResponse"; import { makeErrorResponse } from "../../common/errors/makeErrorResponse";
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 { Context } from "../../common/log";
@Injectable() @Injectable()
export class DeleteService { export class DeleteService {
@ -11,27 +12,41 @@ export class DeleteService {
private readonly deleteRepositoryService: DeleteRepositoryService, private readonly deleteRepositoryService: DeleteRepositoryService,
private readonly blobstorageService: BlobstorageService, private readonly blobstorageService: BlobstorageService,
private readonly adB2cService: AdB2cService private readonly adB2cService: AdB2cService
) {} ) { }
/** /**
* *
* @returns data * @returns data
*/ */
async deleteData(): Promise<void> { async deleteData(context: Context): Promise<void> {
this.logger.log(`[IN] ${this.deleteData.name}`); this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.deleteData.name}`
);
try { try {
// BlobStorageからデータを削除する // BlobStorageからデータを削除する
await this.blobstorageService.deleteContainers(); await this.blobstorageService.deleteContainers(context);
// ADB2Cからユーザ情報を取得する // 100件ずつのユーザー取得なのですべて削除するまでループする
const users = await this.adB2cService.getUsers(); for (let i = 0; i < 500; i++) {
const externalIds = users.map((user) => user.id); // ADB2Cからユーザ情報を取得する
await this.adB2cService.deleteUsers(externalIds); const { users, hasNext } = await this.adB2cService.getUsers(context);
const externalIds = users.map((user) => user.id);
await this.adB2cService.deleteUsers(context, externalIds);
// 削除していないユーザーがいない場合はループを抜ける
if (!hasNext) {
break;
}
}
// データベースからデータを削除する // データベースからデータを削除する
await this.deleteRepositoryService.deleteData(); await this.deleteRepositoryService.deleteData();
// AutoIncrementの値をリセットする // AutoIncrementの値をリセットする
await this.deleteRepositoryService.resetAutoIncrement(); await this.deleteRepositoryService.resetAutoIncrement();
// 初期データを挿入する
await this.deleteRepositoryService.insertInitData(context);
} catch (e) { } catch (e) {
this.logger.error(`error=${e}`); this.logger.error(`error=${e}`);
if (e instanceof Error) { if (e instanceof Error) {

View File

@ -17,11 +17,11 @@ import { AccountsService } from "../accounts/accounts.service";
import { UsersService } from "../users/users.service"; import { UsersService } from "../users/users.service";
import { makeContext } from "../../common/log"; import { makeContext } from "../../common/log";
import { import {
isAccountsInputFileArray, isAccountsFileArray,
isUsersInputFileArray, isUsersFileArray,
isLicensesInputFileArray, isLicensesFileArray,
isWorktypesInputFileArray, isWorktypesFileArray,
isCardLicensesInputFileArray, isCardLicensesFileArray,
} from "../../common/types/types"; } from "../../common/types/types";
import { makePassword } from "../../common/password/password"; import { makePassword } from "../../common/password/password";
import { import {
@ -73,13 +73,24 @@ export class RegisterController {
const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json"; const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json";
// ファイル存在チェックと読み込み // ファイル存在チェックと読み込み
if ( // どのファイルがないのかわからないのでそれぞれに存在しない場合はエラーを出す
!fs.existsSync(accouncsFileFullPath) || if (!fs.existsSync(accouncsFileFullPath)) {
!fs.existsSync(usersFileFullPath) || this.logger.error(`file not exists from ${inputFilePath}`);
!fs.existsSync(licensesFileFullPath) || throw new Error(`file not exists from ${inputFilePath}`);
!fs.existsSync(worktypesFileFullPath) || }
!fs.existsSync(cardLicensesFileFullPath) if (!fs.existsSync(usersFileFullPath)) {
) { this.logger.error(`file not exists from ${inputFilePath}`);
throw new Error(`file not exists from ${inputFilePath}`);
}
if (!fs.existsSync(licensesFileFullPath)) {
this.logger.error(`file not exists from ${inputFilePath}`);
throw new Error(`file not exists from ${inputFilePath}`);
}
if (!fs.existsSync(worktypesFileFullPath)) {
this.logger.error(`file not exists from ${inputFilePath}`);
throw new Error(`file not exists from ${inputFilePath}`);
}
if (!fs.existsSync(cardLicensesFileFullPath)) {
this.logger.error(`file not exists from ${inputFilePath}`); this.logger.error(`file not exists from ${inputFilePath}`);
throw new Error(`file not exists from ${inputFilePath}`); throw new Error(`file not exists from ${inputFilePath}`);
} }
@ -90,34 +101,53 @@ export class RegisterController {
); );
// 型ガードaccount // 型ガードaccount
if (!isAccountsInputFileArray(accountsObject)) { if (!isAccountsFileArray(accountsObject)) {
throw new Error("input file is not accountsInputFiles"); throw new Error("input file is not AccountsFiles");
} }
for (const accountsInputFile of accountsObject) { for (const AccountsFile of accountsObject) {
this.logger.log("ランダムパスワード生成開始");
// ランダムなパスワードを生成する // ランダムなパスワードを生成する
const ramdomPassword = makePassword(); const ramdomPassword = makePassword();
this.logger.log("ランダムパスワード生成完了");
// roleの設定
// roleの値がnullなら"none"、null以外ならroleの値、
// また、roleの値が"author"なら"author"を設定
let role: string;
let authorId: string;
if (AccountsFile.role === null) {
role = USER_ROLES.NONE;
authorId = null;
} else if (AccountsFile.role === USER_ROLES.AUTHOR) {
role = USER_ROLES.AUTHOR;
authorId = AccountsFile.authorId;
} else {
// ありえないが、roleの値が"none"または"author"の文字列以外の場合はエラーを返す
throw new Error("Invalid role value");
}
this.logger.log("account生成開始");
await this.accountsService.createAccount( await this.accountsService.createAccount(
context, context,
accountsInputFile.companyName, AccountsFile.companyName,
accountsInputFile.country, AccountsFile.country,
accountsInputFile.dealerAccountId, AccountsFile.dealerAccountId,
accountsInputFile.adminMail, AccountsFile.adminMail,
ramdomPassword, ramdomPassword,
accountsInputFile.adminName, AccountsFile.adminName,
"none", role,
authorId,
null, null,
null, null,
null, null,
accountsInputFile.type, AccountsFile.type,
accountsInputFile.accountId, AccountsFile.accountId,
accountsInputFile.userId AccountsFile.userId
); );
// ratelimit対応のためsleepを行う // ratelimit対応のためsleepを行う
await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC); await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC);
} }
// const accountsInputFiles = accountsObject as AccountsInputFile[]; // const AccountsFiles = accountsObject as AccountsFile[];
// ユーザの登録用ファイル読み込み // ユーザの登録用ファイル読み込み
const usersObject = JSON.parse( const usersObject = JSON.parse(
@ -125,24 +155,24 @@ export class RegisterController {
); );
// 型ガードuser // 型ガードuser
if (!isUsersInputFileArray(usersObject)) { if (!isUsersFileArray(usersObject)) {
throw new Error("input file is not usersInputFiles"); throw new Error("input file is not UsersFiles");
} }
for (const usersInputFile of usersObject) { for (const UsersFile of usersObject) {
this.logger.log(usersInputFile.name); this.logger.log(UsersFile.name);
await this.usersService.createUser( await this.usersService.createUser(
context, context,
usersInputFile.name, UsersFile.name,
usersInputFile.role === USER_ROLES.AUTHOR UsersFile.role === USER_ROLES.AUTHOR
? USER_ROLES.AUTHOR ? USER_ROLES.AUTHOR
: USER_ROLES.NONE, : USER_ROLES.NONE,
usersInputFile.email, UsersFile.email,
true, true,
true, true,
usersInputFile.accountId, UsersFile.accountId,
usersInputFile.userId, UsersFile.userId,
usersInputFile.authorId, UsersFile.authorId,
false, false,
null, null,
true true
@ -157,8 +187,8 @@ export class RegisterController {
); );
// 型ガードlicense // 型ガードlicense
if (!isLicensesInputFileArray(licensesObject)) { if (!isLicensesFileArray(licensesObject)) {
throw new Error("input file is not licensesInputFiles"); throw new Error("input file is not LicensesFiles");
} }
// ワークタイプの登録用ファイル読み込み // ワークタイプの登録用ファイル読み込み
@ -167,8 +197,8 @@ export class RegisterController {
); );
// 型ガードWorktypes // 型ガードWorktypes
if (!isWorktypesInputFileArray(worktypesObject)) { if (!isWorktypesFileArray(worktypesObject)) {
throw new Error("input file is not WorktypesInputFiles"); throw new Error("input file is not WorktypesFiles");
} }
// カードライセンスの登録用ファイル読み込み // カードライセンスの登録用ファイル読み込み
@ -177,8 +207,8 @@ export class RegisterController {
); );
// 型ガードcardLicenses // 型ガードcardLicenses
if (!isCardLicensesInputFileArray(cardLicensesObject)) { if (!isCardLicensesFileArray(cardLicensesObject)) {
throw new Error("input file is not cardLicensesInputFiles"); throw new Error("input file is not cardLicensesFiles");
} }
// ライセンス・ワークタイプ・カードライセンスの登録 // ライセンス・ワークタイプ・カードライセンスの登録

View File

@ -1,9 +1,9 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { Context } from "../../common/log"; import { Context } from "../../common/log";
import { import {
LicensesInputFile, LicensesFile,
WorktypesInputFile, WorktypesFile,
CardLicensesInputFile, CardLicensesFile,
} from "../../common/types/types"; } from "../../common/types/types";
import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service"; import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service";
import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service"; import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service";
@ -22,9 +22,9 @@ export class RegisterService {
*/ */
async registLicenseAndWorktypeData( async registLicenseAndWorktypeData(
context: Context, context: Context,
licensesInputFiles: LicensesInputFile[], LicensesFiles: LicensesFile[],
worktypesInputFiles: WorktypesInputFile[], WorktypesFiles: WorktypesFile[],
cardlicensesInputFiles: CardLicensesInputFile[] cardLicensesFiles: CardLicensesFile[]
): Promise<void> { ): Promise<void> {
// パラメータ内容が長大なのでログには出さない // パラメータ内容が長大なのでログには出さない
this.logger.log( this.logger.log(
@ -35,20 +35,17 @@ export class RegisterService {
try { try {
this.logger.log("Licenses register start"); this.logger.log("Licenses register start");
await this.licensesRepository.insertLicenses(context, licensesInputFiles); await this.licensesRepository.insertLicenses(context, LicensesFiles);
this.logger.log("Licenses register end"); this.logger.log("Licenses register end");
this.logger.log("Worktypes register start"); this.logger.log("Worktypes register start");
await this.worktypesRepository.createWorktype( await this.worktypesRepository.createWorktype(context, WorktypesFiles);
context,
worktypesInputFiles
);
this.logger.log("Worktypes register end"); this.logger.log("Worktypes register end");
this.logger.log("CardLicenses register start"); this.logger.log("CardLicenses register start");
await this.licensesRepository.insertCardLicenses( await this.licensesRepository.insertCardLicenses(
context, context,
cardlicensesInputFiles cardLicensesFiles
); );
this.logger.log("CardLicenses register end"); this.logger.log("CardLicenses register end");
} catch (e) { } catch (e) {

View File

@ -11,25 +11,16 @@ import fs from "fs";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express"; import { Request } from "express";
import { transferRequest, transferResponse } from "./types/types"; import { transferRequest, transferResponse } from "./types/types";
import { transferService } from "./transfer.service"; import { TransferService } from "./transfer.service";
import { makeContext } from "../../common/log"; import { makeContext } from "../../common/log";
import { csvInputFile } from "../../common/types/types"; import { csvInputFile, AccountsMappingFile } from "../../common/types/types";
import { makeErrorResponse } from "src/common/errors/makeErrorResponse"; import { makeErrorResponse } from "src/common/errors/makeErrorResponse";
import { import { AUTO_INCREMENT_START } from "../../constants";
COUNTRY_LIST,
MIGRATION_TYPE,
TIERS,
WORKTYPE_MAX_COUNT,
RECORDING_MODE,
LICENSE_ALLOCATED_STATUS,
USER_ROLES,
AUTO_INCREMENT_START,
} from "../../../src/constants";
@ApiTags("transfer") @ApiTags("transfer")
@Controller("transfer") @Controller("transfer")
export class transferController { export class TransferController {
private readonly logger = new Logger(transferController.name); private readonly logger = new Logger(TransferController.name);
constructor(private readonly transferService: transferService) {} constructor(private readonly transferService: TransferService) {}
@Post() @Post()
@ApiResponse({ @ApiResponse({
@ -57,16 +48,16 @@ export class transferController {
); );
try { try {
// 読み込みファイルのフルパス // 読み込みファイルのフルパス
const csvFileFullPath = inputFilePath + ".csv"; const accouncsFileFullPath = inputFilePath + "Account_transition.csv";
// ファイル存在チェックと読み込み // ファイル存在チェックと読み込み
if (!fs.existsSync(csvFileFullPath)) { if (!fs.existsSync(accouncsFileFullPath)) {
this.logger.error(`file not exists from ${inputFilePath}`); this.logger.error(`file not exists from ${inputFilePath}`);
throw new Error(`file not exists from ${inputFilePath}`); throw new Error(`file not exists from ${inputFilePath}`);
} }
// CSVファイルを全行読み込む // CSVファイルを全行読み込む
const inputFile = fs.readFileSync(csvFileFullPath, "utf-8"); const inputFile = fs.readFileSync(accouncsFileFullPath, "utf-8");
// レコードごとに分割 // レコードごとに分割
const csvInputFileLines = inputFile.split("\n"); const csvInputFileLines = inputFile.split("\n");
@ -77,45 +68,78 @@ export class transferController {
// 項目ごとに切り分ける // 項目ごとに切り分ける
let csvInputFile: csvInputFile[] = []; let csvInputFile: csvInputFile[] = [];
csvInputFileLines.forEach((line) => { csvInputFileLines.forEach((line) => {
// 項目にカンマが入っている場合を考慮して、ダブルクォーテーションで囲まれた部分を一つの項目として扱う
const regExp = /"[^"]*"/g;
const matchList = line.match(regExp);
if (matchList) {
matchList.forEach((match) => {
// カンマを\に変換
const replaced = match.replace(/,/g, "\\");
line = line.replace(match, replaced);
});
}
const data = line.split(","); const data = line.split(",");
csvInputFile.push({ // ダブルクォーテーションを削除
type: data[0], data.forEach((value, index) => {
account_id: data[1], data[index] = value.replace(/"/g, "");
parent_id: data[2],
email: data[3],
company_name: data[4],
first_name: data[5],
last_name: data[6],
country: data[7],
state: data[8],
start_date: new Date(data[9]),
expired_date: new Date(data[10]),
user_email: data[11],
author_id: data[12],
recording_mode: data[13],
wt1: data[14],
wt2: data[15],
wt3: data[16],
wt4: data[17],
wt5: data[18],
wt6: data[19],
wt7: data[20],
wt8: data[21],
wt9: data[22],
wt10: data[23],
wt11: data[24],
wt12: data[25],
wt13: data[26],
wt14: data[27],
wt15: data[28],
wt16: data[29],
wt17: data[30],
wt18: data[31],
wt19: data[32],
wt20: data[33],
}); });
// "\r"を削除
data[data.length - 1] = data[data.length - 1].replace(/\r/g, "");
// dataの要素数が34(csvInputFileの要素数)より多い場合、フォーマット不一致エラー(移行元はworktypeの数が20より多く設定できるので理論上は存在する)
// worktypeの数の確認を促すエラーを出す
if (data.length > 34) {
this.logger.error(
`[${context.getTrackingId()}] format error.please check the number of elements in worktype. data=${data}`
);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.BAD_REQUEST
);
}
// data[1]がundefinedの場合、配列には格納しない
if (data[1] !== undefined) {
// バックスラッシュをカンマに戻す
data.forEach((value, index) => {
data[index] = value.replace(/\\/g, ",");
});
csvInputFile.push({
type: data[0],
account_id: data[1],
parent_id: data[2],
email: data[3],
company_name: data[4],
first_name: data[5],
last_name: data[6],
country: data[7],
state: data[8],
start_date: data[9],
expired_date: data[10],
user_email: data[11],
author_id: data[12],
recording_mode: data[13],
wt1: data[14],
wt2: data[15],
wt3: data[16],
wt4: data[17],
wt5: data[18],
wt6: data[19],
wt7: data[20],
wt8: data[21],
wt9: data[22],
wt10: data[23],
wt11: data[24],
wt12: data[25],
wt13: data[26],
wt14: data[27],
wt15: data[28],
wt16: data[29],
wt17: data[30],
wt18: data[31],
wt19: data[32],
wt20: data[33],
});
}
}); });
// 各データのバリデーションチェック // 各データのバリデーションチェック
await this.transferService.validateInputData(context, csvInputFile); await this.transferService.validateInputData(context, csvInputFile);
@ -127,36 +151,63 @@ export class transferController {
accountIdListArray.forEach((accountId, index) => { accountIdListArray.forEach((accountId, index) => {
accountIdMap.set(accountId, index + AUTO_INCREMENT_START); accountIdMap.set(accountId, index + AUTO_INCREMENT_START);
}); });
// アカウントID numberとstring対応表の出力
const accountsMappingFiles: AccountsMappingFile[] = [];
accountIdMap.forEach((value, key) => {
const accountsMappingFile = new AccountsMappingFile();
accountsMappingFile.accountIdNumber = value;
accountsMappingFile.accountIdText = key;
accountsMappingFiles.push(accountsMappingFile);
});
fs.writeFileSync(
`${inputFilePath}account_map.json`,
JSON.stringify(accountsMappingFiles)
);
// CSVファイルの変換 // CSVファイルの変換
const transferResponse = await this.transferService.registInputData( const transferResponseCsv = await this.transferService.transferInputData(
context, context,
csvInputFile, csvInputFile,
accountIdMap accountIdMap
); );
// countryを除いた階層の再配置 // countryを除いた階層の再配置
const accountsOutputFileStep1Lines = const AccountsFileTypeLines = transferResponseCsv.accountsFileTypeLines;
transferResponse.accountsOutputFileStep1Lines; const AccountsFile = await this.transferService.relocateHierarchy(
const accountsOutputFile = await this.transferService.relocateHierarchy(
context, context,
accountsOutputFileStep1Lines AccountsFileTypeLines
); );
const UsersFile = transferResponseCsv.usersFileLines;
const LicensesFile = transferResponseCsv.licensesFileLines;
// メールアドレスの重複を削除 // メールアドレスの重複を削除
// デモライセンスの削除 const resultDuplicateEmail =
// いったんこのままコミットしてテストを実施する await this.transferService.removeDuplicateEmail(
context,
AccountsFile,
UsersFile,
LicensesFile
);
// transferResponseをつのJSONファイルの出力する(出力先はinputと同じにする) // AuthorIDが重複している場合通番を付与する
const transferDuplicateAuthorResultUsers =
await this.transferService.transferDuplicateAuthor(
context,
resultDuplicateEmail.accountsFileLines,
resultDuplicateEmail.usersFileLines
);
// transferResponseCsvをつのJSONファイルの出力する(出力先はinputと同じにする)
const outputFilePath = body.inputFilePath; const outputFilePath = body.inputFilePath;
const usersOutputFile = transferResponse.usersOutputFileLines; const WorktypesFile = transferResponseCsv.worktypesFileLines;
const licensesOutputFile = transferResponse.licensesOutputFileLines;
const worktypesOutputFile = transferResponse.worktypesOutputFileLines;
this.transferService.outputJsonFile( this.transferService.outputJsonFile(
context, context,
outputFilePath, outputFilePath,
accountsOutputFile, resultDuplicateEmail.accountsFileLines,
usersOutputFile, transferDuplicateAuthorResultUsers,
licensesOutputFile, resultDuplicateEmail.licensesFileLines,
worktypesOutputFile WorktypesFile
); );
return {}; return {};
} catch (e) { } catch (e) {

View File

@ -1,9 +1,9 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { transferController } from "./transfer.controller"; import { TransferController } from "./transfer.controller";
import { transferService } from "./transfer.service"; import { TransferService } from "./transfer.service";
@Module({ @Module({
imports: [], imports: [],
controllers: [transferController], controllers: [TransferController],
providers: [transferService], providers: [TransferService],
}) })
export class transferModule {} export class TransferModule {}

View File

@ -1,12 +1,12 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { Context } from "../../common/log"; import { Context } from "../../common/log";
import { import {
AccountsOutputFileStep1, AccountsFileType,
UsersOutputFile, UsersFile,
LicensesOutputFile, LicensesFile,
WorktypesOutputFile, WorktypesFile,
csvInputFile, csvInputFile,
AccountsOutputFile, AccountsFile,
} from "../../common/types/types"; } from "../../common/types/types";
import { import {
COUNTRY_LIST, COUNTRY_LIST,
@ -18,49 +18,64 @@ import {
USER_ROLES, USER_ROLES,
SWITCH_FROM_TYPE, SWITCH_FROM_TYPE,
} from "src/constants"; } from "src/constants";
import { registInputDataResponse } from "./types/types"; import {
registInputDataResponse,
removeDuplicateEmailResponse,
} from "./types/types";
import fs from "fs"; import fs from "fs";
import { makeErrorResponse } from "src/common/error/makeErrorResponse";
@Injectable() @Injectable()
export class transferService { export class TransferService {
constructor() {} constructor() {}
private readonly logger = new Logger(transferService.name); private readonly logger = new Logger(TransferService.name);
/** /**
* Regist Data * Transfer Input Data
* @param OutputFilePath: string * @param OutputFilePath: string
* @param csvInputFile: csvInputFile[] * @param csvInputFile: csvInputFile[]
*/ */
async registInputData( async transferInputData(
context: Context, context: Context,
csvInputFile: csvInputFile[], csvInputFile: csvInputFile[],
accountIdMap: Map<string, number> accountIdMap: Map<string, number>
): Promise<registInputDataResponse> { ): Promise<registInputDataResponse> {
// パラメータ内容が長大なのでログには出さない // パラメータ内容が長大なのでログには出さない
this.logger.log( this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.registInputData.name}` `[IN] [${context.getTrackingId()}] ${this.transferInputData.name}`
); );
try { try {
let accountsOutputFileStep1Lines: AccountsOutputFileStep1[] = []; let accountsFileTypeLines: AccountsFileType[] = [];
let usersOutputFileLines: UsersOutputFile[] = []; let usersFileLines: UsersFile[] = [];
let licensesOutputFileLines: LicensesOutputFile[] = []; let licensesFileLines: LicensesFile[] = [];
let worktypesOutputFileLines: WorktypesOutputFile[] = []; let worktypesFileLines: WorktypesFile[] = [];
let errorArray: string[] = [];
let userIdIndex = 0; let userIdIndex = 0;
// authorIdとuserIdの対応関係を保持するMapを定義
const authorIdToUserIdMap: Map<string, number> = new Map();
// countryのリストを生成
const countryAccounts = csvInputFile.filter(
(item) => item.type === "Country"
);
// csvInputFileを一行読み込みする // csvInputFileを一行読み込みする
csvInputFile.forEach((line) => { csvInputFile.forEach((line) => {
// typeが"USER"以外の場合、アカウントデータの作成を行う // typeが"USER"以外の場合、アカウントデータの作成を行う
if (line.type !== MIGRATION_TYPE.USER) { if (line.type !== MIGRATION_TYPE.USER) {
// userIdのインクリメント
userIdIndex = userIdIndex + 1;
// line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する // line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する
const country = COUNTRY_LIST.find( const country = COUNTRY_LIST.find(
(country) => country.label === line.country (country) => country.label === line.country
)?.value; )?.value;
// adminNameの変換(last_name + " "+ first_name) // adminNameの変換(last_name + " "+ first_name)
const adminName = `${line.last_name} ${line.first_name}`; // もしline.last_nameとline.first_nameが存在しない場合、line.admin_mailをnameにする
let adminName = line.email;
if (line.last_name && line.first_name) {
adminName = `${line.last_name} ${line.first_name}`;
// スペースが前後に入っている場合があるのでTrimする
adminName = adminName.trim();
}
// ランダムパスワードの生成(データ登録ツール側で行うのでやらない) // ランダムパスワードの生成(データ登録ツール側で行うのでやらない)
// common/password/password.tsのmakePasswordを使用 // common/password/password.tsのmakePasswordを使用
// const autoGeneratedPassword = makePassword(); // const autoGeneratedPassword = makePassword();
@ -71,8 +86,18 @@ export class transferService {
if (line.parent_id) { if (line.parent_id) {
parentAccountId = accountIdMap.get(line.parent_id); parentAccountId = accountIdMap.get(line.parent_id);
} }
// AccountsOutputFile配列にPush // 万が一parent_idが入力されているのに存在しなかった場合は、エラー配列に追加する
accountsOutputFileStep1Lines.push({ if (parentAccountId === undefined) {
errorArray.push(
`parent_id is invalid. parent_id=${line.parent_id}`
);
}
// userIdIndexをインクリメントする
userIdIndex++;
// AccountsFile配列にPush
accountsFileTypeLines.push({
// accountIdはaccountIdMapから取得する // accountIdはaccountIdMapから取得する
accountId: accountIdMap.get(line.account_id), accountId: accountIdMap.get(line.account_id),
type: line.type, type: line.type,
@ -82,148 +107,180 @@ export class transferService {
adminName: adminName, adminName: adminName,
adminMail: line.email, adminMail: line.email,
userId: userIdIndex, userId: userIdIndex,
role: null,
authorId: null,
}); });
} else { } else {
// typeが"USER"の場合、ユーザデータの作成を行う // typeが"USER"の場合、かつcountryのアカウントIDに所属していない場合
// userIdのインクリメント if (
userIdIndex = userIdIndex + 1; line.type == MIGRATION_TYPE.USER &&
// nameの変換 !countryAccounts.some(
// もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする (countryAccount) => countryAccount.account_id === line.account_id
// 存在する場合は、last_name + " " + first_name )
let name = line.email; ) {
if (line.last_name && line.first_name) { // line.author_idが存在する場合のみユーザーデータを作成する
name = `${line.last_name} ${line.first_name}`; if (line.author_id) {
} // userIdIndexをインクリメントする
// roleの変換 userIdIndex++;
// authorIdが設定されてる場合はauthor、されていない場合は移行しないので次の行に進む
if (line.author_id) { // nameの変換
usersOutputFileLines.push({ // もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする
accountId: accountIdMap.get(line.account_id), // 存在する場合は、last_name + " " + first_name
userId: userIdIndex, let name = line.user_email;
name: name, if (line.last_name && line.first_name) {
role: USER_ROLES.AUTHOR, name = `${line.last_name} ${line.first_name}`;
authorId: line.author_id, }
email: line.user_email, // UsersFileの作成
}); usersFileLines.push({
} else { accountId: accountIdMap.get(line.account_id),
return; userId: userIdIndex,
} name: name,
// ライセンスのデータの作成を行う role: USER_ROLES.AUTHOR,
// authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID authorId: line.author_id,
// されていない場合、statusは"reusable"、allocated_user_idはnull email: line.user_email,
licensesOutputFileLines.push({ });
expiry_date: line.expired_date.toISOString(), // authorIdとuserIdの対応関係をマッピング
account_id: accountIdMap.get(line.account_id), authorIdToUserIdMap.set(line.author_id, userIdIndex);
type: SWITCH_FROM_TYPE.NONE, }
status: line.author_id
? LICENSE_ALLOCATED_STATUS.ALLOCATED // ライセンスのデータの作成を行う
: LICENSE_ALLOCATED_STATUS.REUSABLE, // line.expired_dateが"9999/12/31"で始まるデータの場合はデモライセンスなので登録しない
allocated_user_id: line.author_id ? userIdIndex : null, if (!line.expired_date.startsWith("9999/12/31")) {
}); // authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID
// WorktypesOutputFileの作成 // されていない場合、statusは"reusable"、allocated_user_idはnull
// wt1~wt20まで読み込み、account単位で作成する let status: string;
// 作成したWorktypesOutputFileを配列にPush let allocated_user_id: number | null;
for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) { if (line.author_id) {
const wt = `wt${i}`; status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
if (line[wt]) { allocated_user_id =
// 既に存在する場合は、作成しない authorIdToUserIdMap.get(line.author_id) ?? null; // authorIdに対応するuserIdを取得
if ( } else {
worktypesOutputFileLines.find( status = LICENSE_ALLOCATED_STATUS.REUSABLE;
(worktype) => allocated_user_id = null;
worktype.account_id === accountIdMap.get(line.account_id) && }
worktype.custom_worktype_id === line[wt].toString() // LicensesFileの作成
) licensesFileLines.push({
) { expiry_date: line.expired_date,
continue; account_id: accountIdMap.get(line.account_id),
type: SWITCH_FROM_TYPE.NONE,
status: status,
allocated_user_id: allocated_user_id,
});
}
// WorktypesFileの作成
// wt1~wt20まで読み込み、account単位で作成する
// 作成したWorktypesFileを配列にPush
for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) {
const wt = `wt${i}`;
if (line[wt]) {
// account_idで同一のcustom_worktype_idが存在しない場合は、作成する
if (
!worktypesFileLines.find(
(worktype) =>
worktype.account_id ===
accountIdMap.get(line.account_id) &&
worktype.custom_worktype_id === line[wt]
)
) {
worktypesFileLines.push({
account_id: accountIdMap.get(line.account_id),
custom_worktype_id: line[wt],
});
} else {
continue;
}
} }
} }
} }
} }
// つぎの行に進む
}); });
// エラー配列に値が存在する場合はエラーファイルを出力する
if (errorArray.length > 0) {
const errorFileJson = JSON.stringify(errorArray);
fs.writeFileSync(`error.json`, errorFileJson);
throw new HttpException(
`errorArray is invalid. errorArray=${errorArray}`,
HttpStatus.BAD_REQUEST
);
}
return { return {
accountsOutputFileStep1Lines, accountsFileTypeLines,
usersOutputFileLines, usersFileLines,
licensesOutputFileLines, licensesFileLines,
worktypesOutputFileLines, worktypesFileLines,
}; };
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally { } finally {
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.registInputData.name}` `[OUT] [${context.getTrackingId()}] ${this.transferInputData.name}`
); );
} }
} }
/** /**
* *
* @param accountsOutputFileStep1: AccountsOutputFileStep1[] * @param accountsFileType: AccountsFileType[]
* @returns AccountsOutputFile[] * @returns AccountsFile[]
*/ */
async relocateHierarchy( async relocateHierarchy(
context: Context, context: Context,
accountsOutputFileStep1: AccountsOutputFileStep1[] accountsFileType: AccountsFileType[]
): Promise<AccountsOutputFile[]> { ): Promise<AccountsFile[]> {
// パラメータ内容が長大なのでログには出さない // パラメータ内容が長大なのでログには出さない
this.logger.log( this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` `[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
); );
try { try {
// dealerAccountIdを検索し、typeがCountryの場合 const relocatedAccounts: AccountsFile[] = [];
accountsOutputFileStep1.forEach((account) => { const dealerRecords: Map<number, number> = new Map();
if (account.type === MIGRATION_TYPE.COUNTRY) {
// そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する const countryAccounts = accountsFileType.filter(
const distributor = accountsOutputFileStep1.find( (item) => item.type === MIGRATION_TYPE.COUNTRY
(distributor) =>
account.type === MIGRATION_TYPE.DISTRIBUTOR &&
distributor.dealerAccountId === account.accountId
);
// DistributorのdealerAccountIdをBCCountryの親に付け替える
distributor.dealerAccountId = account.dealerAccountId;
}
});
// typeがCountryのアカウントを取り除く
accountsOutputFileStep1 = accountsOutputFileStep1.filter(
(account) => account.type !== MIGRATION_TYPE.COUNTRY
); );
// typeをtierに変換し、AccountsOutputFileに変換する const notCountryAccounts = accountsFileType.filter(
let accountsOutputFile: AccountsOutputFile[] = []; (item) => item.type !== MIGRATION_TYPE.COUNTRY
accountsOutputFileStep1.forEach((account) => { );
let tier = 0;
switch (account.type) { notCountryAccounts.forEach((notCountryAccount) => {
case MIGRATION_TYPE.ADMINISTRATOR: let assignDealerAccountId = notCountryAccount.dealerAccountId;
tier = TIERS.TIER1; // 親アカウントIDがcountryの場合、countryの親アカウントIDを設定する
break; for (const countryAccount of countryAccounts) {
case MIGRATION_TYPE.BC: if (countryAccount.accountId === notCountryAccount.dealerAccountId) {
tier = TIERS.TIER2; assignDealerAccountId = countryAccount.dealerAccountId;
break; }
case MIGRATION_TYPE.DISTRIBUTOR:
tier = TIERS.TIER3;
break;
case MIGRATION_TYPE.DEALER:
tier = TIERS.TIER4;
break;
case MIGRATION_TYPE.CUSTOMER:
tier = TIERS.TIER5;
break;
} }
accountsOutputFile.push({
accountId: account.accountId, const assignType = this.getAccountType(notCountryAccount.type);
type: tier,
companyName: account.companyName, const newAccount: AccountsFile = {
country: account.country, accountId: notCountryAccount.accountId,
dealerAccountId: account.dealerAccountId, type: assignType,
adminName: account.adminName, companyName: notCountryAccount.companyName,
adminMail: account.adminMail, country: notCountryAccount.country,
userId: account.userId, dealerAccountId: assignDealerAccountId,
}); adminName: notCountryAccount.adminName,
adminMail: notCountryAccount.adminMail,
userId: notCountryAccount.userId,
role: notCountryAccount.role,
authorId: notCountryAccount.authorId,
};
relocatedAccounts.push(newAccount);
}); });
return accountsOutputFile;
return relocatedAccounts;
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally { } finally {
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` `[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
@ -231,21 +288,38 @@ export class transferService {
} }
} }
// メソッド: アカウントタイプを数値に変換するヘルパー関数
private getAccountType(type: string): number {
switch (type) {
case MIGRATION_TYPE.ADMINISTRATOR:
return TIERS.TIER1;
case MIGRATION_TYPE.BC:
return TIERS.TIER2;
case MIGRATION_TYPE.DISTRIBUTOR:
return TIERS.TIER3;
case MIGRATION_TYPE.DEALER:
return TIERS.TIER4;
case MIGRATION_TYPE.CUSTOMER:
return TIERS.TIER5;
default:
return 0;
}
}
/** /**
* JSONファイルの出力 * JSONファイルの出力
* @param outputFilePath: string * @param outputFilePath: string
* @param accountsOutputFile: AccountsOutputFile[] * @param accountsFile: AccountsFile[]
* @param usersOutputFile: UsersOutputFile[] * @param usersFile: UsersFile[]
* @param licensesOutputFile: LicensesOutputFile[] * @param licensesFile: LicensesFile[]
* @param worktypesOutputFile: WorktypesOutputFile[] * @param worktypesFile: WorktypesFile[]
*/ */
async outputJsonFile( async outputJsonFile(
context: Context, context: Context,
outputFilePath: string, outputFilePath: string,
accountsOutputFile: AccountsOutputFile[], accountsFile: AccountsFile[],
usersOutputFile: UsersOutputFile[], usersFile: UsersFile[],
licensesOutputFile: LicensesOutputFile[], licensesFile: LicensesFile[],
worktypesOutputFile: WorktypesOutputFile[] worktypesFile: WorktypesFile[]
): Promise<void> { ): Promise<void> {
// パラメータ内容が長大なのでログには出さない // パラメータ内容が長大なのでログには出さない
this.logger.log( this.logger.log(
@ -254,29 +328,24 @@ export class transferService {
try { try {
// JSONファイルの出力を行う // JSONファイルの出力を行う
// accountsOutputFile配列の出力 // AccountsFile配列の出力
const accountsOutputFileJson = JSON.stringify(accountsOutputFile); const accountsFileJson = JSON.stringify(accountsFile);
fs.writeFileSync( fs.writeFileSync(`${outputFilePath}accounts.json`, accountsFileJson);
`${outputFilePath}_accounts.json`, // UsersFile
accountsOutputFileJson const usersFileJson = JSON.stringify(usersFile);
); fs.writeFileSync(`${outputFilePath}users.json`, usersFileJson);
// usersOutputFile // LicensesFile
const usersOutputFileJson = JSON.stringify(usersOutputFile); const licensesFileJson = JSON.stringify(licensesFile);
fs.writeFileSync(`${outputFilePath}_users.json`, usersOutputFileJson); fs.writeFileSync(`${outputFilePath}licenses.json`, licensesFileJson);
// licensesOutputFile // WorktypesFile
const licensesOutputFileJson = JSON.stringify(licensesOutputFile); const worktypesFileJson = JSON.stringify(worktypesFile);
fs.writeFileSync( fs.writeFileSync(`${outputFilePath}worktypes.json`, worktypesFileJson);
`${outputFilePath}_licenses.json`,
licensesOutputFileJson
);
// worktypesOutputFile
const worktypesOutputFileJson = JSON.stringify(worktypesOutputFile);
fs.writeFileSync(
`${outputFilePath}_worktypes.json`,
worktypesOutputFileJson
);
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally { } finally {
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.outputJsonFile.name}` `[OUT] [${context.getTrackingId()}] ${this.outputJsonFile.name}`
@ -298,12 +367,17 @@ export class transferService {
); );
try { try {
// エラー配列を定義
let errorArray: string[] = [];
// アカウントに対するworktypeのMap配列を作成する
const accountWorktypeMap = new Map<string, string[]>();
// csvInputFileのバリデーションチェックを行う // csvInputFileのバリデーションチェックを行う
csvInputFile.forEach((line, index) => { csvInputFile.forEach((line, index) => {
// typeのバリデーションチェック // typeのバリデーションチェック
if ( if (
line.type !== MIGRATION_TYPE.ADMINISTRATOR && line.type !== MIGRATION_TYPE.ADMINISTRATOR &&
line.type !== MIGRATION_TYPE.BC && line.type !== MIGRATION_TYPE.BC &&
line.type !== MIGRATION_TYPE.COUNTRY &&
line.type !== MIGRATION_TYPE.DISTRIBUTOR && line.type !== MIGRATION_TYPE.DISTRIBUTOR &&
line.type !== MIGRATION_TYPE.DEALER && line.type !== MIGRATION_TYPE.DEALER &&
line.type !== MIGRATION_TYPE.CUSTOMER && line.type !== MIGRATION_TYPE.CUSTOMER &&
@ -314,44 +388,310 @@ export class transferService {
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
); );
} }
// typeがUSER以外の場合で、countryがnullの場合エラー配列に格納する
if (line.type !== MIGRATION_TYPE.USER) {
if (!line.country) {
// countryがnullの場合エラー配列に格納する
errorArray.push(`country is null. index=${index}`);
}
}
// countryのバリデーションチェック // countryのバリデーションチェック
if (!COUNTRY_LIST.find((country) => country.label === line.country)) { if (line.country) {
throw new HttpException( if (!COUNTRY_LIST.find((country) => country.label === line.country)) {
`country is invalid. index=${index} country=${line.country}`, throw new HttpException(
HttpStatus.BAD_REQUEST `country is invalid. index=${index} country=${line.country}`,
); HttpStatus.BAD_REQUEST
);
}
} }
// mailのバリデーションチェック // mailのバリデーションチェック
// メールアドレスの形式が正しいかどうかのチェック // メールアドレスの形式が正しいかどうかのチェック
const mailRegExp = new RegExp( const mailRegExp =
/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$/ /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
); if (line.email) {
if (!mailRegExp.test(line.email)) { if (!mailRegExp.test(line.email)) {
throw new HttpException( throw new HttpException(
`email is invalid. index=${index} email=${line.email}`, `email is invalid. index=${index} email=${line.email}`,
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
); );
}
} }
// recording_modeのバリデーションチェック if (line.user_email) {
// RECORDING_MODEに存在するかどうかのチェック if (!mailRegExp.test(line.user_email)) {
if ( throw new HttpException(
line.recording_mode !== RECORDING_MODE.DS2_QP && `user_email is invalid. index=${index} user_email=${line.email}`,
line.recording_mode !== RECORDING_MODE.DS2_SP && HttpStatus.BAD_REQUEST
line.recording_mode !== RECORDING_MODE.DSS && );
line.recording_mode !== null }
) { }
throw new HttpException( // recording_modeの値が存在する場合
`recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`, if (line.recording_mode) {
HttpStatus.BAD_REQUEST // recording_modeのバリデーションチェック
); if (
line.recording_mode !== RECORDING_MODE.DS2_QP &&
line.recording_mode !== RECORDING_MODE.DS2_SP &&
line.recording_mode !== RECORDING_MODE.DSS
) {
throw new HttpException(
`recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`,
HttpStatus.BAD_REQUEST
);
}
}
// worktypeの1アカウント20件上限チェック
for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) {
const wt = `wt${i}`;
if (line[wt]) {
if (accountWorktypeMap.has(line.account_id)) {
const worktypes = accountWorktypeMap.get(line.account_id);
// 重複している場合はPushしない
if (worktypes?.includes(line[wt])) {
continue;
} else {
worktypes?.push(line[wt]);
}
// 20件を超えたらエラー
if (worktypes?.length > WORKTYPE_MAX_COUNT) {
throw new HttpException(
`worktype is over. index=${index} account_id=${line.account_id}`,
HttpStatus.BAD_REQUEST
);
}
} else {
accountWorktypeMap.set(line.account_id, [line[wt]]);
}
}
} }
}); });
// エラー配列に値が存在する場合はエラーファイルを出力する
if (errorArray.length > 0) {
const errorFileJson = JSON.stringify(errorArray);
fs.writeFileSync(`error.json`, errorFileJson);
throw new HttpException(
`errorArray is invalid. errorArray=${errorArray}`,
HttpStatus.BAD_REQUEST
);
}
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally { } finally {
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.validateInputData.name}` `[OUT] [${context.getTrackingId()}] ${this.validateInputData.name}`
); );
} }
} }
/**
* removeDuplicateEmail
* @param accountsFileLines: AccountsFile[]
* @param usersFileLines: UsersFile[]
* @param licensesFileLines: LicensesFile[]
* @returns registInputDataResponse
*/
async removeDuplicateEmail(
context: Context,
accountsFileLines: AccountsFile[],
usersFileLines: UsersFile[],
licensesFileLines: LicensesFile[]
): Promise<removeDuplicateEmailResponse> {
// パラメータ内容が長大なのでログには出さない
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.removeDuplicateEmail.name}`
);
try {
const newAccountsFileLines: AccountsFile[] = [];
const newUsersFileLines: UsersFile[] = [];
const newLicensesFileLines: LicensesFile[] = [...licensesFileLines]; // licensesFileLinesを新規配列にコピー
// accountsFileLinesの行ループ
accountsFileLines.forEach((account) => {
const duplicateAdminMail = newAccountsFileLines.find(
(a) => a.adminMail.toLowerCase() === account.adminMail.toLowerCase() // メールアドレスは大文字小文字を区別しない
);
if (duplicateAdminMail) {
// 重複がある場合はどちらが取込対象か判断できないのでファイルを出力し、エラーにする
const errorFileJson = JSON.stringify(account);
fs.writeFileSync(`duplicate_error.json`, errorFileJson);
throw new HttpException(
`adminMail is duplicate. adminMail=${account.adminMail}`,
HttpStatus.BAD_REQUEST
);
} else {
// 重複がない場合
newAccountsFileLines.push(account);
}
});
// usersFileLinesの行ループ
usersFileLines.forEach((user) => {
const duplicateUserEmail = newUsersFileLines.find(
(u) => u.email.toLowerCase() === user.email.toLowerCase() // メールアドレスは大文字小文字を区別しない
);
if (duplicateUserEmail) {
// 重複がある場合
const index = newLicensesFileLines.findIndex(
(license) =>
license.account_id === user.accountId &&
license.allocated_user_id === duplicateUserEmail.userId
);
if (index !== -1) {
// ライセンスの割り当てを解除
newLicensesFileLines[index].status =
LICENSE_ALLOCATED_STATUS.REUSABLE;
newLicensesFileLines[index].allocated_user_id = null;
}
} else {
// 重複がない場合
newUsersFileLines.push(user);
}
// newAccountsFileLinesとの突合せ
const duplicateAdminUserEmail = newAccountsFileLines.find(
(a) => a.adminMail.toLowerCase() === user.email.toLowerCase() // メールアドレスは大文字小文字を区別しない
);
// 重複がある場合
if (duplicateAdminUserEmail) {
// 同一アカウント内での重複の場合
const isDuplicateInSameAccount =
duplicateAdminUserEmail.accountId === user.accountId;
if (isDuplicateInSameAccount) {
// アカウント管理者にauthorロールを付与する
duplicateAdminUserEmail.role = USER_ROLES.AUTHOR;
duplicateAdminUserEmail.authorId = user.authorId;
// アカウントにライセンスが割り当てられているか確認する
const isAllocatedLicense = newLicensesFileLines.some(
(license) =>
license.account_id === duplicateAdminUserEmail.accountId &&
license.allocated_user_id === duplicateAdminUserEmail.userId
);
// 割り当てられていなければアカウントに割り当てる
if (!isAllocatedLicense) {
const index = newLicensesFileLines.findIndex(
(license) =>
license.account_id === user.accountId &&
license.allocated_user_id === user.userId
);
if (index !== -1) {
newLicensesFileLines[index].allocated_user_id =
duplicateAdminUserEmail.userId;
}
}
}
// ユーザーから割り当て解除する
const index = newLicensesFileLines.findIndex(
(license) =>
license.account_id === user.accountId &&
license.allocated_user_id === user.userId
);
if (index !== -1) {
// ライセンスの割り当てを解除
newLicensesFileLines[index].status =
LICENSE_ALLOCATED_STATUS.REUSABLE;
newLicensesFileLines[index].allocated_user_id = null;
}
// ユーザーの削除
const userIndex = newUsersFileLines.findIndex(
(u) => u.userId === user.userId
);
if (userIndex !== -1) {
newUsersFileLines.splice(userIndex, 1);
}
}
});
return {
accountsFileLines: newAccountsFileLines,
usersFileLines: newUsersFileLines,
licensesFileLines: newLicensesFileLines,
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.removeDuplicateEmail.name}`
);
}
}
/**
* transferDuplicateAuthor
* @param accountsFileLines: AccountsFile[]
* @param usersFileLines: UsersFile[]
* @returns UsersFile[]
*/
async transferDuplicateAuthor(
context: Context,
accountsFileLines: AccountsFile[],
usersFileLines: UsersFile[]
): Promise<UsersFile[]> {
// パラメータ内容が長大なのでログには出さない
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.transferDuplicateAuthor.name}`
);
try {
const newUsersFileLines: UsersFile[] = [];
for (const accountsFileLine of accountsFileLines) {
let duplicateSequence: number = 2;
let authorIdList: String[] = [];
// メールアドレス重複時はアカウントにもAuthorIdが設定されるので重複チェック用のリストに追加しておく
if (accountsFileLine.authorId) {
authorIdList.push(accountsFileLine.authorId);
}
const targetaccountUsers = usersFileLines.filter(
(item) => item.accountId === accountsFileLine.accountId
);
for (const targetaccountUser of targetaccountUsers) {
let assignAuthorId = targetaccountUser.authorId;
if (authorIdList.includes(targetaccountUser.authorId)) {
// 同じauthorIdがいる場合、自分のauthorIdに連番を付与する
assignAuthorId = assignAuthorId + duplicateSequence;
duplicateSequence = duplicateSequence + 1;
}
authorIdList.push(targetaccountUser.authorId);
// 新しいAuthorIdのユーザに詰め替え
const newUser: UsersFile = {
accountId: targetaccountUser.accountId,
userId: targetaccountUser.userId,
name: targetaccountUser.name,
role: targetaccountUser.role,
authorId: assignAuthorId,
email: targetaccountUser.email,
};
newUsersFileLines.push(newUser);
}
}
return newUsersFileLines;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.transferDuplicateAuthor.name
}`
);
}
}
} }

View File

@ -1,9 +1,10 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { import {
AccountsOutputFileStep1, AccountsFile,
LicensesOutputFile, AccountsFileType,
UsersOutputFile, LicensesFile,
WorktypesOutputFile, UsersFile,
WorktypesFile,
} from "src/common/types/types"; } from "src/common/types/types";
export class transferRequest { export class transferRequest {
@ -15,11 +16,20 @@ export class transferResponse {}
export class registInputDataResponse { export class registInputDataResponse {
@ApiProperty() @ApiProperty()
accountsOutputFileStep1Lines: AccountsOutputFileStep1[]; accountsFileTypeLines: AccountsFileType[];
@ApiProperty() @ApiProperty()
usersOutputFileLines: UsersOutputFile[]; usersFileLines: UsersFile[];
@ApiProperty() @ApiProperty()
licensesOutputFileLines: LicensesOutputFile[]; licensesFileLines: LicensesFile[];
@ApiProperty() @ApiProperty()
worktypesOutputFileLines: WorktypesOutputFile[]; worktypesFileLines: WorktypesFile[];
}
export class removeDuplicateEmailResponse {
@ApiProperty()
accountsFileLines: AccountsFile[];
@ApiProperty()
usersFileLines: UsersFile[];
@ApiProperty()
licensesFileLines: LicensesFile[];
} }

View File

@ -74,6 +74,9 @@ export class UsersService {
accountId, accountId,
authorId authorId
); );
this.logger.log(
`[${context.getTrackingId()}] isAuthorIdDuplicated=${isAuthorIdDuplicated}`
);
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException( throw new HttpException(
@ -88,9 +91,10 @@ export class UsersService {
); );
} }
} }
this.logger.log("ランダムパスワード生成開始");
// ランダムなパスワードを生成する // ランダムなパスワードを生成する
const ramdomPassword = makePassword(); const ramdomPassword = makePassword();
this.logger.log("ランダムパスワード生成完了");
//Azure AD B2Cにユーザーを新規登録する //Azure AD B2Cにユーザーを新規登録する
let externalUser: { sub: string } | ConflictError; let externalUser: { sub: string } | ConflictError;

View File

@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class VerificationRequest {
@ApiProperty()
inputFilePath: string;
}
export class VerificationResponse {}

View File

@ -0,0 +1,148 @@
import {
Body,
Controller,
HttpStatus,
Post,
Req,
Logger,
HttpException,
} from "@nestjs/common";
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
import fs from "fs";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { VerificationRequest, VerificationResponse } from "./types/types";
import { VerificationService } from "./verification.service";
import { makeContext } from "../../common/log";
import {
csvInputFileWithRow,
isAccountsMappingFileArray,
isCardLicensesFileArray,
isCsvInputFileForValidateArray,
} from "../../common/types/types";
import * as csv from "csv";
@ApiTags("verification")
@Controller("verification")
export class VerificationController {
private readonly logger = new Logger(VerificationController.name);
constructor(private readonly verificationService: VerificationService) {}
@Post()
@ApiResponse({
status: HttpStatus.OK,
type: VerificationResponse,
description: "成功時のレスポンス",
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: "想定外のサーバーエラー",
})
@ApiOperation({ operationId: "dataVerification" })
async dataVerification(
@Body() body: VerificationRequest,
@Req() req: Request
): Promise<VerificationResponse> {
const context = makeContext("iko", "varification");
const inputFilePath = body.inputFilePath;
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.dataVerification.name
} | params: { inputFilePath: ${inputFilePath}};`
);
try {
// 読み込みファイルのフルパス
const accountTransitionFileFullPath =
inputFilePath + "Account_transition.csv";
const accountMapFileFullPath = inputFilePath + "account_map.json";
const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json";
// ファイル存在チェックと読み込み
if (!fs.existsSync(accountTransitionFileFullPath)) {
this.logger.error(
`file not exists from ${accountTransitionFileFullPath}`
);
throw new Error(
`file not exists from ${accountTransitionFileFullPath}`
);
}
if (!fs.existsSync(accountMapFileFullPath)) {
this.logger.error(`file not exists from ${accountMapFileFullPath}`);
throw new Error(`file not exists from ${accountMapFileFullPath}`);
}
if (!fs.existsSync(cardLicensesFileFullPath)) {
this.logger.error(`file not exists from ${cardLicensesFileFullPath}`);
throw new Error(`file not exists from ${cardLicensesFileFullPath}`);
}
// カードライセンスの登録用ファイル読み込み
const cardLicensesObject = JSON.parse(
fs.readFileSync(cardLicensesFileFullPath, "utf8")
);
// 型ガードcardLicenses
if (!isCardLicensesFileArray(cardLicensesObject)) {
throw new Error("input file is not cardLicensesInputFiles");
}
// アカウントIDマッピング用ファイル読み込み
const accountsMapObject = JSON.parse(
fs.readFileSync(accountMapFileFullPath, "utf8")
);
// 型ガードaccountsMapingFile
if (!isAccountsMappingFileArray(accountsMapObject)) {
throw new Error("input file is not accountsMapingFile");
}
// 移行用csvファイルの読み込みcsv parse
fs.createReadStream(accountTransitionFileFullPath).pipe(
csv.parse({ columns: true, delimiter: "," }, (err, csvInputFiles) => {
// 型ガードcsvInputFile
if (!isCsvInputFileForValidateArray(csvInputFiles)) {
throw new Error("input file is not csvInputFile");
}
const csvInputFileswithRows: csvInputFileWithRow[] = [];
let rowCount = 2; // csvの何行目かを表す変数。ヘッダ行があるので2から開始
for (const csvInputFile of csvInputFiles) {
const csvInputFileswithRow: csvInputFileWithRow = {
...csvInputFile,
row: rowCount
};
csvInputFileswithRows.push(csvInputFileswithRow);
rowCount = rowCount + 1;
}
this.verificationService.varificationData(
context,
inputFilePath,
csvInputFileswithRows,
accountsMapObject,
cardLicensesObject
);
})
);
return {};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.dataVerification.name}`
);
}
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -0,0 +1,17 @@
import { Module } from "@nestjs/common";
import { VerificationController } from "./verification.controller";
import { VerificationService } from "./verification.service";
import { LicensesRepositoryModule } from "../../repositories/licenses/licenses.repository.module";
import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module";
import { UsersRepositoryModule } from "../../repositories//users/users.repository.module";
@Module({
imports: [
LicensesRepositoryModule,
AccountsRepositoryModule,
UsersRepositoryModule,
],
controllers: [VerificationController],
providers: [VerificationService],
})
export class VerificationModule {}

View File

@ -0,0 +1,747 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { Context } from "../../common/log";
import {
AccountsMappingFile,
CardLicensesFile,
csvInputFileWithRow,
VerificationResultDetails,
} from "../../common/types/types";
import {
AUTO_INCREMENT_START,
MIGRATION_TYPE,
COUNTRY_LIST,
} from "../../constants/index";
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service";
import {
License,
CardLicense,
} from "../../repositories/licenses/entity/license.entity";
import { AccountsRepositoryService } from "../../repositories/accounts/accounts.repository.service";
import { UsersRepositoryService } from "../../repositories//users/users.repository.service";
import { Account } from "src/repositories/accounts/entity/account.entity";
import fs from "fs";
@Injectable()
export class VerificationService {
constructor(
private readonly AccountsRepository: AccountsRepositoryService,
private readonly UsersRepository: UsersRepositoryService,
private readonly licensesRepository: LicensesRepositoryService
) {}
private readonly logger = new Logger(VerificationService.name);
/**
* Verification Data
* @param inputFilePath: string
*/
async varificationData(
context: Context,
inputFilePath: string,
csvInputFiles: csvInputFileWithRow[],
accountsMappingInputFiles: AccountsMappingFile[],
cardlicensesInputFiles: CardLicensesFile[]
): Promise<void> {
// パラメータ内容が長大なのでログには出さない
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.varificationData.name}`
);
// this.logger.log(csvInputFiles);
try {
// 件数情報の取得
this.logger.log(`入力ファイルから件数情報を取得する`);
const accountFromFile = csvInputFiles.filter(
(item) => item.type !== "USER" && item.type !== "Country"
);
const accountCountFromFile = accountFromFile.length;
const cardLicensesCountFromFile = cardlicensesInputFiles.length;
const licensesCountFromFile =
csvInputFiles.filter(
(item) =>
item.type === "USER" && !item.expired_date.startsWith("9999/12/31")
).length + cardLicensesCountFromFile;
// 管理ユーザ数のカウント
const administratorCountFromFile = accountCountFromFile;
// 一般ユーザ数のカウント
// countryのアカウントに所属するユーザをカウント対象外とする
const countryAccountFromFile = csvInputFiles.filter(
(item) => item.type === "Country"
);
// USER、かつuser_emailが設定なし、かつcountryのアカウントID以外をユーザとする
const normaluserFromFile = csvInputFiles.filter(
(item) =>
item.type === "USER" &&
item.user_email.length !== 0 &&
!countryAccountFromFile.some(
(countryItem) => countryItem.account_id === item.account_id
)
);
const normaluserCountFromFile = normaluserFromFile.length;
// ユーザ重複数のカウント
let mailAdresses: string[] = [];
accountFromFile.forEach((item) => {
// メールアドレスの要素を配列に追加
if (item.email.length !== 0) {
mailAdresses.push(item.email);
}
});
normaluserFromFile.forEach((item) => {
// メールアドレスの要素を配列に追加
if (item.user_email.length !== 0) {
mailAdresses.push(item.user_email);
}
});
// 重複する要素を抽出
const duplicates: { [key: string]: number } = {};
mailAdresses.forEach((str) => {
duplicates[str.toLowerCase()] =
(duplicates[str.toLowerCase()] || 0) + 1;
});
// 重複する要素と件数を表示
let duplicateCount = 0;
Object.keys(duplicates).forEach((key) => {
const count = duplicates[key];
if (count > 1) {
// 重複件数をカウント
duplicateCount = duplicateCount + (count - 1);
//console.log(`${key}が${count}件`);
}
});
const userCountFromFile =
administratorCountFromFile + normaluserCountFromFile - duplicateCount;
this.logger.log(`accountCountFromFile=${accountCountFromFile}`);
this.logger.log(`cardLicensesCountFromFile=${cardLicensesCountFromFile}`);
this.logger.log(`licensesCountFromFile=${licensesCountFromFile}`);
this.logger.log(`userCountFromFile=${userCountFromFile}`);
// DBから情報を取得する
this.logger.log(`DBの情報を取得する`);
const accounts = await this.AccountsRepository.getAllAccounts(context);
const users = await this.UsersRepository.getAllUsers(context);
const licenses = await this.licensesRepository.getAllLicenses(context);
const cardLicenses = await this.licensesRepository.getAllCardLicense(
context
);
// DB件数のカウント
this.logger.log(`DBの情報から件数を取得する`);
const accountsCountFromDB = accounts.length;
const usersCountFromDB = users.length;
const licensesCountFromDB = licenses.length;
const cardLicensesCountFromDB = cardLicenses.length;
this.logger.log(`accountsCountFromDB=${accountsCountFromDB}`);
this.logger.log(`usersCountFromDB=${usersCountFromDB}`);
this.logger.log(`licensesCountFromDB=${licensesCountFromDB}`);
this.logger.log(`cardLicensesCountFromDB=${cardLicensesCountFromDB}`);
// エラー情報の定義
const VerificationResultDetails: VerificationResultDetails[] = [];
// カードライセンス関連の情報突き合わせ
this.logger.log(`カードライセンス関連の情報突き合わせ`);
const isCardDetailNoError = compareCardLicenses(
VerificationResultDetails,
cardlicensesInputFiles,
cardLicenses,
licenses
);
// ライセンス関連の情報突き合わせ
this.logger.log(`ライセンス関連の情報突き合わせ`);
const isLicensesDetailNoError = compareLicenses(
VerificationResultDetails,
csvInputFiles.filter(
(item) =>
item.type === "USER" && !item.expired_date.startsWith("9999/12/31")
),
licenses.filter((item) => item.expiry_date !== null),
accountsMappingInputFiles
);
// アカウント情報の突き合わせ
this.logger.log(`アカウント関連の情報突き合わせ`);
const isAccountsDetailNoError = compareAccounts(
VerificationResultDetails,
csvInputFiles.filter(
(item) => item.type !== "USER" && item.type !== "Country"
),
csvInputFiles.filter((item) => item.type === "Country"),
accounts,
accountsMappingInputFiles
);
// 結果の判定と出力
this.logger.log(`結果の判定と出力`);
const isAccountCountNoDifference =
accountCountFromFile === accountsCountFromDB;
const isUsersCountNoDifference = userCountFromFile === usersCountFromDB;
const isLicensesCountNoDifference =
licensesCountFromFile === licensesCountFromDB;
const isCardLicensesCountNoDifference =
cardLicensesCountFromFile === cardLicensesCountFromDB;
const isNoDetailError = VerificationResultDetails.length === 0;
const isSummaryNoError =
isAccountCountNoDifference &&
isUsersCountNoDifference &&
isLicensesCountNoDifference &&
isCardLicensesCountNoDifference &&
isNoDetailError;
const summaryString = `
${isSummaryNoError ? "OK" : "NG"}
${
isAccountCountNoDifference ? "OK" : "NG"
}csv件数:${accountCountFromFile}/DB件数:${accountsCountFromDB}
${
isLicensesCountNoDifference ? "OK" : "NG"
}csv件数:${licensesCountFromFile}/DB件数:${licensesCountFromDB}
${
isCardLicensesCountNoDifference ? "OK" : "NG"
}csv件数:${cardLicensesCountFromFile}/DB件数:${cardLicensesCountFromDB}
${
isUsersCountNoDifference ? "OK" : "NG"
}csv件数:${userCountFromFile}/DB件数:${usersCountFromDB}
${isAccountsDetailNoError ? "OK" : "NG"}
${isCardDetailNoError ? "OK" : "NG"}
${isLicensesDetailNoError ? "OK" : "NG"}
`;
// サマリファイルの書き込み
fs.writeFileSync(`${inputFilePath}resultsummary.txt`, summaryString);
// 詳細ファイルの書き込み
// 配列をJSON文字列に変換
const jsonContent = JSON.stringify(VerificationResultDetails, null, 2);
// JSONをファイルに書き込み
fs.writeFileSync(`${inputFilePath}resultdetail.json`, jsonContent);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.varificationData.name}`
);
}
}
}
// dateを任意のフォーマットに変換する
const getFormattedDate = (
date: Date | null,
format: string,
padHours: boolean = false // trueの場合、hhについてゼロパディングする00→0、01→1、23→23
) => {
if (!date) {
return null;
}
const symbol = {
M: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
m: date.getMinutes(),
s: date.getSeconds(),
};
// hhの値をゼロパディングするかどうかのフラグを確認
const hourSymbol = padHours ? "hh" : "h";
const formatted = format.replace(/(M+|d+|h+|m+|s+)/g, (v) =>
(
(v.length > 1 && v !== hourSymbol ? "0" : "") +
symbol[v.slice(-1) as keyof typeof symbol]
).slice(-2)
);
return formatted.replace(/(y+)/g, (v) =>
date.getFullYear().toString().slice(-v.length)
);
};
// 親の階層がcountryの場合、countryの親を返却する
function transrateCountryHierarchy(
countriesFromFile: csvInputFileWithRow[],
targetParentAccountIdString: string
): string {
for (const countryFromFile of countriesFromFile) {
if (countryFromFile.account_id === targetParentAccountIdString) {
return countryFromFile.parent_id;
}
}
return targetParentAccountIdString;
}
// アカウントIDnumberを対応するアカウントIDstringに変換する
function findAccountIdText(
accountsMappings: AccountsMappingFile[],
targetAccountIdNumber: number
): string {
if (targetAccountIdNumber == null) {
return "";
}
for (const accountsMapping of accountsMappings) {
if (accountsMapping.accountIdNumber === targetAccountIdNumber) {
return accountsMapping.accountIdText;
}
}
return `NO_MATCHED_ACCOUNTID_${targetAccountIdNumber}`; // マッチするものが見つからない場合
}
// 階層numberを対応する階層stringに変換する
function getMigrationTypeByNumber(numberValue: number): string {
switch (numberValue) {
case 1:
return MIGRATION_TYPE.ADMINISTRATOR;
case 2:
return MIGRATION_TYPE.BC;
case 3:
return MIGRATION_TYPE.DISTRIBUTOR;
case 4:
return MIGRATION_TYPE.DEALER;
case 5:
return MIGRATION_TYPE.CUSTOMER;
default:
return `NO_MATCHED_TIER_${numberValue}`;
}
}
// 国(省略版)を対応する国(非省略版)に変換する
function getCountryLabelByValue(value: string): string {
const country = COUNTRY_LIST.find((country) => country.value === value);
return country ? country.label : `NO_MATCHED_COUNTRY_${value}`;
}
// カードライセンス情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する
function compareCardLicenses(
VerificationResultDetails: VerificationResultDetails[],
cardlicensesInputFiles: CardLicensesFile[],
cardLicenses: CardLicense[],
licenses: License[]
): boolean {
let isNoError = true;
let row = 1; // カードライセンスファイルの行数
for (const cardlicensesInputFile of cardlicensesInputFiles) {
const filterdCardLicenses = cardLicenses.filter(
(cardLicenses) =>
cardLicenses.card_license_key === cardlicensesInputFile.card_license_key
);
if (filterdCardLicenses.length === 0) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "cardLicenses",
columnName: "card_license_key",
fileData: cardlicensesInputFile.card_license_key,
databaseData: "-",
reason: "レコード無し",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
/* issue_id
if (cardlicensesInputFile.issue_id !== filterdCardLicenses[0].issue_id) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
diffTargetTable: "cardLicenses",
columnName: "issue_id",
fileData: cardlicensesInputFile.issue_id.toString(),
databaseData: filterdCardLicenses[0].issue_id.toString(),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
*/
const formattedFileActivated = getFormattedDate(
cardlicensesInputFile.activated_at
? new Date(cardlicensesInputFile.activated_at)
: null,
`yyyy/MM/dd hh:mm:ss`,
true
);
const formattedDbActivated = getFormattedDate(
filterdCardLicenses[0].activated_at,
`yyyy/MM/dd hh:mm:ss`,
true
);
if (formattedFileActivated !== formattedDbActivated) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "cardLicenses",
columnName: "activated_at",
fileData: formattedFileActivated,
databaseData: formattedDbActivated,
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
const filterdLicenses = licenses.filter(
(licenses) => licenses.id === filterdCardLicenses[0].license_id
);
if (filterdLicenses.length === 0) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "id",
fileData: filterdCardLicenses[0].license_id.toString(),
databaseData: "-",
reason: "紐つくライセンスのレコード無し",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
if (filterdLicenses[0].expiry_date !== null) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "expiry_date",
fileData: null,
databaseData: getFormattedDate(
filterdLicenses[0].expiry_date,
`yyyy/MM/dd hh:mm:ss`
),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].account_id !== AUTO_INCREMENT_START) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "account_id",
fileData: AUTO_INCREMENT_START.toString(),
databaseData: filterdLicenses[0].account_id.toString(),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].type !== "CARD") {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "type",
fileData: "CARD",
databaseData: filterdLicenses[0].type,
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].status !== "Unallocated") {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "status",
fileData: "Unallocated",
databaseData: filterdLicenses[0].status,
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].allocated_user_id !== null) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "allocated_user_id",
fileData: null,
databaseData: filterdLicenses[0].allocated_user_id.toString(),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].order_id !== null) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "order_id",
fileData: null,
databaseData: filterdLicenses[0].order_id.toString(),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].deleted_at !== null) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "deleted_at",
fileData: null,
databaseData: getFormattedDate(
filterdLicenses[0].deleted_at,
`yyyy/MM/dd hh:mm:ss`
),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
if (filterdLicenses[0].delete_order_id !== null) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "cardLicenses",
inputRow: row,
diffTargetTable: "licenses",
columnName: "delete_order_id",
fileData: null,
databaseData: filterdLicenses[0].delete_order_id.toString(),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
row = row + 1;
}
return isNoError;
}
// ライセンス情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する
function compareLicenses(
VerificationResultDetails: VerificationResultDetails[],
licensesFromFile: csvInputFileWithRow[],
licensesFromDatabase: License[],
accountsMappingInputFiles: AccountsMappingFile[]
): boolean {
let isNoError = true;
for (let i = 0; i < licensesFromFile.length; i++) {
if (
!licensesFromDatabase[i] ||
licensesFromFile[i].account_id !==
findAccountIdText(
accountsMappingInputFiles,
licensesFromDatabase[i].account_id
)
) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: licensesFromFile[i].row,
diffTargetTable: "licenses",
columnName: "account_id",
fileData: licensesFromFile[i].account_id,
databaseData: licensesFromDatabase[i]
? findAccountIdText(
accountsMappingInputFiles,
licensesFromDatabase[i].account_id
) + `(${licensesFromDatabase[i].account_id})`
: "undifined",
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
// expiry_dateについて、時はゼロパディングした値で比較する×0109 ○19
if (
!licensesFromDatabase[i] ||
getFormattedDate(
licensesFromFile[i].expired_date
? new Date(licensesFromFile[i].expired_date)
: null,
`yyyy/MM/dd hh:mm:ss`,
true
) !==
getFormattedDate(
licensesFromDatabase[i].expiry_date,
`yyyy/MM/dd hh:mm:ss`,
true
)
) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: licensesFromFile[i].row,
diffTargetTable: "licenses",
columnName: "expired_date",
fileData: getFormattedDate(
licensesFromFile[i].expired_date
? new Date(licensesFromFile[i].expired_date)
: null,
`yyyy/MM/dd hh:mm:ss`,
true
),
databaseData: licensesFromDatabase[i]
? getFormattedDate(
licensesFromDatabase[i].expiry_date,
`yyyy/MM/dd hh:mm:ss`,
true
)
: "undifined",
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
}
}
return isNoError;
}
// アカウント情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する
function compareAccounts(
VerificationResultDetails: VerificationResultDetails[],
accountsFromFile: csvInputFileWithRow[],
countriesFromFile: csvInputFileWithRow[],
accountsFromDatabase: Account[],
accountsMappingInputFiles: AccountsMappingFile[]
): boolean {
let isNoError = true;
for (const accountFromFile of accountsFromFile) {
// DBレコードの存在チェック
const filterdAccounts = accountsFromDatabase.filter(
(accountsFromDatabase) =>
findAccountIdText(
accountsMappingInputFiles,
accountsFromDatabase.id
) === accountFromFile.account_id
);
if (filterdAccounts.length === 0) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: accountFromFile.row,
diffTargetTable: "accounts",
columnName: "account_id",
fileData: accountFromFile.account_id,
databaseData: "-",
reason: "レコード無し",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
// 項目チェックparent_account_id
const transratedParentId = transrateCountryHierarchy(
countriesFromFile,
accountFromFile.parent_id
);
if (
transratedParentId !==
findAccountIdText(
accountsMappingInputFiles,
filterdAccounts[0].parent_account_id
)
) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: accountFromFile.row,
diffTargetTable: "accounts",
columnName: "parent_account_id",
fileData:
transratedParentId === accountFromFile.parent_id
? accountFromFile.parent_id
: `${transratedParentId}(${accountFromFile.parent_id})`,
databaseData:
findAccountIdText(
accountsMappingInputFiles,
filterdAccounts[0].parent_account_id
) + `(${filterdAccounts[0].parent_account_id})`,
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
// 項目チェックtier
if (
accountFromFile.type !== getMigrationTypeByNumber(filterdAccounts[0].tier)
) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: accountFromFile.row,
diffTargetTable: "accounts",
columnName: "tier",
fileData: accountFromFile.type,
databaseData: getMigrationTypeByNumber(filterdAccounts[0].tier),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
// 項目チェックcountry
if (
accountFromFile.country !==
getCountryLabelByValue(filterdAccounts[0].country)
) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: accountFromFile.row,
diffTargetTable: "accounts",
columnName: "country",
fileData: accountFromFile.country,
databaseData: getCountryLabelByValue(filterdAccounts[0].country),
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
// 項目チェックcompany_name
if (accountFromFile.company_name !== filterdAccounts[0].company_name) {
const VerificationResultDetailsOne: VerificationResultDetails = {
input: "Account_transition",
inputRow: accountFromFile.row,
diffTargetTable: "accounts",
columnName: "company_name",
fileData: accountFromFile.company_name,
databaseData: filterdAccounts[0].company_name,
reason: "内容不一致",
};
VerificationResultDetails.push(VerificationResultDetailsOne);
isNoError = false;
continue;
}
}
return isNoError;
}

View File

@ -30,14 +30,10 @@ export const isConflictError = (arg: unknown): arg is ConflictError => {
export class AdB2cService { export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name); private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName: string; private readonly tenantName: string;
private readonly flowName: string;
private readonly ttl: number;
private graphClient: Client; private graphClient: Client;
constructor(private readonly configService: ConfigService) { constructor(private readonly configService: ConfigService) {
this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME"); this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME");
this.flowName = this.configService.getOrThrow<string>("SIGNIN_FLOW_NAME");
this.ttl = this.configService.getOrThrow<number>("ADB2C_CACHE_TTL");
// ADB2Cへの認証情報 // ADB2Cへの認証情報
const credential = new ClientSecretCredential( const credential = new ClientSecretCredential(
@ -68,41 +64,57 @@ export class AdB2cService {
this.logger.log( this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.createUser.name}` `[IN] [${context.getTrackingId()}] ${this.createUser.name}`
); );
try {
// ユーザをADB2Cに登録 const retryCount: number = 3;
const newUser = await this.graphClient.api("users/").post({ let retry = 0;
accountEnabled: true,
displayName: username, while (retry < retryCount) {
passwordPolicies: "DisableStrongPassword", try {
passwordProfile: { // ユーザをADB2Cに登録
forceChangePasswordNextSignIn: false, const newUser = await this.graphClient.api("users/").post({
password: password, accountEnabled: true,
}, displayName: username,
identities: [ passwordPolicies: "DisableStrongPassword",
{ passwordProfile: {
signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, forceChangePasswordNextSignIn: false,
issuer: `${this.tenantName}.onmicrosoft.com`, password: password,
issuerAssignedId: email,
}, },
], identities: [
}); {
return { sub: newUser.id }; signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
} catch (e) { issuer: `${this.tenantName}.onmicrosoft.com`,
this.logger.error(`[${context.getTrackingId()}] error=${e}`); issuerAssignedId: email,
if (e?.statusCode === 400 && e?.body) { },
const error = JSON.parse(e.body); ],
});
this.logger.log(
`[${context.getTrackingId()}] [ADB2C CREATE] newUser: ${newUser}`
);
return { sub: newUser.id };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e?.statusCode === 400 && e?.body) {
const error = JSON.parse(e.body);
// エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す // エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す
if (error?.details?.find((x) => x.code === "ObjectConflict")) { if (error?.details?.find((x) => x.code === "ObjectConflict")) {
return { reason: "email", message: "ObjectConflict" }; return { reason: "email", message: "ObjectConflict" };
}
} }
}
throw e; if (++retry < retryCount) {
} finally { this.logger.log(`ADB2Cエラー発生。5秒sleepしてリトライします (${retry}/${retryCount})...`);
this.logger.log( await new Promise(resolve => setTimeout(resolve, 5000));
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}` } else {
); this.logger.log(`リトライ数が上限に達したのでエラーを返却します`);
throw e;
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`
);
}
} }
} }
@ -111,8 +123,10 @@ export class AdB2cService {
* @param externalIds * @param externalIds
* @returns users * @returns users
*/ */
async getUsers(): Promise<AdB2cUser[]> { async getUsers(
this.logger.log(`[IN] ${this.getUsers.name}`); context: Context
): Promise<{ users: AdB2cUser[]; hasNext: boolean }> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
try { try {
const res: AdB2cResponse = await this.graphClient const res: AdB2cResponse = await this.graphClient
@ -121,7 +135,7 @@ export class AdB2cService {
.filter(`creationType eq 'LocalAccount'`) .filter(`creationType eq 'LocalAccount'`)
.get(); .get();
return res.value; return { users: res.value, hasNext: !!res["@odata.nextLink"] };
} catch (e) { } catch (e) {
this.logger.error(`error=${e}`); this.logger.error(`error=${e}`);
const { statusCode } = e; const { statusCode } = e;
@ -177,9 +191,11 @@ export class AdB2cService {
* Azure AD B2Cからユーザ情報を削除する * Azure AD B2Cからユーザ情報を削除する
* @param externalIds ID * @param externalIds ID
*/ */
async deleteUsers(externalIds: string[]): Promise<void> { async deleteUsers(context: Context, externalIds: string[]): Promise<void> {
this.logger.log( this.logger.log(
`[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };` `[IN] [${context.getTrackingId()}] ${
this.deleteUsers.name
} | params: { externalIds: ${externalIds} };`
); );
try { try {

View File

@ -89,8 +89,10 @@ export class BlobstorageService {
* *
* @returns containers * @returns containers
*/ */
async deleteContainers(): Promise<void> { async deleteContainers(context: Context): Promise<void> {
this.logger.log(`[IN] ${this.deleteContainers.name}`); this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.deleteContainers.name}`
);
try { try {
for await (const container of this.blobServiceClientAU.listContainers({ for await (const container of this.blobServiceClientAU.listContainers({

View File

@ -1,20 +1,18 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { import { DataSource } from "typeorm";
DataSource, import { User } from "../users/entity/user.entity";
} from 'typeorm'; import { Account } from "./entity/account.entity";
import { User } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import { import {
getDirection, getDirection,
getTaskListSortableAttribute, getTaskListSortableAttribute,
} from '../../common/types/sort/util'; } from "../../common/types/sort/util";
import { SortCriteria } from "../sort_criteria/entity/sort_criteria.entity"; import { SortCriteria } from "../sort_criteria/entity/sort_criteria.entity";
import { import {
insertEntity, insertEntity,
updateEntity, updateEntity,
deleteEntity, deleteEntity,
} from '../../common/repository'; } from "../../common/repository";
import { Context } from '../../common/log'; import { Context } from "../../common/log";
@Injectable() @Injectable()
export class AccountsRepositoryService { export class AccountsRepositoryService {
@ -30,6 +28,7 @@ export class AccountsRepositoryService {
* @param tier * @param tier
* @param adminExternalUserId * @param adminExternalUserId
* @param adminUserRole * @param adminUserRole
* @param adminUserAuthId
* @param accountId * @param accountId
* @param userId * @param userId
* @param adminUserAcceptedEulaVersion * @param adminUserAcceptedEulaVersion
@ -45,6 +44,7 @@ export class AccountsRepositoryService {
tier: number, tier: number,
adminExternalUserId: string, adminExternalUserId: string,
adminUserRole: string, adminUserRole: string,
adminUserAuthId: string,
accountId: number, accountId: number,
userId: number, userId: number,
adminUserAcceptedEulaVersion?: string, adminUserAcceptedEulaVersion?: string,
@ -77,10 +77,12 @@ export class AccountsRepositoryService {
user.account_id = persistedAccount.id; user.account_id = persistedAccount.id;
user.external_id = adminExternalUserId; user.external_id = adminExternalUserId;
user.role = adminUserRole; user.role = adminUserRole;
user.author_id = adminUserAuthId;
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null; user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
user.accepted_privacy_notice_version = user.accepted_privacy_notice_version =
adminUserAcceptedPrivacyNoticeVersion ?? null; adminUserAcceptedPrivacyNoticeVersion ?? null;
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null; user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
user.email_verified = true;
} }
const usersRepo = entityManager.getRepository(User); const usersRepo = entityManager.getRepository(User);
const newUser = usersRepo.create(user); const newUser = usersRepo.create(user);
@ -161,4 +163,21 @@ export class AccountsRepositoryService {
); );
}); });
} }
/**
*
* @returns Account[]
*/
async getAllAccounts(
context: Context,
): Promise<Account[]> {
return await this.dataSource.transaction(async (entityManager) => {
const accountsRepo = entityManager.getRepository(Account);
const accouts = accountsRepo.find({
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
return accouts;
});
}
} }

View File

@ -1,11 +1,15 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { logger } from "@azure/identity"; import { logger } from "@azure/identity";
import { Account } from "./entity/account.entity";
import { AUTO_INCREMENT_START } from "../../constants"; import { AUTO_INCREMENT_START } from "../../constants";
import { Term } from "./entity/term.entity";
import { insertEntities } from "../../common/repository";
import { Context } from "../../common/log";
@Injectable() @Injectable()
export class DeleteRepositoryService { export class DeleteRepositoryService {
// クエリログにコメントを出力するかどうか
private readonly isCommentOut = process.env.STAGE !== "local";
constructor(private dataSource: DataSource) {} constructor(private dataSource: DataSource) {}
/** /**
@ -54,4 +58,35 @@ export class DeleteRepositoryService {
await queryRunner.release(); await queryRunner.release();
} }
} }
/**
*
* @returns data
*/
async insertInitData(context: Context): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const termRepo = entityManager.getRepository(Term);
// ワークフローのデータ作成
const newTarmDpa = new Term();
newTarmDpa.document_type = "DPA";
newTarmDpa.version = "V0.1";
const newTarmEula = new Term();
newTarmEula.document_type = "EULA";
newTarmEula.version = "V0.1";
const newTarmPrivacyNotice = new Term();
newTarmPrivacyNotice.document_type = "PrivacyNotice";
newTarmPrivacyNotice.version = "V0.1";
const initTerms = [newTarmDpa, newTarmEula, newTarmPrivacyNotice];
await insertEntities(
Term,
termRepo,
initTerms,
this.isCommentOut,
context
);
});
}
} }

View File

@ -129,6 +129,33 @@ export class CardLicense {
@Column({ nullable: true, type: "datetime" }) @Column({ nullable: true, type: "datetime" })
updated_by: string | null; updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
}
@Entity({ name: "card_license_issue" })
export class CardLicenseIssue {
@PrimaryGeneratedColumn()
id: number;
@Column()
issued_at: Date;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({ @UpdateDateColumn({
default: () => "datetime('now', 'localtime')", default: () => "datetime('now', 'localtime')",
type: "datetime", type: "datetime",

View File

@ -2,6 +2,7 @@ import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from "@nestjs/typeorm";
import { import {
CardLicense, CardLicense,
CardLicenseIssue,
License, License,
LicenseAllocationHistory, LicenseAllocationHistory,
} from "./entity/license.entity"; } from "./entity/license.entity";
@ -9,7 +10,11 @@ import { LicensesRepositoryService } from "./licenses.repository.service";
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([License, CardLicense, LicenseAllocationHistory]), TypeOrmModule.forFeature([
License,
CardLicense,
CardLicenseIssue, LicenseAllocationHistory,
]),
], ],
providers: [LicensesRepositoryService], providers: [LicensesRepositoryService],
exports: [LicensesRepositoryService], exports: [LicensesRepositoryService],

View File

@ -1,17 +1,17 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { DataSource, In } from "typeorm"; import { DataSource } from "typeorm";
import { import {
License, License,
LicenseAllocationHistory, LicenseAllocationHistory,
CardLicense, CardLicense,
CardLicenseIssue,
} from "./entity/license.entity"; } from "./entity/license.entity";
import { insertEntities } from "../../common/repository"; import { insertEntity, insertEntities } from "../../common/repository";
import { Context } from "../../common/log"; import { Context } from "../../common/log";
import {
LicensesInputFile,
CardLicensesInputFile,
} from "../../common/types/types";
import { AUTO_INCREMENT_START } from "../../constants/index";
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from "../../constants";
import { CardLicensesFile, LicensesFile } from "src/common/types/types";
@Injectable() @Injectable()
export class LicensesRepositoryService { export class LicensesRepositoryService {
//クエリログにコメントを出力するかどうか //クエリログにコメントを出力するかどうか
@ -22,25 +22,27 @@ export class LicensesRepositoryService {
/** /**
* *
* @context Context * @context Context
* @param licensesInputFiles * @param LicensesFiles
*/ */
async insertLicenses( async insertLicenses(
context: Context, context: Context,
licensesInputFiles: LicensesInputFile[] LicensesFiles: LicensesFile[]
): Promise<{}> { ): Promise<{}> {
const nowDate = new Date(); const nowDate = new Date();
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const licenseRepo = entityManager.getRepository(License); const licenseRepo = entityManager.getRepository(License);
let newLicenses: License[] = []; let newLicenses: License[] = [];
licensesInputFiles.forEach((licensesInputFile) => { LicensesFiles.forEach((LicensesFile) => {
const license = new License(); const license = new License();
license.account_id = licensesInputFile.account_id; license.account_id = LicensesFile.account_id;
license.status = licensesInputFile.status; license.status = LicensesFile.status;
license.type = licensesInputFile.type; license.type = LicensesFile.type;
license.expiry_date = (licensesInputFile.expiry_date) ? new Date(licensesInputFile.expiry_date) : null; license.expiry_date = LicensesFile.expiry_date
if (licensesInputFile.allocated_user_id) { ? new Date(LicensesFile.expiry_date)
license.allocated_user_id = licensesInputFile.allocated_user_id; : null;
if (LicensesFile.allocated_user_id) {
license.allocated_user_id = LicensesFile.allocated_user_id;
} }
newLicenses.push(license); newLicenses.push(license);
}); });
@ -89,30 +91,68 @@ export class LicensesRepositoryService {
/** /**
* *
* @context Context * @context Context
* @param cardLicensesInputFiles * @param cardLicensesFiles
*/ */
async insertCardLicenses( async insertCardLicenses(
context: Context, context: Context,
cardLicensesInputFiles: CardLicensesInputFile[] cardLicensesFiles: CardLicensesFile[]
): Promise<{}> { ): Promise<{}> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const cardLicenseRepo = entityManager.getRepository(CardLicense); const cardLicenseRepo = entityManager.getRepository(CardLicense);
const licensesRepo = entityManager.getRepository(License);
const cardLicenseIssueRepo =
entityManager.getRepository(CardLicenseIssue);
const licenses: License[] = [];
// ライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < cardLicensesFiles.length; i++) {
const license = new License();
license.account_id = AUTO_INCREMENT_START; // 最初に登場するアカウント(第一アカウント)
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
license.type = LICENSE_TYPE.CARD;
licenses.push(license);
}
const savedLicenses = await insertEntities(
License,
licensesRepo,
licenses,
this.isCommentOut,
context
);
let newCardLicenses: CardLicense[] = []; // カードライセンス発行テーブルを作成する
cardLicensesInputFiles.forEach((cardLicensesInputFile) => { const cardLicenseIssue = new CardLicenseIssue();
cardLicenseIssue.issued_at = new Date();
const newCardLicenseIssue = cardLicenseIssueRepo.create(cardLicenseIssue);
const savedCardLicensesIssue = await insertEntity(
CardLicenseIssue,
cardLicenseIssueRepo,
newCardLicenseIssue,
this.isCommentOut,
context
);
const newCardLicenses: CardLicense[] = [];
// カードライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < cardLicensesFiles.length; i++) {
const cardLicense = new CardLicense(); const cardLicense = new CardLicense();
cardLicense.license_id = cardLicensesInputFile.license_id; cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入
cardLicense.issue_id = cardLicensesInputFile.issue_id; cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
cardLicense.card_license_key = cardLicensesInputFile.card_license_key; cardLicense.card_license_key = cardLicensesFiles[i].card_license_key;
cardLicense.activated_at = (cardLicensesInputFile.activated_at) ? new Date(cardLicensesInputFile.activated_at) : null; cardLicense.activated_at = cardLicensesFiles[i].activated_at
cardLicense.created_at = (cardLicensesInputFile.created_at) ? new Date(cardLicensesInputFile.created_at) : null; ? new Date(cardLicensesFiles[i].activated_at)
cardLicense.created_by = cardLicensesInputFile.created_by; : null;
cardLicense.updated_at = (cardLicensesInputFile.updated_at) ? new Date(cardLicensesInputFile.updated_at) : null; cardLicense.created_at = cardLicensesFiles[i].created_at
cardLicense.updated_by = cardLicensesInputFile.updated_by; ? new Date(cardLicensesFiles[i].created_at)
: null;
cardLicense.created_by = cardLicensesFiles[i].created_by;
cardLicense.updated_at = cardLicensesFiles[i].updated_at
? new Date(cardLicensesFiles[i].updated_at)
: null;
cardLicense.updated_by = cardLicensesFiles[i].updated_by;
newCardLicenses.push(cardLicense); newCardLicenses.push(cardLicense);
}); }
const query = cardLicenseRepo const query = cardLicenseRepo
.createQueryBuilder() .createQueryBuilder()
@ -127,4 +167,33 @@ export class LicensesRepositoryService {
}); });
} }
/**
*
* @returns License[]
*/
async getAllLicenses(context: Context): Promise<License[]> {
return await this.dataSource.transaction(async (entityManager) => {
const licenseRepo = entityManager.getRepository(License);
const licenses = licenseRepo.find({
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
return licenses;
});
}
/**
*
* @returns CardLicense[]
*/
async getAllCardLicense(context: Context): Promise<CardLicense[]> {
return await this.dataSource.transaction(async (entityManager) => {
const cardLicenseRepo = entityManager.getRepository(CardLicense);
const cardLicenses = cardLicenseRepo.find({
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
return cardLicenses;
});
}
} }

View File

@ -138,4 +138,20 @@ export class UsersRepositoryService {
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context); await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
}); });
} }
/**
*
* @returns User[]
*/
async getAllUsers(context: Context): Promise<User[]> {
return await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
const users = userRepo.find({
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
return users;
});
}
} }

View File

@ -13,7 +13,7 @@ import {
import { OptionItem } from "./entity/option_item.entity"; import { OptionItem } from "./entity/option_item.entity";
import { insertEntities, insertEntity } from "../../common/repository"; import { insertEntities, insertEntity } from "../../common/repository";
import { Context } from "../../common/log"; import { Context } from "../../common/log";
import { WorktypesInputFile } from "../../common/types/types"; import { WorktypesFile } from "../../common/types/types";
@Injectable() @Injectable()
export class WorktypesRepositoryService { export class WorktypesRepositoryService {
@ -30,15 +30,15 @@ export class WorktypesRepositoryService {
*/ */
async createWorktype( async createWorktype(
context: Context, context: Context,
worktypesInputFiles: WorktypesInputFile[] WorktypesFiles: WorktypesFile[]
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { await this.dataSource.transaction(async (entityManager) => {
const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRepo = entityManager.getRepository(Worktype);
const optionItemRepo = entityManager.getRepository(OptionItem); const optionItemRepo = entityManager.getRepository(OptionItem);
for (const worktypesInputFile of worktypesInputFiles) { for (const WorktypesFile of WorktypesFiles) {
const accountId = worktypesInputFile.account_id; const accountId = WorktypesFile.account_id;
const worktypeId = worktypesInputFile.custom_worktype_id; const worktypeId = WorktypesFile.custom_worktype_id;
const description = null; const description = null;
const duplicatedWorktype = await worktypeRepo.findOne({ const duplicatedWorktype = await worktypeRepo.findOne({

View File

@ -17,10 +17,6 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$
&& apt-get install default-jre -y \ && apt-get install default-jre -y \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# Update NPM
RUN npm install -g npm
# Install mob # Install mob
RUN curl -sL install.mob.sh | sh RUN curl -sL install.mob.sh | sh

View File

@ -1,5 +1,5 @@
VITE_STAGE=production VITE_STAGE=production
VITE_B2C_CLIENTID=b0ec473b-6b2b-4f12-adc6-39a24ebe6a3f VITE_B2C_CLIENTID=ea6e2535-c914-4889-8659-7ca1ec2e420d
VITE_B2C_AUTHORITY=https://adb2codmsprod.b2clogin.com/adb2codmsprod.onmicrosoft.com/b2c_1_signin_prod VITE_B2C_AUTHORITY=https://adb2codmsproduction.b2clogin.com/adb2codmsproduction.onmicrosoft.com/b2c_1_signin_production
VITE_B2C_KNOWNAUTHORITIES=adb2codmsprod.b2clogin.com VITE_B2C_KNOWNAUTHORITIES=adb2codmsproduction.b2clogin.com
VITE_DESK_TOP_APP_SCHEME=odms-desktopapp VITE_DESK_TOP_APP_SCHEME=odms-desktopapp

View File

@ -1,5 +1,5 @@
VITE_STAGE=staging VITE_STAGE=staging
VITE_B2C_CLIENTID=5d8f0db9-4506-41d6-a5bb-5ec39f6eba8d VITE_B2C_CLIENTID=6ddb8ca0-c39e-4eba-a3c1-d18ea289a315
VITE_B2C_AUTHORITY=https://adb2codmsstg.b2clogin.com/adb2codmsstg.onmicrosoft.com/b2c_1_signin_stg VITE_B2C_AUTHORITY=https://adb2codmsstaging.b2clogin.com/adb2codmsstaging.onmicrosoft.com/b2c_1_signin_staging
VITE_B2C_KNOWNAUTHORITIES=adb2codmsstg.b2clogin.com VITE_B2C_KNOWNAUTHORITIES=adb2codmsstaging.b2clogin.com
VITE_DESK_TOP_APP_SCHEME=odms-desktopapp VITE_DESK_TOP_APP_SCHEME=odms-desktopapp

View File

@ -27,6 +27,7 @@ module.exports = {
rules: { rules: {
"react/jsx-uses-react": "off", "react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"react/function-component-definition": [ "react/function-component-definition": [
"error", "error",
{ {

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,8 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"codegen": "sh codegen.sh", "codegen": "sh codegen.sh",
"lint": "eslint --cache . --ext .js,.ts,.tsx", "lint": "eslint --cache . --ext .js,.ts,.tsx",
"lint:fix": "npm run lint -- --fix" "lint:fix": "npm run lint -- --fix",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@azure/msal-browser": "^2.33.0", "@azure/msal-browser": "^2.33.0",
@ -25,7 +26,6 @@
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.1", "@testing-library/user-event": "^14.2.1",
"@types/jest": "^27.5.2",
"@types/node": "^17.0.45", "@types/node": "^17.0.45",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
@ -38,6 +38,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"papaparse": "^5.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-google-recaptcha-v3": "^1.10.0", "react-google-recaptcha-v3": "^1.10.0",
@ -56,8 +57,10 @@
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@mdx-js/react": "^2.1.2", "@mdx-js/react": "^2.1.2",
"@openapitools/openapi-generator-cli": "^2.5.2", "@openapitools/openapi-generator-cli": "^2.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/luxon": "^3.2.0", "@types/luxon": "^3.2.0",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@types/redux-mock-store": "^1.0.3", "@types/redux-mock-store": "^1.0.3",
@ -67,16 +70,18 @@
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.7.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"prettier": "^2.7.1", "prettier": "^2.8.8",
"redux-mock-store": "^1.5.4", "redux-mock-store": "^1.5.4",
"sass": "^1.58.3", "sass": "^1.58.3",
"ts-jest": "^29.1.2",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"vite": "^4.1.4", "vite": "^4.1.4",
"vite-plugin-env-compatible": "^1.1.1", "vite-plugin-env-compatible": "^1.1.1",
@ -99,4 +104,4 @@
} }
] ]
} }
} }

View File

@ -11,13 +11,29 @@ import { selectSnackber } from "features/ui/selectors";
import { closeSnackbar } from "features/ui/uiSlice"; import { closeSnackbar } from "features/ui/uiSlice";
import { UNAUTHORIZED_TO_CONTINUE_ERROR_CODES } from "components/auth/constants"; import { UNAUTHORIZED_TO_CONTINUE_ERROR_CODES } from "components/auth/constants";
import { clearUserInfo } from "features/login"; import { clearUserInfo } from "features/login";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
/*
UpdateTokenTimerをApp.tsxに移動する2024627
App.tsxに移動する
*/
const App = (): JSX.Element => { const App = (): JSX.Element => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { instance } = useMsal(); const { instance } = useMsal();
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [t, i18n] = useTranslation(); const [t, i18n] = useTranslation();
useEffect(() => { useEffect(() => {
// すべてのリクエストのヘッダーにX-Requested-Withを追加
globalAxios.interceptors.request.use((config) => {
// headersがあれば追加、なければ新規作成
config.headers = config.headers || {};
// X-Requested-Withを追加
config.headers["X-Requested-With"] = "XMLHttpRequest";
return config;
});
const id = globalAxios.interceptors.response.use( const id = globalAxios.interceptors.response.use(
(response: AxiosResponse) => response, (response: AxiosResponse) => response,
(e: AxiosError<{ code?: string }>) => { (e: AxiosError<{ code?: string }>) => {
@ -73,6 +89,7 @@ const App = (): JSX.Element => {
/> />
<BrowserRouter> <BrowserRouter>
<AppRouter /> <AppRouter />
<UpdateTokenTimer />
</BrowserRouter> </BrowserRouter>
</> </>
); );

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@ import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssue
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice"; import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice"; import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice"; import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice";
import licenseTrialIssue from "features/license/licenseTrialIssue/licenseTrialIssueSlice";
import searchPartners from "features/license/searchPartner/searchPartnerSlice";
import dictation from "features/dictation/dictationSlice"; 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";
@ -35,6 +37,8 @@ export const store = configureStore({
licenseSummary, licenseSummary,
licenseOrderHistory, licenseOrderHistory,
partnerLicense, partnerLicense,
licenseTrialIssue,
searchPartners,
dictation, dictation,
partner, partner,
typistGroup, typistGroup,

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.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="M24.1,38l5.7-5.7l-5.7-5.6L22,28.8l2.1,2.1c-0.9,0-1.8-0.1-2.7-0.4c-0.9-0.3-1.7-0.9-2.4-1.6
c-0.7-0.7-1.2-1.4-1.5-2.3C17.2,25.8,17,24.9,17,24c0-0.6,0.1-1.1,0.2-1.7s0.4-1.1,0.6-1.7l-2.2-2.2c-0.6,0.8-1,1.7-1.2,2.6
C14.1,22.1,14,23,14,24c0,1.3,0.2,2.5,0.8,3.8C15.2,29,16,30.1,17,31s2,1.7,3.2,2.2c1.2,0.5,2.4,0.7,3.7,0.8L22,35.9L24.1,38z
M32.4,29.5c0.6-0.8,1-1.7,1.2-2.6C33.9,25.9,34,25,34,24c0-1.3-0.2-2.5-0.7-3.8s-1.2-2.4-2.2-3.3s-2.1-1.7-3.3-2.2
c-1.2-0.5-2.5-0.7-3.7-0.7l1.9-1.9L23.9,10l-5.7,5.7l5.7,5.6l2.1-2.1L23.8,17c0.9,0,1.8,0.2,2.8,0.5s1.7,0.9,2.4,1.5
s1.2,1.4,1.5,2.3c0.4,0.9,0.5,1.7,0.5,2.6c0,0.6-0.1,1.1-0.2,1.7c-0.1,0.6-0.4,1.1-0.6,1.6L32.4,29.5z M24,44
c-2.7,0-5.3-0.5-7.8-1.6s-4.6-2.5-6.4-4.3s-3.2-3.9-4.3-6.4S4,26.7,4,24c0-2.8,0.5-5.4,1.6-7.8s2.5-4.5,4.3-6.3s3.9-3.2,6.4-4.3
S21.3,4,24,4c2.8,0,5.4,0.5,7.8,1.6s4.6,2.5,6.4,4.3s3.2,3.9,4.3,6.3c1.1,2.4,1.6,5,1.6,7.8c0,2.7-0.5,5.3-1.6,7.8
c-1,2.4-2.5,4.6-4.3,6.4s-3.9,3.2-6.4,4.3S26.8,44,24,44z M24,41c4.7,0,8.8-1.7,12-5c3.3-3.3,5-7.3,5-12c0-4.7-1.6-8.8-5-12.1
c-3.3-3.3-7.3-5-12-5c-4.7,0-8.7,1.7-12,5S7,19.3,7,24c0,4.7,1.7,8.7,5,12C15.3,39.3,19.3,41,24,41z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 42.949219 37.109375 L 33.898438 28.0625 L 33.007812 28.949219 L 31.476562 27.414062 C 33.183594 24.882812 34.046875 21.945312 34.042969 19.011719 C 34.046875 15.171875 32.578125 11.320312 29.644531 8.390625 C 26.722656 5.464844 22.867188 4 19.019531 4.003906 C 15.179688 4 11.324219 5.46875 8.398438 8.390625 C 5.46875 11.320312 4 15.167969 4.003906 19.011719 C 4 22.851562 5.46875 26.703125 8.394531 29.628906 C 11.328125 32.554688 15.179688 34.019531 19.023438 34.019531 C 21.957031 34.019531 24.902344 33.160156 27.433594 31.453125 L 28.96875 32.988281 L 28.082031 33.871094 L 37.136719 42.917969 C 37.847656 43.636719 38.796875 43.996094 39.738281 43.996094 C 40.671875 43.996094 41.621094 43.636719 42.335938 42.917969 L 42.949219 42.308594 C 43.664062 41.59375 44.027344 40.644531 44.027344 39.707031 C 44.027344 38.769531 43.664062 37.824219 42.949219 37.109375 Z M 19.023438 32.003906 C 15.6875 32.003906 12.359375 30.738281 9.824219 28.199219 C 7.285156 25.667969 6.019531 22.34375 6.019531 19.011719 C 6.019531 15.675781 7.289062 12.351562 9.824219 9.820312 C 12.359375 7.285156 15.6875 6.019531 19.019531 6.019531 C 22.355469 6.019531 25.683594 7.285156 28.21875 9.820312 C 30.757812 12.351562 32.023438 15.675781 32.027344 19.011719 C 32.023438 22.34375 30.757812 25.667969 28.222656 28.199219 C 25.683594 30.738281 22.355469 32.003906 19.023438 32.003906 Z M 28.78125 30.421875 C 29.074219 30.171875 29.367188 29.910156 29.648438 29.628906 C 29.929688 29.351562 30.191406 29.058594 30.445312 28.761719 L 31.820312 30.136719 L 30.15625 31.800781 Z M 41.523438 40.882812 L 40.910156 41.492188 C 40.582031 41.820312 40.164062 41.976562 39.734375 41.980469 C 39.308594 41.980469 38.890625 41.820312 38.5625 41.496094 L 30.9375 33.875 L 33.898438 30.917969 L 41.523438 38.535156 C 41.847656 38.863281 42.007812 39.28125 42.007812 39.707031 C 42.007812 40.136719 41.847656 40.554688 41.523438 40.882812 Z M 41.523438 40.882812 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 25.695312 12.34375 C 23.855469 10.507812 21.433594 9.585938 19.023438 9.585938 C 16.609375 9.585938 14.191406 10.507812 12.351562 12.34375 C 10.511719 14.179688 9.589844 16.601562 9.59375 19.011719 C 9.589844 21.421875 10.511719 23.84375 12.351562 25.675781 C 14.191406 27.511719 16.609375 28.433594 19.019531 28.433594 C 21.433594 28.433594 23.855469 27.511719 25.695312 25.675781 C 27.535156 23.839844 28.453125 21.421875 28.453125 19.011719 C 28.457031 16.601562 27.53125 14.183594 25.695312 12.34375 Z M 24.503906 24.488281 C 22.992188 25.996094 21.011719 26.753906 19.019531 26.75 C 17.03125 26.75 15.050781 25.996094 13.539062 24.488281 C 12.027344 22.976562 11.277344 21 11.277344 19.011719 C 11.277344 17.023438 12.027344 15.042969 13.539062 13.53125 C 15.054688 12.023438 17.03125 11.269531 19.023438 11.265625 C 21.011719 11.269531 22.992188 12.023438 24.503906 13.53125 C 26.015625 15.042969 26.769531 17.023438 26.769531 19.011719 C 26.769531 21 26.015625 22.976562 24.503906 24.488281 Z M 24.503906 24.488281 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 37.17"><defs><style>.cls-1{fill:#282828;}.cls-1,.cls-2{stroke-width:0px;}.cls-2{fill:#e6e6e6;}</style></defs><path class="cls-2" d="M42.13,35.07l-2.15,2.1L3,5.1v6.1H0V0h11.15v3h-6l36.98,32.07Z"/><path class="cls-1" d="M39.98,37.17l-2.1-2.15L74.9,3h-6.1V0h11.2v11.15h-3v-6l-37.03,32.02Z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M450-313v-371L330-564l-43-43 193-193 193 193-43 43-120-120v371h-60ZM220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@ -54,6 +54,7 @@ export const errorCodes = [
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
"E010812", // ライセンス未割当エラー
"E010908", // タイピストグループ不在エラー "E010908", // タイピストグループ不在エラー
"E010909", // タイピストグループ名重複エラー "E010909", // タイピストグループ名重複エラー
"E011001", // ワークタイプ重複エラー "E011001", // ワークタイプ重複エラー
@ -62,4 +63,26 @@ export const errorCodes = [
"E011004", // ワークタイプ使用中エラー "E011004", // ワークタイプ使用中エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
"E013002", // ワークフロー不在エラー "E013002", // ワークフロー不在エラー
"E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった)
"E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)
"E014003", // ユーザー削除エラー削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた
"E014004", // ユーザー削除エラー削除しようとしたTypistがWorkflowのTypist候補として指定されていた
"E014005", // ユーザー削除エラー削除しようとしたTypistがUserGroupに所属していた
"E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている)
"E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた)
"E014009", // ユーザー削除エラー削除しようとしたTypistが未完了のタスクのルーティングに設定されている
"E015001", // タイピストグループ削除済みエラー
"E015002", // タイピストグループがワークフローに紐づいているエラー
"E015003", // タイピストグループがルーティングされているエラー
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
"E016002", // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
"E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない)
"E017002", // 親アカウント変更不可エラー(階層関係が不正)
"E017003", // 親アカウント変更不可エラー(リージョンが同一でない)
"E018001", // パートナーアカウント削除エラー(削除条件を満たしていない)
"E019001", // パートナーアカウント取得不可エラー(階層構造が不正)
"E020001", // パートナーアカウント変更エラー(変更条件を満たしていない)
"E021001", // 音声ファイル名変更不可エラー(権限不足)
"E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在)
] as const; ] as const;

View File

@ -6,4 +6,4 @@ export type ErrorObject = {
statusCode?: number; statusCode?: number;
}; };
export type ErrorCodeType = typeof errorCodes[number]; export type ErrorCodeType = (typeof errorCodes)[number];

View File

@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/naming-convention */
// Jestによるparser.tsのテスト
import fs from "fs";
import { CSVType, parseCSV } from "./parser";
describe("parse", () => {
it("指定形式のCSV文字列をパースできる", async () => {
const text = fs.readFileSync("src/common/test/test_001.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のヘッダでない場合、例外が送出される | author_id(値がoptionial)がない", async () => {
const text = fs.readFileSync("src/common/test/test_002.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のヘッダでない場合、例外が送出される | email(値が必須)がない", async () => {
const text = fs.readFileSync("src/common/test/test_003.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のヘッダでない場合、例外が送出される | emailがスペルミス", async () => {
const text = fs.readFileSync("src/common/test/test_004.csv", "utf-8");
try {
await parseCSV(text);
fail("例外が発生しませんでした");
} catch (e) {
expect(e).toEqual(new Error("Invalid CSV format"));
}
});
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(文字列)はnullとなる", async () => {
const text = fs.readFileSync("src/common/test/test_005.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: null,
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(数値)はnullとなる", async () => {
const text = fs.readFileSync("src/common/test/test_006.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: null,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
it("指定形式のCSV文字列をパースできる | 余計なパラメータがあっても問題はない", async () => {
const text = fs.readFileSync("src/common/test/test_007.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "HOGE",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd",
prompt: 0,
},
{
name: "hoge2",
email: "sample2@example.com",
role: 1,
author_id: "HOGE2",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "abcd2",
prompt: 0,
},
];
expect(actualData.length).toBe(expectData.length);
// 余計なパラメータ格納用に __parsed_extra: string[] というプロパティが作られてしまうので、既知のプロパティ毎に比較
for (let i = 0; i < actualData.length; i += 1) {
const actualValue = actualData[i];
const expectValue = expectData[i];
expect(actualValue.author_id).toEqual(expectValue.author_id);
expect(actualValue.auto_assign).toEqual(expectValue.auto_assign);
expect(actualValue.email).toEqual(expectValue.email);
expect(actualValue.encryption).toEqual(expectValue.encryption);
expect(actualValue.encryption_password).toEqual(
expectValue.encryption_password
);
expect(actualValue.name).toEqual(expectValue.name);
expect(actualValue.notification).toEqual(expectValue.notification);
expect(actualValue.prompt).toEqual(expectValue.prompt);
expect(actualValue.role).toEqual(expectValue.role);
}
});
it("author_id,encryption_passwordが数値のみの場合でも、文字列として変換できる", async () => {
const text = fs.readFileSync("src/common/test/test_008.csv", "utf-8");
const actualData = await parseCSV(text);
const expectData: CSVType[] = [
{
name: "hoge",
email: "sample@example.com",
role: 1,
author_id: "1111",
auto_assign: 1,
notification: 1,
encryption: 1,
encryption_password: "222222",
prompt: 0,
},
];
expect(actualData).toEqual(expectData);
});
});

View File

@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/naming-convention */
import Papa, { ParseResult } from "papaparse";
export type CSVType = {
name: string | null;
email: string | null;
role: number | null;
author_id: string | null;
auto_assign: number | null;
notification: number;
encryption: number | null;
encryption_password: string | null;
prompt: number | null;
};
// CSVTypeのプロパティ名を文字列の配列で定義する
const CSVTypeFields: (keyof CSVType)[] = [
"name",
"email",
"role",
"author_id",
"auto_assign",
"notification",
"encryption",
"encryption_password",
"prompt",
];
// 2つの配列が等しいかどうかを判定する
const equals = (lhs: string[], rhs: string[]) => {
if (lhs.length !== rhs.length) return false;
for (let i = 0; i < lhs.length; i += 1) {
if (lhs[i] !== rhs[i]) return false;
}
return true;
};
/** CSVファイルをCSVType型に変換するパーサー */
export const parseCSV = async (csvString: string): Promise<CSVType[]> =>
new Promise((resolve, reject) => {
Papa.parse<CSVType>(csvString, {
download: false,
worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定
header: true,
dynamicTyping: {
// author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない
role: true,
auto_assign: true,
notification: true,
encryption: true,
prompt: true,
},
// dynamicTypingがfalseの場合、空文字をnullに変換できないためtransformを使用する
transform: (value, field) => {
if (field === "author_id" || field === "encryption_password") {
// 空文字の場合はnullに変換する
if (value === "") {
return null;
}
}
return value;
},
complete: (results: ParseResult<CSVType>) => {
// ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す
if (!equals(results.meta.fields ?? [], CSVTypeFields)) {
reject(new Error("Invalid CSV format"));
}
resolve(results.data);
},
error: (error: Error) => {
reject(error);
},
});
});

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,emeil,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
1 name emeil role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,,1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 1 1 1 abcd 0

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,,"HOGE",1,1,1,abcd,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com HOGE 1 1 1 abcd 0

View File

@ -0,0 +1,3 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x
hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,2 @@
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
hoge,sample@example.com,1,1111,1,1,1,222222,0
1 name email role author_id auto_assign notification encryption encryption_password prompt
2 hoge sample@example.com 1 1111 1 1 1 222222 0

View File

@ -47,6 +47,7 @@ export const KEYS_TO_PRESERVE = [
"accessToken", "accessToken",
"refreshToken", "refreshToken",
"displayInfo", "displayInfo",
"filterCriteria",
"sortCriteria", "sortCriteria",
]; ];

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react"; import React, { useCallback, useLayoutEffect, useState } from "react";
import { AppDispatch } from "app/store"; import { AppDispatch } from "app/store";
import { decodeToken } from "common/decodeToken"; import { decodeToken } from "common/decodeToken";
import { useInterval } from "common/useInterval"; import { useInterval } from "common/useInterval";
@ -17,41 +17,58 @@ import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants";
export const UpdateTokenTimer = () => { export const UpdateTokenTimer = () => {
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
// トークンの更新中かどうか
const [isUpdating, setIsUpdating] = useState(false);
const delegattionToken = useSelector(selectDelegationAccessToken); const delegattionToken = useSelector(selectDelegationAccessToken);
// 期限が分以内であれば更新APIを呼ぶ // 期限が分以内であれば更新APIを呼ぶ
const updateToken = useCallback(async () => { const updateToken = useCallback(async () => {
// localStorageからトークンを取得 if (isUpdating) {
const jwt = loadAccessToken(); return;
// 現在時刻を取得
const now = DateTime.local().toSeconds();
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
if (jwt) {
const token = decodeToken(jwt);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
await dispatch(updateTokenAsync());
}
}
} }
setIsUpdating(true);
// 代行操作トークン更新処理 try {
if (delegattionToken) { // localStorageからトークンを取得
const token = decodeToken(delegattionToken); const jwt = loadAccessToken();
if (token) { // 現在時刻を取得
const { exp } = token; const now = DateTime.local().toSeconds();
if (exp - now <= TOKEN_UPDATE_TIME) { // selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
const { meta } = await dispatch(updateDelegationTokenAsync()); if (jwt) {
if (meta.requestStatus === "rejected") { const token = decodeToken(jwt);
dispatch(cleanupDelegateAccount()); if (token) {
navigate("/partners"); const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
await dispatch(updateTokenAsync());
} }
} }
} }
// 代行操作トークン更新処理
if (delegattionToken) {
const token = decodeToken(delegattionToken);
if (token) {
const { exp } = token;
if (exp - now <= TOKEN_UPDATE_TIME) {
const { meta } = await dispatch(updateDelegationTokenAsync());
if (meta.requestStatus === "rejected") {
dispatch(cleanupDelegateAccount());
navigate("/partners");
}
}
}
}
} catch (e) {
// eslint-disable-next-line no-console
console.error("Token update error:", e);
} finally {
setIsUpdating(false);
} }
}, [dispatch, delegattionToken, navigate]); }, [isUpdating, delegattionToken, dispatch, navigate]);
useLayoutEffect(() => {
updateToken();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS); useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS);

View File

@ -6,8 +6,50 @@ import { getTranslationID } from "translation";
const Footer: React.FC = () => { const Footer: React.FC = () => {
const [t] = useTranslation(); const [t] = useTranslation();
return ( return (
<footer className={`${styles.footer}`}> <footer
<div>{t(getTranslationID("common.label.copyRight"))}</div> className={`${styles.footer}`}
style={{
padding: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{
marginBottom: "0.5rem",
textAlign: "center",
}}
>
{t(getTranslationID("common.label.copyRight"))}
</div>
<div
style={{
display: "flex",
gap: "1rem",
}}
>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_eula/eula/en/"
target="_blank"
className={styles.linkTx}
style={{ color: "#999999" }}
data-tag="open-eula"
rel="noreferrer"
>
{t(getTranslationID("common.label.eula"))}
</a>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_eula/privacy_notice/en/"
target="_blank"
className={styles.linkTx}
style={{ color: "#999999" }}
data-tag="open-privacy-notice"
rel="noreferrer"
>
{t(getTranslationID("common.label.privacyNotice"))}
</a>
</div>
</footer> </footer>
); );
}; };

View File

@ -4,6 +4,7 @@ import {
updateAccountInfoAsync, updateAccountInfoAsync,
getAccountRelationsAsync, getAccountRelationsAsync,
deleteAccountAsync, deleteAccountAsync,
updateFileDeleteSettingAsync,
} from "./operations"; } from "./operations";
const initialState: AccountState = { const initialState: AccountState = {
@ -15,6 +16,8 @@ const initialState: AccountState = {
tier: 0, tier: 0,
country: "", country: "",
delegationPermission: false, delegationPermission: false,
autoFileDelete: false,
fileRetentionDays: 0,
}, },
}, },
dealers: [], dealers: [],
@ -29,6 +32,8 @@ const initialState: AccountState = {
secondryAdminUserId: undefined, secondryAdminUserId: undefined,
}, },
isLoading: false, isLoading: false,
autoFileDelete: false,
fileRetentionDays: 0,
}, },
}; };
@ -64,6 +69,20 @@ export const accountSlice = createSlice({
const { secondryAdminUserId } = action.payload; const { secondryAdminUserId } = action.payload;
state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId; state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId;
}, },
changeAutoFileDelete: (
state,
action: PayloadAction<{ autoFileDelete: boolean }>
) => {
const { autoFileDelete } = action.payload;
state.apps.autoFileDelete = autoFileDelete;
},
changeFileRetentionDays: (
state,
action: PayloadAction<{ fileRetentionDays: number }>
) => {
const { fileRetentionDays } = action.payload;
state.apps.fileRetentionDays = fileRetentionDays;
},
cleanupApps: (state) => { cleanupApps: (state) => {
state.domain = initialState.domain; state.domain = initialState.domain;
}, },
@ -85,6 +104,10 @@ export const accountSlice = createSlice({
action.payload.accountInfo.account.primaryAdminUserId; action.payload.accountInfo.account.primaryAdminUserId;
state.apps.updateAccountInfo.secondryAdminUserId = state.apps.updateAccountInfo.secondryAdminUserId =
action.payload.accountInfo.account.secondryAdminUserId; action.payload.accountInfo.account.secondryAdminUserId;
state.apps.autoFileDelete =
action.payload.accountInfo.account.autoFileDelete;
state.apps.fileRetentionDays =
action.payload.accountInfo.account.fileRetentionDays;
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(getAccountRelationsAsync.rejected, (state) => { builder.addCase(getAccountRelationsAsync.rejected, (state) => {
@ -99,6 +122,15 @@ export const accountSlice = createSlice({
builder.addCase(updateAccountInfoAsync.rejected, (state) => { builder.addCase(updateAccountInfoAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(updateFileDeleteSettingAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateFileDeleteSettingAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateFileDeleteSettingAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteAccountAsync.pending, (state) => { builder.addCase(deleteAccountAsync.pending, (state) => {
state.apps.isLoading = true; state.apps.isLoading = true;
}); });
@ -115,6 +147,8 @@ export const {
changeDealerPermission, changeDealerPermission,
changePrimaryAdministrator, changePrimaryAdministrator,
changeSecondryAdministrator, changeSecondryAdministrator,
changeAutoFileDelete,
changeFileRetentionDays,
cleanupApps, cleanupApps,
} = accountSlice.actions; } = accountSlice.actions;
export default accountSlice.reducer; export default accountSlice.reducer;

View File

@ -9,6 +9,7 @@ import {
UpdateAccountInfoRequest, UpdateAccountInfoRequest,
UsersApi, UsersApi,
DeleteAccountRequest, DeleteAccountRequest,
UpdateFileDeleteSettingRequest,
} from "../../api/api"; } from "../../api/api";
import { Configuration } from "../../api/configuration"; import { Configuration } from "../../api/configuration";
import { ViewAccountRelationsInfo } from "./types"; import { ViewAccountRelationsInfo } from "./types";
@ -38,7 +39,7 @@ export const getAccountRelationsAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
const dealers = await accountsApi.getDealers(); const dealers = await accountsApi.getDealers();
const users = await usersApi.getUsers({ const users = await usersApi.getUsers(undefined, undefined, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
return { return {
@ -112,6 +113,58 @@ export const updateAccountInfoAsync = createAsyncThunk<
} }
}); });
export const updateFileDeleteSettingAsync = createAsyncThunk<
{
/* Empty Object */
},
{ autoFileDelete: boolean; fileRetentionDays: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/updateFileDeleteSettingAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const requestParam: UpdateFileDeleteSettingRequest = {
autoFileDelete: args.autoFileDelete,
retentionDays: args.fileRetentionDays,
};
try {
await accountApi.updateFileDeleteSetting(requestParam, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const deleteAccountAsync = createAsyncThunk< export const deleteAccountAsync = createAsyncThunk<
{ {
/* Empty Object */ /* Empty Object */

View File

@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) =>
state.account.apps.isLoading; state.account.apps.isLoading;
export const selectUpdateAccountInfo = (state: RootState) => export const selectUpdateAccountInfo = (state: RootState) =>
state.account.apps.updateAccountInfo; state.account.apps.updateAccountInfo;
export const selectFileDeleteSetting = (state: RootState) => {
const { autoFileDelete, fileRetentionDays } = state.account.apps;
return {
autoFileDelete,
fileRetentionDays,
};
};
export const selectInputValidationErrors = (state: RootState) => {
const { fileRetentionDays } = state.account.apps;
const hasFileRetentionDaysError =
fileRetentionDays <= 0 || fileRetentionDays >= 1000;
return {
hasFileRetentionDaysError,
};
};

View File

@ -19,4 +19,6 @@ export interface Domain {
export interface Apps { export interface Apps {
updateAccountInfo: UpdateAccountInfoRequest; updateAccountInfo: UpdateAccountInfoRequest;
isLoading: boolean; isLoading: boolean;
autoFileDelete: boolean;
fileRetentionDays: number;
} }

View File

@ -6,7 +6,7 @@ export const STATUS = {
BACKUP: "Backup", BACKUP: "Backup",
} as const; } as const;
export type StatusType = typeof STATUS[keyof typeof STATUS]; export type StatusType = (typeof STATUS)[keyof typeof STATUS];
export const LIMIT_TASK_NUM = 100; export const LIMIT_TASK_NUM = 100;
@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = {
TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE", TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE",
} as const; } as const;
export type SortableColumnType = export type SortableColumnType =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
export const isSortableColumnType = ( export const isSortableColumnType = (
value: string value: string
@ -36,14 +36,14 @@ export const isSortableColumnType = (
}; };
export type SortableColumnList = export type SortableColumnList =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
export const DIRECTION = { export const DIRECTION = {
ASC: "ASC", ASC: "ASC",
DESC: "DESC", DESC: "DESC",
} as const; } as const;
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION];
// DirectionTypeの型チェック関数 // DirectionTypeの型チェック関数
export const isDirectionType = (arg: string): arg is DirectionType => export const isDirectionType = (arg: string): arg is DirectionType =>

View File

@ -11,6 +11,8 @@ import {
playbackAsync, playbackAsync,
updateAssigneeAsync, updateAssigneeAsync,
cancelAsync, cancelAsync,
deleteTaskAsync,
renameFileAsync,
} from "./operations"; } from "./operations";
import { import {
SORTABLE_COLUMN, SORTABLE_COLUMN,
@ -41,6 +43,8 @@ const initialState: DictationState = {
direction: DIRECTION.ASC, direction: DIRECTION.ASC,
paramName: SORTABLE_COLUMN.JobNumber, paramName: SORTABLE_COLUMN.JobNumber,
selectedTask: undefined, selectedTask: undefined,
authorId: "",
fileName: "",
assignee: { assignee: {
selected: [], selected: [],
pool: [], pool: [],
@ -76,6 +80,14 @@ export const dictationSlice = createSlice({
const { paramName } = action.payload; const { paramName } = action.payload;
state.apps.paramName = paramName; state.apps.paramName = paramName;
}, },
changeAuthorId: (state, action: PayloadAction<{ authorId: string }>) => {
const { authorId } = action.payload;
state.apps.authorId = authorId;
},
changeFileName: (state, action: PayloadAction<{ fileName: string }>) => {
const { fileName } = action.payload;
state.apps.fileName = fileName;
},
changeSelectedTask: (state, action: PayloadAction<{ task: Task }>) => { changeSelectedTask: (state, action: PayloadAction<{ task: Task }>) => {
const { task } = action.payload; const { task } = action.payload;
state.apps.selectedTask = task; state.apps.selectedTask = task;
@ -218,6 +230,25 @@ export const dictationSlice = createSlice({
builder.addCase(backupTasksAsync.rejected, (state) => { builder.addCase(backupTasksAsync.rejected, (state) => {
state.apps.isDownloading = false; state.apps.isDownloading = false;
}); });
builder.addCase(deleteTaskAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteTaskAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteTaskAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(renameFileAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(renameFileAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(renameFileAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });
@ -225,6 +256,8 @@ export const {
changeDisplayInfo, changeDisplayInfo,
changeDirection, changeDirection,
changeParamName, changeParamName,
changeAuthorId,
changeFileName,
changeSelectedTask, changeSelectedTask,
changeAssignee, changeAssignee,
changeBackupTaskChecked, changeBackupTaskChecked,

View File

@ -35,6 +35,8 @@ export const listTasksAsync = createAsyncThunk<
filter?: string; filter?: string;
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
authorId?: string;
fileName?: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -43,7 +45,8 @@ export const listTasksAsync = createAsyncThunk<
}; };
} }
>("dictations/listTasksAsync", async (args, thunkApi) => { >("dictations/listTasksAsync", async (args, thunkApi) => {
const { limit, offset, filter, direction, paramName } = args; const { limit, offset, filter, direction, paramName, authorId, fileName } =
args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -60,6 +63,8 @@ export const listTasksAsync = createAsyncThunk<
filter, filter,
direction, direction,
paramName, paramName,
authorId,
fileName,
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
@ -80,6 +85,136 @@ export const listTasksAsync = createAsyncThunk<
} }
}); });
export const getTaskFiltersAsync = createAsyncThunk<
{
authorId?: string;
fileName?: string;
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/getTaskFiltersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
const usertaskfilter = await usersApi.getTaskFilter({
headers: { authorization: `Bearer ${accessToken}` },
});
const { authorId, fileName } = usertaskfilter.data;
return { authorId, fileName };
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateTaskFiltersAsync = createAsyncThunk<
{
/** empty */
},
{
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateTaskFiltersAsync", async (args, thunkApi) => {
const { filterConditionAuthorId, filterConditionFileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateSortColumnAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateSortColumnAsync", async (args, thunkApi) => {
const { direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getSortColumnAsync = createAsyncThunk< export const getSortColumnAsync = createAsyncThunk<
{ {
direction: DirectionType; direction: DirectionType;
@ -280,6 +415,8 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
filterConditionAuthorId: string;
filterConditionFileName: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -288,7 +425,13 @@ export const playbackAsync = createAsyncThunk<
}; };
} }
>("dictations/playbackAsync", async (args, thunkApi) => { >("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName } = args; const {
audioFileId,
direction,
paramName,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -305,6 +448,12 @@ export const playbackAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
); );
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await tasksApi.checkout(audioFileId, { await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
@ -343,6 +492,30 @@ export const playbackAsync = createAsyncThunk<
); );
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
// ライセンスの有効期限が切れている場合
if (error.code === "E010805") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.licenseExpiredError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// ライセンスが未割当の場合
if (error.code === "E010812") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.licenseNotAssignedError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch( thunkApi.dispatch(
openSnackbar({ openSnackbar({
@ -363,6 +536,8 @@ export const cancelAsync = createAsyncThunk<
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
isTypist: boolean; isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -371,7 +546,14 @@ export const cancelAsync = createAsyncThunk<
}; };
} }
>("dictations/cancelAsync", async (args, thunkApi) => { >("dictations/cancelAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args; const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -382,15 +564,25 @@ export const cancelAsync = createAsyncThunk<
const tasksApi = new TasksApi(config); const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config); const usersApi = new UsersApi(config);
try { try {
// ユーザーがタイピストである場合に、ソート条件を保存する // ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
if (isTypist) { if (isTypist) {
await usersApi.updateSortCriteria( await usersApi.updateSortCriteria(
{ direction, paramName }, {
direction,
paramName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
); );
} }
await tasksApi.cancel(audioFileId, { await tasksApi.cancel(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
@ -426,6 +618,93 @@ export const cancelAsync = createAsyncThunk<
} }
}); });
export const reopenAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/reopenAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.reopen(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Finished]以外、またはタスクが存在しない場合、またはtypistで自分のタスクでない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.reopenFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listBackupPopupTasksAsync = createAsyncThunk< export const listBackupPopupTasksAsync = createAsyncThunk<
TasksResponse, TasksResponse,
{ {
@ -456,6 +735,8 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.DESC, DIRECTION.DESC,
SORTABLE_COLUMN.Status, SORTABLE_COLUMN.Status,
undefined, // backupポップアップ表示時には検索条件は未指定
undefined, // backupポップアップ表示時には検索条件は未指定
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
@ -541,10 +822,21 @@ export const backupTasksAsync = createAsyncThunk<
a.click(); a.click();
a.parentNode?.removeChild(a); a.parentNode?.removeChild(a);
// eslint-disable-next-line no-await-in-loop // バックアップ済みに更新
await tasksApi.backup(task.audioFileId, { try {
headers: { authorization: `Bearer ${accessToken}` }, // eslint-disable-next-line no-await-in-loop
}); await tasksApi.backup(task.audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
} else {
throw e;
}
}
} }
} }
@ -556,8 +848,22 @@ export const backupTasksAsync = createAsyncThunk<
); );
return {}; return {};
} catch (e) { } catch (e) {
// e ⇒ errorObjectに変換" // e ⇒ errorObjectに変換
const error = createErrorObject(e); const error = createErrorObject(e);
if (error.code === "E010603") {
// 存在しない音声ファイルをダウンロードしようとした場合
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.fileAlreadyDeletedError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch( thunkApi.dispatch(
openSnackbar({ openSnackbar({
level: "error", level: "error",
@ -568,3 +874,143 @@ export const backupTasksAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const deleteTaskAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/deleteTaskAsync", async (args, thunkApi) => {
const { audioFileId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
await tasksApi.deleteTask(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
if (error.statusCode === 400) {
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
if (error.code === "E010601") {
// タスクがInprogressの場合はエラー
message = getTranslationID("dictationPage.message.deleteFailedError");
}
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const renameFileAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
fileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/renameFileAsync", async (args, thunkApi) => {
const { audioFileId, fileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const filesApi = new FilesApi(config);
try {
await filesApi.fileRename(
{ fileName, audioFileId },
{ headers: { authorization: `Bearer ${accessToken}` } }
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
// 変更権限がない場合はエラー
if (error.code === "E021001") {
message = getTranslationID("dictationPage.message.fileRenameFailedError");
}
// ファイル名が既に存在する場合はエラー
if (error.code === "E021002") {
message = getTranslationID(
"dictationPage.message.fileNameAleadyExistsError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -72,6 +72,12 @@ export const selectDirection = (state: RootState) =>
export const selectParamName = (state: RootState) => export const selectParamName = (state: RootState) =>
state.dictation.apps.paramName; state.dictation.apps.paramName;
export const selectAuthorId = (state: RootState) =>
state.dictation.apps.authorId;
export const selectFilename = (state: RootState) =>
state.dictation.apps.fileName;
export const selectSelectedTask = (state: RootState) => export const selectSelectedTask = (state: RootState) =>
state.dictation.apps.selectedTask; state.dictation.apps.selectedTask;

View File

@ -25,6 +25,8 @@ export interface Apps {
displayInfo: DisplayInfoType; displayInfo: DisplayInfoType;
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
authorId: string;
fileName: string;
selectedTask?: Task; selectedTask?: Task;
selectedFileTask?: Task; selectedFileTask?: Task;
assignee: { assignee: {

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state"; import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = { const initialState: LicenseCardActivateState = {
apps: { apps: {
@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({
state.apps = initialState.apps; state.apps = initialState.apps;
}, },
}, },
extraReducers: (builder) => {
builder.addCase(activateCardLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(activateCardLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(activateCardLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
}); });
export const { cleanupApps } = licenseCardActivateSlice.actions; export const { cleanupApps } = licenseCardActivateSlice.actions;

View File

@ -7,3 +7,8 @@ export const STATUS = {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
ORDER_CANCELED: "Order Canceled", ORDER_CANCELED: "Order Canceled",
} as const; } as const;
export const LICENSE_TYPE = {
NORMAL: "NORMAL",
TRIAL: "TRIAL",
} as const;

View File

@ -3,7 +3,12 @@ import type { RootState } from "app/store";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice"; import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth"; import { getAccessToken } from "features/auth";
import { AccountsApi, LicensesApi } from "../../../api/api"; import {
AccountsApi,
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration"; import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors"; import { ErrorObject, createErrorObject } from "../../../common/errors";
import { OrderHistoryView } from "./types"; import { OrderHistoryView } from "./types";
@ -15,6 +20,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
// パラメータ // パラメータ
limit: number; limit: number;
offset: number; offset: number;
selectedRow?: PartnerLicenseInfo | SearchPartner;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -23,7 +29,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
}; };
} }
>("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => { >("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => {
const { limit, offset } = args; const { limit, offset, selectedRow } = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
const state = getState() as RootState; const state = getState() as RootState;
@ -33,7 +39,6 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
const accountsApi = new AccountsApi(config); const accountsApi = new AccountsApi(config);
try { try {
const { selectedRow } = state.partnerLicense.apps;
let accountId = 0; let accountId = 0;
let companyName = ""; let companyName = "";
// 他の画面から指定されていない場合はログインアカウントのidを取得する // 他の画面から指定されていない場合はログインアカウントのidを取得する
@ -46,7 +51,9 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
companyName = getMyAccountResponse.data.account.companyName; companyName = getMyAccountResponse.data.account.companyName;
} else { } else {
accountId = selectedRow.accountId; accountId = selectedRow.accountId;
companyName = selectedRow.companyName; // パートナーライセンスとパートナー検索で型が異なるため、型ガードで推論させる
if ("companyName" in selectedRow) companyName = selectedRow.companyName;
if ("name" in selectedRow) companyName = selectedRow.name;
} }
const res = await accountsApi.getOrderHistories( const res = await accountsApi.getOrderHistories(

View File

@ -1,6 +1,10 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { LicenseSummaryState } from "./state"; import { LicenseSummaryState } from "./state";
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations"; import {
getCompanyNameAsync,
getLicenseSummaryAsync,
updateRestrictionStatusAsync,
} from "./operations";
const initialState: LicenseSummaryState = { const initialState: LicenseSummaryState = {
domain: { domain: {
@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getLicenseSummaryAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => { builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
state.domain.licenseSummaryInfo = action.payload; state.domain.licenseSummaryInfo = action.payload;
state.apps.isLoading = false;
}); });
builder.addCase(getLicenseSummaryAsync.rejected, (state) => {
state.apps.isLoading = false;
});
// 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない
// 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => { builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
state.domain.accountInfo.companyName = action.payload.companyName; state.domain.accountInfo.companyName = action.payload.companyName;
}); });
builder.addCase(updateRestrictionStatusAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateRestrictionStatusAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });

View File

@ -7,7 +7,9 @@ import {
AccountsApi, AccountsApi,
GetCompanyNameResponse, GetCompanyNameResponse,
GetLicenseSummaryResponse, GetLicenseSummaryResponse,
SearchPartner,
PartnerLicenseInfo, PartnerLicenseInfo,
UpdateRestrictionStatusRequest,
} from "../../../api/api"; } from "../../../api/api";
import { Configuration } from "../../../api/configuration"; import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors"; import { ErrorObject, createErrorObject } from "../../../common/errors";
@ -16,7 +18,7 @@ export const getLicenseSummaryAsync = createAsyncThunk<
// 正常時の戻り値の型 // 正常時の戻り値の型
GetLicenseSummaryResponse, GetLicenseSummaryResponse,
// 引数 // 引数
{ selectedRow?: PartnerLicenseInfo }, { selectedRow?: PartnerLicenseInfo | SearchPartner },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
rejectValue: { rejectValue: {
@ -72,7 +74,7 @@ export const getCompanyNameAsync = createAsyncThunk<
// 正常時の戻り値の型 // 正常時の戻り値の型
GetCompanyNameResponse, GetCompanyNameResponse,
// 引数 // 引数
{ selectedRow?: PartnerLicenseInfo }, { selectedRow?: PartnerLicenseInfo | SearchPartner },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
rejectValue: { rejectValue: {
@ -123,3 +125,58 @@ export const getCompanyNameAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const updateRestrictionStatusAsync = createAsyncThunk<
{
/* Empty Object */
},
{
accountId: number;
restricted: boolean;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const requestParam: UpdateRestrictionStatusRequest = {
accountId: args.accountId,
restricted: args.restricted,
};
try {
await accountApi.updateRestrictionStatus(requestParam, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
// このAPIでは個別のエラーメッセージは不要
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,10 +1,11 @@
import { RootState } from "app/store"; import { RootState } from "app/store";
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
export const selecLicenseSummaryInfo = (state: RootState) => export const selectLicenseSummaryInfo = (state: RootState) =>
state.licenseSummary.domain.licenseSummaryInfo; state.licenseSummary.domain.licenseSummaryInfo;
export const selectCompanyName = (state: RootState) => export const selectCompanyName = (state: RootState) =>
state.licenseSummary.domain.accountInfo.companyName; state.licenseSummary.domain.accountInfo.companyName;
export const selectIsLoading = (state: RootState) => state.license; export const selectIsLoading = (state: RootState) =>
state.licenseSummary.apps.isLoading;

View File

@ -0,0 +1,2 @@
export const ISSUED_TRIAL_LICENSE_QUANTITY = 10;
export const TRIAL_LICENSE_EXPIRATION_DAY = 30;

View File

@ -0,0 +1,5 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./licenseTrialIssueSlice";
export * from "./constants";

View File

@ -0,0 +1,60 @@
import { createSlice } from "@reduxjs/toolkit";
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
import { LicenseTrialIssueState } from "./state";
import { issueTrialLicenseAsync } from "./operations";
import {
TRIAL_LICENSE_EXPIRATION_DAY,
ISSUED_TRIAL_LICENSE_QUANTITY,
} from "./constants";
const initialState: LicenseTrialIssueState = {
apps: {
isLoading: false,
expirationDate: "",
quantity: ISSUED_TRIAL_LICENSE_QUANTITY,
},
};
export const licenseTrialIssueSlice = createSlice({
name: "licenseTrialIssue",
initialState,
reducers: {
cleanupApps: (state) => {
state.apps = initialState.apps;
},
setExpirationDate: (state) => {
// 有効期限を設定
const currentDate = new Date();
const expiryDate = new Date();
expiryDate.setDate(currentDate.getDate() + TRIAL_LICENSE_EXPIRATION_DAY);
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
const expirationDateLocal = convertLocalToUTCDate(expiryDate);
const expirationDateWithoutTime = new Date(
expirationDateLocal.getFullYear(),
expirationDateLocal.getMonth(),
expirationDateLocal.getDate()
);
const expirationYear = expirationDateWithoutTime.getFullYear();
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
const expirationDay = expirationDateWithoutTime.getDate();
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay} (${TRIAL_LICENSE_EXPIRATION_DAY})`;
state.apps.expirationDate = formattedExpirationDate;
},
},
extraReducers: (builder) => {
builder.addCase(issueTrialLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(issueTrialLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(issueTrialLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { cleanupApps, setExpirationDate } =
licenseTrialIssueSlice.actions;
export default licenseTrialIssueSlice.reducer;

View File

@ -0,0 +1,84 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import {
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const issueTrialLicenseAsync = createAsyncThunk<
{
/* Empty Object */
},
{
selectedRow?: PartnerLicenseInfo | SearchPartner;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/issueTrialLicenseAsync", async (args, thunkApi) => {
const { selectedRow } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
if (!selectedRow) {
// アカウントが選択されていない場合はエラーとする。
const errorMessage = getTranslationID(
"trialLicenseIssuePopupPage.message.accountNotSelected"
);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return {};
}
// トライアルライセンス発行処理を実行
await licensesApi.issueTrialLicenses(
{
issuedAccount: selectedRow.accountId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,10 @@
import { RootState } from "app/store";
export const selectIsLoading = (state: RootState) =>
state.licenseTrialIssue.apps.isLoading;
export const selectExpirationDate = (state: RootState) =>
state.licenseTrialIssue.apps.expirationDate;
export const selectNumberOfLicenses = (state: RootState) =>
state.licenseTrialIssue.apps.quantity;

View File

@ -0,0 +1,9 @@
export interface LicenseTrialIssueState {
apps: Apps;
}
export interface Apps {
isLoading: boolean;
expirationDate: string;
quantity: number;
}

View File

@ -105,3 +105,82 @@ export const getPartnerLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const switchParentAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
to: number;
children: number[];
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/switchParentAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
const { to, children } = args;
try {
await accountsApi.switchParent(
{
to,
children,
},
{
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");
// TODO:エラー処理
if (error.code === "E017001") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.accountNotFoundError"
);
}
if (error.code === "E017002") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.hierarchyMismatchError"
);
}
if (error.code === "E017003") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.regionMismatchError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,7 +1,11 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { PartnerLicenseInfo } from "api"; import { PartnerLicenseInfo } from "api";
import { PartnerLicensesState, HierarchicalElement } from "./state"; import { PartnerLicensesState, HierarchicalElement } from "./state";
import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations"; import {
getMyAccountAsync,
getPartnerLicenseAsync,
switchParentAsync,
} from "./operations";
import { ACCOUNTS_VIEW_LIMIT } from "./constants"; import { ACCOUNTS_VIEW_LIMIT } from "./constants";
const initialState: PartnerLicensesState = { const initialState: PartnerLicensesState = {
@ -12,6 +16,8 @@ const initialState: PartnerLicensesState = {
tier: 0, tier: 0,
country: "", country: "",
delegationPermission: false, delegationPermission: false,
autoFileDelete: false,
fileRetentionDays: 0,
}, },
total: 0, total: 0,
ownPartnerLicense: { ownPartnerLicense: {
@ -19,6 +25,7 @@ const initialState: PartnerLicensesState = {
tier: 0, tier: 0,
companyName: "", companyName: "",
stockLicense: 0, stockLicense: 0,
allocatedLicense: 0,
issuedRequested: 0, issuedRequested: 0,
shortage: 0, shortage: 0,
issueRequesting: 0, issueRequesting: 0,
@ -33,6 +40,9 @@ const initialState: PartnerLicensesState = {
hierarchicalElements: [], hierarchicalElements: [],
isLoading: true, isLoading: true,
selectedRow: undefined, selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
isSearchPopupOpen: false,
}, },
}; };
@ -61,6 +71,9 @@ export const partnerLicenseSlice = createSlice({
const { deleteCount } = action.payload; const { deleteCount } = action.payload;
state.apps.hierarchicalElements.splice(-deleteCount); state.apps.hierarchicalElements.splice(-deleteCount);
}, },
clearHierarchicalElement: (state) => {
state.apps.hierarchicalElements = [];
},
changeSelectedRow: ( changeSelectedRow: (
state, state,
action: PayloadAction<{ value?: PartnerLicenseInfo }> action: PayloadAction<{ value?: PartnerLicenseInfo }>
@ -79,6 +92,24 @@ export const partnerLicenseSlice = createSlice({
state.apps.limit = limit; state.apps.limit = limit;
state.apps.offset = offset; state.apps.offset = offset;
}, },
setIsLicenseOrderHistoryOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
},
setIsViewDetailsOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isViewDetailsOpen = action.payload.value;
},
setIsSearchPopupOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isSearchPopupOpen = action.payload.value;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getMyAccountAsync.pending, (state) => { builder.addCase(getMyAccountAsync.pending, (state) => {
@ -104,14 +135,27 @@ export const partnerLicenseSlice = createSlice({
builder.addCase(getPartnerLicenseAsync.rejected, (state) => { builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(switchParentAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(switchParentAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(switchParentAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });
export const { export const {
pushHierarchicalElement, pushHierarchicalElement,
popHierarchicalElement, popHierarchicalElement,
spliceHierarchicalElement, spliceHierarchicalElement,
clearHierarchicalElement,
changeSelectedRow, changeSelectedRow,
savePageInfo, savePageInfo,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
setIsSearchPopupOpen,
} = partnerLicenseSlice.actions; } = partnerLicenseSlice.actions;
export default partnerLicenseSlice.reducer; export default partnerLicenseSlice.reducer;

View File

@ -30,3 +30,10 @@ export const selectCurrentPage = (state: RootState) => {
}; };
export const selectSelectedRow = (state: RootState) => export const selectSelectedRow = (state: RootState) =>
state.partnerLicense.apps.selectedRow; state.partnerLicense.apps.selectedRow;
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
state.partnerLicense.apps.isLicenseOrderHistoryOpen;
export const selectIsViewDetailsOpen = (state: RootState) =>
state.partnerLicense.apps.isViewDetailsOpen;
export const selectIsSearchPopupOpen = (state: RootState) =>
state.partnerLicense.apps.isSearchPopupOpen;

View File

@ -20,6 +20,9 @@ export interface Apps {
hierarchicalElements: HierarchicalElement[]; hierarchicalElements: HierarchicalElement[];
isLoading: boolean; isLoading: boolean;
selectedRow?: PartnerLicenseInfo; selectedRow?: PartnerLicenseInfo;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
isSearchPopupOpen: boolean;
} }
export interface HierarchicalElement { export interface HierarchicalElement {

View File

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

View File

@ -0,0 +1,96 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getAccessToken } from "features/auth";
import type { RootState } from "../../../app/store";
import { getTranslationID } from "../../../translation";
import { openSnackbar } from "../../ui/uiSlice";
import { AccountsApi, SearchPartner, PartnerHierarchy } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const searchPartnersAsync = createAsyncThunk<
// 正常時の戻り値の型
SearchPartner[],
// 引数
{
companyName?: string;
accountId?: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/searchPartners", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { companyName, accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const searchPartnerResponse = await accountsApi.searchPartners(
companyName,
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return searchPartnerResponse.data.searchResult;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getPartnerHierarchy = createAsyncThunk<
// 正常時の戻り値の型
PartnerHierarchy[],
// 引数
{
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/getPartnerHierarchy", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const partnerHierarchyResponse = await accountsApi.getPartnerHierarchy(
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return partnerHierarchyResponse.data.accountHierarchy;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,80 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { SearchPartner } from "../../../api";
import { SearchPartnerState } from "./state";
import { searchPartnersAsync, getPartnerHierarchy } from "./operations";
const initialState: SearchPartnerState = {
domain: {
searchResult: [],
partnerHierarchy: [],
},
apps: {
isLoading: false,
selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
},
};
export const searchPartnersSlice = createSlice({
name: "searchPartners",
initialState,
reducers: {
changeSelectedRow: (
state,
action: PayloadAction<{ value?: SearchPartner }>
) => {
const { value } = action.payload;
state.apps.selectedRow = value;
},
setIsLicenseOrderHistoryOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
},
setIsViewDetailsOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isViewDetailsOpen = action.payload.value;
},
cleanupSearchResult: (state) => {
state.domain.searchResult = initialState.domain.searchResult;
},
cleanupPartnerHierarchy: (state) => {
state.domain.partnerHierarchy = initialState.domain.partnerHierarchy;
},
},
extraReducers: (builder) => {
builder.addCase(searchPartnersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(searchPartnersAsync.fulfilled, (state, action) => {
state.domain.searchResult = action.payload;
state.apps.isLoading = false;
});
builder.addCase(searchPartnersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getPartnerHierarchy.fulfilled, (state, action) => {
state.domain.partnerHierarchy = action.payload;
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
changeSelectedRow,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
cleanupSearchResult,
cleanupPartnerHierarchy,
} = searchPartnersSlice.actions;
export default searchPartnersSlice.reducer;

View File

@ -0,0 +1,14 @@
import { RootState } from "../../../app/store";
export const selectSearchResult = (state: RootState) =>
state.searchPartners.domain.searchResult;
export const selectPartnerHierarchy = (state: RootState) =>
state.searchPartners.domain.partnerHierarchy;
export const selectIsLoading = (state: RootState) =>
state.searchPartners.apps.isLoading;
export const selectSelectedRow = (state: RootState) =>
state.searchPartners.apps.selectedRow;
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
state.searchPartners.apps.isLicenseOrderHistoryOpen;
export const selectIsViewDetailsOpen = (state: RootState) =>
state.searchPartners.apps.isViewDetailsOpen;

View File

@ -0,0 +1,18 @@
import { SearchPartner, PartnerHierarchy } from "../../../api/api";
export interface SearchPartnerState {
domain: Domain;
apps: Apps;
}
export interface Domain {
searchResult: SearchPartner[];
partnerHierarchy: PartnerHierarchy[];
}
export interface Apps {
isLoading: boolean;
selectedRow?: SearchPartner;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
}

View File

@ -8,6 +8,8 @@ import {
AccountsApi, AccountsApi,
CreatePartnerAccountRequest, CreatePartnerAccountRequest,
GetPartnersResponse, GetPartnersResponse,
DeletePartnerAccountRequest,
GetPartnerUsersResponse,
} from "../../api/api"; } from "../../api/api";
import { Configuration } from "../../api/configuration"; import { Configuration } from "../../api/configuration";
@ -116,3 +118,170 @@ export const getPartnerInfoAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
// パートナーアカウント削除
export const deletePartnerAccountAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/deletePartnerAccountAsync", async (args, thunkApi) => {
const { accountId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
const deletePartnerAccountRequest: DeletePartnerAccountRequest = {
targetAccountId: accountId,
};
await accountApi.deletePartnerAccount(deletePartnerAccountRequest, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
if (error.code === "E018001") {
errorMessage = getTranslationID(
"partnerPage.message.partnerDeleteFailedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
// パートナーアカウントユーザー取得
export const getPartnerUsersAsync = createAsyncThunk<
GetPartnerUsersResponse,
{
// パラメータ
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/getPartnerUsersAsync", async (args, thunkApi) => {
const { accountId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
const res = await accountApi.getPartnerUsers(
{ targetAccountId: accountId },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
// パートナーアカウントユーザー編集
export const editPartnerInfoAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("partner/editPartnerInfoAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const { id, companyName, selectedAdminId } = state.partner.apps.editPartner;
try {
await accountApi.updatePartnerInfo(
{
targetAccountId: id,
primaryAdminUserId: selectedAdminId,
companyName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
if (error.code === "E010502" || error.code === "E020001") {
errorMessage = getTranslationID("partnerPage.message.editFailedError");
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,6 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { PartnerState } from "./state"; import { PartnerState } from "./state";
import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations"; import {
createPartnerAccountAsync,
getPartnerInfoAsync,
deletePartnerAccountAsync,
getPartnerUsersAsync,
editPartnerInfoAsync,
} from "./operations";
import { LIMIT_PARTNER_VIEW_NUM } from "./constants"; import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
const initialState: PartnerState = { const initialState: PartnerState = {
@ -17,6 +23,13 @@ const initialState: PartnerState = {
adminName: "", adminName: "",
email: "", email: "",
}, },
editPartner: {
users: [],
id: 0,
companyName: "",
country: "",
selectedAdminId: 0,
},
limit: LIMIT_PARTNER_VIEW_NUM, limit: LIMIT_PARTNER_VIEW_NUM,
offset: 0, offset: 0,
isLoading: false, isLoading: false,
@ -75,6 +88,37 @@ export const partnerSlice = createSlice({
state.apps.delegatedAccountId = undefined; state.apps.delegatedAccountId = undefined;
state.apps.delegatedCompanyName = undefined; state.apps.delegatedCompanyName = undefined;
}, },
changeEditPartner: (
state,
action: PayloadAction<{
id: number;
companyName: string;
country: string;
}>
) => {
const { id, companyName, country } = action.payload;
state.apps.editPartner.id = id;
state.apps.editPartner.companyName = companyName;
state.apps.editPartner.country = country;
},
changeEditCompanyName: (
state,
action: PayloadAction<{ companyName: string }>
) => {
const { companyName } = action.payload;
state.apps.editPartner.companyName = companyName;
},
changeSelectedAdminId: (
state,
action: PayloadAction<{ adminId: number }>
) => {
const { adminId } = action.payload;
state.apps.editPartner.selectedAdminId = adminId;
},
cleanupPartnerAccount: (state) => {
state.apps.editPartner = initialState.apps.editPartner;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(createPartnerAccountAsync.pending, (state) => { builder.addCase(createPartnerAccountAsync.pending, (state) => {
@ -97,6 +141,37 @@ export const partnerSlice = createSlice({
builder.addCase(getPartnerInfoAsync.rejected, (state) => { builder.addCase(getPartnerInfoAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(deletePartnerAccountAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deletePartnerAccountAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deletePartnerAccountAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getPartnerUsersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => {
const { users } = action.payload;
state.apps.editPartner.users = users;
state.apps.editPartner.selectedAdminId =
users.find((user) => user.isPrimaryAdmin)?.id ?? 0;
state.apps.isLoading = false;
});
builder.addCase(getPartnerUsersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(editPartnerInfoAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(editPartnerInfoAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(editPartnerInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });
export const { export const {
@ -108,5 +183,9 @@ export const {
savePageInfo, savePageInfo,
changeDelegateAccount, changeDelegateAccount,
cleanupDelegateAccount, cleanupDelegateAccount,
changeEditPartner,
changeEditCompanyName,
changeSelectedAdminId,
cleanupPartnerAccount,
} = partnerSlice.actions; } = partnerSlice.actions;
export default partnerSlice.reducer; export default partnerSlice.reducer;

View File

@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) =>
state.partner.apps.delegatedAccountId; state.partner.apps.delegatedAccountId;
export const selectDelegatedCompanyName = (state: RootState) => export const selectDelegatedCompanyName = (state: RootState) =>
state.partner.apps.delegatedCompanyName; state.partner.apps.delegatedCompanyName;
// edit
export const selectEditPartnerId = (state: RootState) =>
state.partner.apps.editPartner.id;
export const selectEditPartnerCompanyName = (state: RootState) =>
state.partner.apps.editPartner.companyName;
export const selectEditPartnerCountry = (state: RootState) =>
state.partner.apps.editPartner.country;
export const selectEditPartnerUsers = (state: RootState) =>
state.partner.apps.editPartner.users;
export const selectSelectedAdminId = (state: RootState) =>
state.partner.apps.editPartner.selectedAdminId;

View File

@ -1,6 +1,7 @@
import { import {
CreatePartnerAccountRequest, CreatePartnerAccountRequest,
GetPartnersResponse, GetPartnersResponse,
PartnerUser,
} from "../../api/api"; } from "../../api/api";
export interface PartnerState { export interface PartnerState {
@ -19,4 +20,11 @@ export interface Apps {
isLoading: boolean; isLoading: boolean;
delegatedAccountId?: number; delegatedAccountId?: number;
delegatedCompanyName?: string; delegatedCompanyName?: string;
editPartner: {
users: PartnerUser[];
id: number;
companyName: string;
country: string;
selectedAdminId: number;
};
} }

Some files were not shown because too many files have changed in this diff Show More