commit 0b4acd7475e0796c4f857370f9eeb8005b410734 Author: Your Name Date: Fri Jan 16 19:28:13 2026 +0900 Batch & API diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35db1dd --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitea/workflows/deploy-api.yml b/.gitea/workflows/deploy-api.yml new file mode 100644 index 0000000..d6ab06c --- /dev/null +++ b/.gitea/workflows/deploy-api.yml @@ -0,0 +1,18 @@ +name: Deploy api + +on: + push: + branches: [ "main" ] + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ["native"] + steps: + - uses: actions/checkout@v4 + - name: Deploy main to server + run: /usr/local/bin/deploy_api.sh diff --git a/.gitea/workflows/deploy-preview-api.yml b/.gitea/workflows/deploy-preview-api.yml new file mode 100644 index 0000000..ab045cb --- /dev/null +++ b/.gitea/workflows/deploy-preview-api.yml @@ -0,0 +1,27 @@ +name: Deploy preview (main_xxx) + +on: + push: + branches: + - 'main_*' + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ["native"] + steps: + - uses: actions/checkout@v4 + + - name: Set BRANCH env + shell: bash + run: | + BR="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" + echo "BRANCH=$BR" >> "$GITHUB_ENV" + echo "Branch: $BR" + + - name: Deploy to preview + run: /usr/local/bin/deploy_api_branch.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb6f629 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Laravel +/node_modules +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log + +# SHJ-1 specific ignores +*.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3049972 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# SO-Manager API & Batch + +Laravel 12 ベースのバッチ処理・API プロジェクトです。 + +## 概要 + +本プロジェクトは、SO-Manager システムのバックエンド機能を提供します。 + +- **バッチ処理**: 定期実行される各種業務処理(決済処理、メール送信など) +- **API**: 外部システム連携用 REST API + +## 動作環境 + +- PHP 8.2+ +- Laravel 12 +- MySQL 8.0+ + +## バッチコマンド + +```bash +php artisan list shj +``` + +## API + +API 認証には `X-API-Key` ヘッダーが必要です。 + +詳細は `api.doc/` ディレクトリ内のドキュメントを参照してください。 + +## ディレクトリ構成 + +``` +app/ +├── Console/Commands/ # バッチコマンド +├── Http/Controllers/Api/ # APIコントローラー +├── Models/ # Eloquentモデル +├── Services/ # ビジネスロジック +└── Mail/ # メールクラス +``` + +## 設定 + +環境変数は `.env` ファイルで管理します。 + +``` +API_KEYS=your-api-key-here +``` diff --git a/app/CommonFunction.php b/app/CommonFunction.php new file mode 100644 index 0000000..2d17ca1 --- /dev/null +++ b/app/CommonFunction.php @@ -0,0 +1,46 @@ + $digit) { + $sum += $digit * $weights[$i % count($weights)]; + } + $checkDigit = (10 - ($sum % 10)) % 10; + return $checkDigit; + } + + // 初期パスワード作成 + public static function createPassword() { + // 使用可能文字 (使用不可:1,l,L,i,I,z,Z,2,o,O,0) + $chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz3456789'; + $password = ''; + $length = 8; + $max = strlen($chars) - 1; + for ($i = 0; $i < $length; $i++) { + $password .= $chars[random_int(0, $max)]; + } + return $password; + } + + // パスワードハッシュ化 + public static function hashPassword($user_seq, $password) { + $hash = hash('sha256', $password) . $user_seq . 'SOMSALT'; + for ($i = 0; $i < 25; $i++) { + $hash = hash('sha256', $hash); + } + return $hash; + } + + // パスワード照合 + public static function verifyPassword($user_seq, $inputPassword, $hashedPassword) { + return self::hashPassword($user_seq, $inputPassword) === $hashedPassword; + } +} \ No newline at end of file diff --git a/app/Console/Commands/ShjBatchLogCommand.php b/app/Console/Commands/ShjBatchLogCommand.php new file mode 100644 index 0000000..b68128e --- /dev/null +++ b/app/Console/Commands/ShjBatchLogCommand.php @@ -0,0 +1,163 @@ +shjEightService = $shjEightService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. ShjEightServiceを呼び出して処理を実行 + * 2. 処理結果を返却する + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-8 バッチ処理ログ登録を開始します。'); + + // 引数取得 + $deviceId = (int) $this->argument('device_id'); + $processName = $this->argument('process_name'); + $jobName = $this->argument('job_name'); + $status = $this->argument('status'); + $statusComment = $this->argument('status_comment'); + $createdDate = $this->argument('created_date'); + $updatedDate = $this->argument('updated_date'); + + Log::info('SHJ-8 バッチ処理ログ登録開始', [ + 'start_time' => $startTime, + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ]); + + // ShjEightServiceを呼び出して処理を実行 + $result = $this->shjEightService->execute( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $createdDate, + $updatedDate + ); + + $endTime = now(); + + // 処理結果に応じた出力 + if ($result['result'] === 0) { + // 正常終了 + $this->info('SHJ-8 バッチ処理ログ登録が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + // 標準出力形式 (SHJ-8仕様書準拠) + $this->line('処理結果: 0'); + $this->line('異常情報: '); + + Log::info('SHJ-8 バッチ処理ログ登録完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime) + ]); + + return self::SUCCESS; + + } else { + // 異常終了 + $errorMessage = $result['error_message'] ?? '不明なエラー'; + $this->error('SHJ-8 バッチ処理ログ登録エラー: ' . $errorMessage); + + // 標準出力形式 (SHJ-8仕様書準拠) + $this->line('処理結果: 1'); + $this->line('異常情報: ' . $errorMessage); + + Log::error('SHJ-8 バッチ処理ログ登録エラー', [ + 'error_message' => $errorMessage, + 'error_code' => $result['error_code'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-8 バッチ処理ログ登録で予期しないエラーが発生しました: ' . $e->getMessage()); + + // 標準出力形式 (SHJ-8仕様書準拠 - 例外時) + $this->line('処理結果: 1'); + $this->line('異常情報: ' . $e->getMessage()); + + Log::error('SHJ-8 バッチ処理ログ登録例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjElevenCommand.php b/app/Console/Commands/ShjElevenCommand.php new file mode 100644 index 0000000..222efc7 --- /dev/null +++ b/app/Console/Commands/ShjElevenCommand.php @@ -0,0 +1,164 @@ +shjElevenService = $shjElevenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー(仕様書準拠): + * 【処理1】集計単位每个の契約台数を算出する + * 【判断1】取得件数判定 + * - 取得件数 = 0 → 【処理4】バッチログ作成(「全駐輪場契約なし」)→ 終了 + * - 取得件数 ≥ 1 → 【処理2】ゾーンマスタ処理(循環)→ 終了 + * + * ※【処理4】は各レコードの処理ごとにService層で呼び出される + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-11 現在契約台数集計を開始します。'); + Log::info('SHJ-11 現在契約台数集計開始', [ + 'start_time' => $startTime + ]); + + // 【処理1】集計単位每个の契約台数を算出する + $this->info('【処理1】集計単位每个の契約台数を算出しています...'); + $contractCounts = $this->shjElevenService->calculateContractCounts(); + + // 【判断1】取得件数判定 + $countResults = count($contractCounts); + $this->info("取得件数: {$countResults}件"); + + if ($countResults === 0) { + // 【判断1】取得件数 = 0 の場合 + $this->info('対象なしのため処理を終了します。'); + + // 【処理4】bat_job_logに直接書き込み(取得件数=0時) + $batchLogResult = $this->shjElevenService->writeBatJobLogForNoContracts(); + + // bat_job_log書き込み結果をチェック + if (!$batchLogResult['success']) { + $this->warn('bat_job_log書き込みで異常が発生しました'); + if (isset($batchLogResult['error_message'])) { + $this->warn('error_message: ' . $batchLogResult['error_message']); + } + + Log::warning('bat_job_log書き込み失敗(対象なし)', [ + 'error_message' => $batchLogResult['error_message'] ?? null + ]); + } + + Log::info('SHJ-11 現在契約台数集計完了(対象なし)', [ + 'end_time' => now(), + 'duration_seconds' => $startTime->diffInSeconds(now()), + 'bat_job_log_success' => $batchLogResult['success'] + ]); + + return self::SUCCESS; + } + + // 【判断1】取得件数 ≥ 1 の場合 + // 【処理2・3・4】ゾーンマスタ処理(各レコードごとに処理4を実行) + $this->info('【処理2】ゾーンマスタ処理を実行しています...'); + $this->info('※各レコードごとに【処理4】バッチログを作成します'); + $processResult = $this->shjElevenService->processZoneManagement($contractCounts); + + // 処理結果確認 + if ($processResult['success']) { + $endTime = now(); + $this->info('SHJ-11 現在契約台数集計が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("処理対象件数: {$countResults}件"); + $this->info("ゾーン新規作成件数: {$processResult['created_zones']}件"); + $this->info("ゾーン更新件数: {$processResult['updated_zones']}件"); + $this->info("限界台数超過件数: {$processResult['over_capacity_count']}件"); + + if (!empty($processResult['batch_log_errors'])) { + $this->warn("バッチログエラー件数: " . count($processResult['batch_log_errors']) . "件"); + } + + Log::info('SHJ-11 現在契約台数集計完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'processed_count' => $countResults, + 'created_zones' => $processResult['created_zones'], + 'updated_zones' => $processResult['updated_zones'], + 'over_capacity_count' => $processResult['over_capacity_count'], + 'batch_log_errors' => count($processResult['batch_log_errors']) + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-11 現在契約台数集計でエラーが発生しました: ' . $processResult['message']); + + Log::error('SHJ-11 現在契約台数集計エラー', [ + 'error' => $processResult['message'], + 'details' => $processResult['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-11 現在契約台数集計で予期しないエラーが発生しました: ' . $e->getMessage()); + + Log::error('SHJ-11 現在契約台数集計例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjFiveCommand.php b/app/Console/Commands/ShjFiveCommand.php new file mode 100644 index 0000000..bf8a81c --- /dev/null +++ b/app/Console/Commands/ShjFiveCommand.php @@ -0,0 +1,148 @@ +shjFiveService = $shjFiveService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. バッチログ開始記録 + * 2. 駐輪場の空き状況を取得する + * 3. 空き状況判定 + * 4. 空き待ち者の情報を取得する + * 5. 取得件数判定 + * 6. 空き待ち者への通知、またはオペレーターキュー追加処理 + * 7. バッチ処理ログを作成する + * 8. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-5 空き待ち通知処理を開始します。'); + + Log::info('SHJ-5 空き待ち通知処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-5メイン処理実行 + $result = $this->shjFiveService->executeParkVacancyNotification(); + + $endTime = now(); + $this->info('SHJ-5 空き待ち通知処理が完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + // 処理結果表示 + $this->displayProcessResult($result); + + // バッチログはShjFiveServiceが自動的にSHJ-8経由で作成 + + Log::info('SHJ-5 空き待ち通知処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return $result['success'] ? self::SUCCESS : self::FAILURE; + + } catch (\Exception $e) { + $this->error('SHJ-5 空き待ち通知処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + // エラー時もShjFiveServiceが自動的にバッチログを作成 + + Log::error('SHJ-5 空き待ち通知処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * 処理結果を表示 + * + * @param array $result 処理結果 + * @return void + */ + private function displayProcessResult(array $result): void + { + $this->line(''); + $this->info('=== 処理結果 ==='); + + if ($result['success']) { + $this->info("✓ 処理実行成功: " . $result['message']); + } else { + $this->error("✗ 処理実行失敗: " . $result['message']); + } + + $this->line(''); + $this->info('=== 処理統計 ==='); + $this->line(" 処理対象駐輪場数: " . ($result['processed_parks_count'] ?? 0)); + $this->line(" 空きあり駐輪場数: " . ($result['vacant_parks_count'] ?? 0)); + $this->line(" 空き待ち者総数: " . ($result['total_waiting_users'] ?? 0)); + $this->line(" 通知送信成功: " . ($result['notification_success_count'] ?? 0) . "件"); + $this->line(" オペレーターキュー追加: " . ($result['operator_queue_count'] ?? 0) . "件"); + $this->line(" エラー件数: " . ($result['error_count'] ?? 0) . "件"); + $this->line(" 処理時間: " . ($result['duration_seconds'] ?? 0) . "秒"); + + // エラー詳細があれば表示 + if (!empty($result['errors'])) { + $this->line(''); + $this->warn('=== エラー詳細 ==='); + foreach ($result['errors'] as $index => $error) { + $this->line(" " . ($index + 1) . ". " . $error); + } + } + } + +} diff --git a/app/Console/Commands/ShjFourBCheckCommand.php b/app/Console/Commands/ShjFourBCheckCommand.php new file mode 100644 index 0000000..5d62123 --- /dev/null +++ b/app/Console/Commands/ShjFourBCheckCommand.php @@ -0,0 +1,317 @@ + /dev/null 2>&1 + */ +class ShjFourBCheckCommand extends Command +{ + /** + * コマンド名と説明 + * + * @var string + */ + protected $signature = 'shj4b:check + {--dry-run : 実際の処理を行わず対象のみ表示} + {--limit=100 : 処理する最大件数} + {--hours=24 : 指定時間以内の決済のみ対象}'; + + /** + * コマンドの説明 + * + * @var string + */ + protected $description = 'SHJ-4B 兜底チェック - 未処理の決済トランザクションを検索してProcessSettlementJobをディスパッチ'; + + /** + * SHJ-4B サービス + * + * @var ShjFourBService + */ + protected $shjFourBService; + + /** + * コンストラクタ + */ + public function __construct(ShjFourBService $shjFourBService) + { + parent::__construct(); + $this->shjFourBService = $shjFourBService; + } + + /** + * コマンド実行 + * + * @return int + */ + public function handle() + { + $startTime = now(); + $isDryRun = $this->option('dry-run'); + $limit = (int) $this->option('limit'); + $hours = (int) $this->option('hours'); + + $this->info("SHJ-4B チェックコマンド開始"); + $this->info("実行モード: " . ($isDryRun ? "ドライラン(実際の処理なし)" : "本実行")); + $this->info("処理制限: {$limit}件"); + $this->info("対象期間: {$hours}時間以内"); + + try { + // 未処理の決済トランザクション取得 + $unprocessedSettlements = $this->getUnprocessedSettlements($hours, $limit); + + $this->info("未処理決済トランザクション: " . $unprocessedSettlements->count() . "件"); + + if ($unprocessedSettlements->isEmpty()) { + $this->info("処理対象なし"); + + // バッチログ作成 - 処理対象なし + $this->createBatchLog('success', 0, 0, 'SHJ-4B チェック完了 - 処理対象なし'); + + return 0; + } + + // 対象一覧表示 + $this->displayTargets($unprocessedSettlements); + + if ($isDryRun) { + $this->info("ドライランモードのため、実際の処理はスキップします"); + + // バッチログ作成 - ドライラン + $this->createBatchLog('success', 0, 0, 'SHJ-4B チェック完了 - ドライラン(' . $unprocessedSettlements->count() . '件検出)'); + + return 0; + } + + // 実際の処理実行 + $processed = $this->processSettlements($unprocessedSettlements); + + $this->info("処理完了: {$processed['success']}件成功, {$processed['failed']}件失敗"); + + // バッチログ作成 - 処理完了 + $status = $processed['failed'] > 0 ? 'error' : 'success'; + $message = "SHJ-4B チェック完了 - 成功:{$processed['success']}件, 失敗:{$processed['failed']}件"; + $this->createBatchLog($status, $processed['success'], $processed['failed'], $message); + + return $processed['failed'] > 0 ? 1 : 0; + + } catch (\Throwable $e) { + $this->error("SHJ-4B チェック処理でエラーが発生しました: " . $e->getMessage()); + Log::error('SHJ-4B チェックコマンドエラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + // バッチログ作成 - エラー + $this->createBatchLog('error', 0, 1, 'SHJ-4B チェック失敗: ' . $e->getMessage()); + + return 1; + } + } + + /** + * 未処理の決済トランザクション取得 + * + * @param int $hours + * @param int $limit + * @return \Illuminate\Database\Eloquent\Collection + */ + private function getUnprocessedSettlements(int $hours, int $limit) + { + $cutoffTime = Carbon::now()->subHours($hours); + + // 条件: + // 1. 指定時間以内に作成された + // 2. contract_payment_numberがnullでない + // 3. まだregular_contractのsettlement_transaction_idに関連付けられていない + // 4. ProcessSettlementJobが実行されていない(batch_logで確認) + $query = SettlementTransaction::where('created_at', '>=', $cutoffTime) + ->whereNotNull('contract_payment_number') + ->whereNotNull('pay_date') + ->whereNotNull('settlement_amount') + ->orderBy('created_at', 'asc'); + + $settlements = $query->limit($limit)->get(); + + // 既に処理済みのものを除外 + $unprocessed = $settlements->filter(function ($settlement) { + return !$this->isAlreadyProcessed($settlement); + }); + + return $unprocessed; + } + + /** + * 既に処理済みかチェック + * + * @param SettlementTransaction $settlement + * @return bool + */ + private function isAlreadyProcessed(SettlementTransaction $settlement): bool + { + // 1. regular_contractの同一contract_payment_numberが既に処理済みかチェック + $linkedContract = DB::table('regular_contract') + ->where('contract_payment_number', $settlement->contract_payment_number) + ->whereNotNull('contract_payment_day') + ->exists(); + + if ($linkedContract) { + return true; + } + + // 2. bat_job_logで処理完了記録があるかチェック + $processedInBatch = BatJobLog::where('process_name', 'SHJ-4B') + ->where('status', 'success') + ->where('status_comment', 'like', '%settlement_transaction_id:' . $settlement->settlement_transaction_id . '%') + ->exists(); + + if ($processedInBatch) { + return true; + } + + // 3. 現在キューに入っているかチェック(簡易版) + // 注: より正確にはRedis/DBキューの内容を確認する必要がある + $recentJobDispatched = BatJobLog::where('process_name', 'SHJ-4B') + ->where('status_comment', 'like', '%settlement_transaction_id:' . $settlement->settlement_transaction_id . '%') + ->where('created_at', '>=', Carbon::now()->subHours(1)) + ->exists(); + + return $recentJobDispatched; + } + + /** + * 対象一覧表示 + * + * @param \Illuminate\Database\Eloquent\Collection $settlements + */ + private function displayTargets($settlements) + { + $this->info("対象の決済トランザクション:"); + $this->table( + ['ID', '契約支払番号', '決済金額', '支払日', '作成日時'], + $settlements->map(function ($settlement) { + return [ + $settlement->settlement_transaction_id, + $settlement->contract_payment_number, + number_format($settlement->settlement_amount) . '円', + Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'), + $settlement->created_at->format('Y-m-d H:i:s'), + ]; + })->toArray() + ); + } + + /** + * 決済処理実行 + * + * @param \Illuminate\Database\Eloquent\Collection $settlements + * @return array + */ + private function processSettlements($settlements): array + { + $success = 0; + $failed = 0; + $results = []; + + foreach ($settlements as $settlement) { + try { + $this->info("処理中: 決済トランザクションID {$settlement->settlement_transaction_id}"); + + // ProcessSettlementJobをディスパッチ + ProcessSettlementJob::dispatch( + $settlement->settlement_transaction_id, + [ + 'source' => 'shj4b_check_command', + 'triggered_at' => now()->toISOString(), + ] + ); + + $success++; + $results[] = [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'status' => 'dispatched', + 'message' => 'ProcessSettlementJobディスパッチ成功', + ]; + + $this->info("✓ 成功: {$settlement->settlement_transaction_id}"); + + } catch (\Throwable $e) { + $failed++; + $results[] = [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'status' => 'failed', + 'error' => $e->getMessage(), + ]; + + $this->error("✗ 失敗: {$settlement->settlement_transaction_id} - {$e->getMessage()}"); + + Log::error('SHJ-4B チェック 個別処理失敗', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'error' => $e->getMessage(), + ]); + } + } + + return [ + 'success' => $success, + 'failed' => $failed, + 'results' => $results, + 'total' => $settlements->count(), + ]; + } + + /** + * バッチログ作成 - 直接bat_job_log書き込み + * + * @param string $status + * @param int $successCount + * @param int $errorCount + * @param string $message + */ + private function createBatchLog(string $status, int $successCount, int $errorCount, string $message): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + BatJobLog::create([ + 'device_id' => $deviceId, + 'process_name' => 'SHJ-4B-CHECK', + 'job_name' => 'SHJ-4Bチェックコマンド', + 'status' => $status, + 'status_comment' => $message . " (成功:{$successCount}/失敗:{$errorCount})", + 'created_at' => now()->startOfDay(), + 'updated_at' => now()->startOfDay() + ]); + + } catch (\Exception $e) { + Log::error('SHJ-4B-CHECK バッチログ作成エラー', [ + 'error' => $e->getMessage(), + 'status' => $status, + 'message' => $message + ]); + } + } +} diff --git a/app/Console/Commands/ShjFourCCommand.php b/app/Console/Commands/ShjFourCCommand.php new file mode 100644 index 0000000..4d84407 --- /dev/null +++ b/app/Console/Commands/ShjFourCCommand.php @@ -0,0 +1,157 @@ +shjFourCService = $shjFourCService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. パラメータ取得と検証 + * 2. ゾーン情報取得処理 + * 3. 割当判定処理 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-4C 室割当処理を開始します。'); + Log::info('SHJ-4C 室割当処理開始', [ + 'start_time' => $startTime, + 'park_id' => $this->argument('park_id'), + 'ptype_id' => $this->argument('ptype_id'), + 'psection_id' => $this->argument('psection_id') + ]); + + // 引数取得 + $parkId = $this->argument('park_id'); + $ptypeId = $this->argument('ptype_id'); + $psectionId = $this->argument('psection_id'); + + // パラメータ検証 + if (!$this->validateParameters($parkId, $ptypeId, $psectionId)) { + $this->error('パラメータが不正です。'); + return self::FAILURE; + } + + // SHJ-4C処理実行 + $result = $this->shjFourCService->executeRoomAllocation($parkId, $ptypeId, $psectionId); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-4C 室割当処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ-4C 室割当処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-4C 室割当処理でエラーが発生しました: ' . $result['message']); + Log::error('SHJ-4C 室割当処理エラー', [ + 'error' => $result['message'], + 'details' => $result['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-4C 室割当処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-4C 室割当処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * パラメータの妥当性を検証 + * + * @param mixed $parkId 駐輪場ID + * @param mixed $ptypeId 駐輪分類ID + * @param mixed $psectionId 車種区分ID + * @return bool 検証結果 + */ + private function validateParameters($parkId, $ptypeId, $psectionId): bool + { + // 必須パラメータチェック + if (empty($parkId) || empty($ptypeId) || empty($psectionId)) { + $this->error('全てのパラメータは必須です。'); + return false; + } + + // 数値形式チェック + if (!is_numeric($parkId) || !is_numeric($ptypeId) || !is_numeric($psectionId)) { + $this->error('全てのパラメータは数値である必要があります。'); + return false; + } + + // 正の整数チェック + if ($parkId <= 0 || $ptypeId <= 0 || $psectionId <= 0) { + $this->error('全てのパラメータは正の整数である必要があります。'); + return false; + } + + return true; + } +} diff --git a/app/Console/Commands/ShjMailSendCommand.php b/app/Console/Commands/ShjMailSendCommand.php new file mode 100644 index 0000000..d44e55b --- /dev/null +++ b/app/Console/Commands/ShjMailSendCommand.php @@ -0,0 +1,123 @@ +shjMailSendService = $shjMailSendService; + } + + /** + * コンソールコマンドを実行 + * + * SHJ-7 メール送信処理フロー: + * 【処理1】入力パラメーターをチェックする + * 【判断1】チェック結果 + * 【処理2】メール送信テンプレート情報を取得する + * 【判断2】取得結果 + * 【処理3】メールを送信する + * 【処理4】処理結果を返却する + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-7 メール送信処理を開始します。'); + Log::info('SHJ-7 メール送信処理開始', [ + 'start_time' => $startTime, + 'mail_address' => $this->argument('mail_address'), + 'backup_mail_address' => $this->argument('backup_mail_address'), + 'mail_template_id' => $this->argument('mail_template_id') + ]); + + // 引数取得 + $mailAddress = $this->argument('mail_address'); + $backupMailAddress = $this->argument('backup_mail_address'); + $mailTemplateId = $this->argument('mail_template_id'); + + // SHJ-7 メール送信処理実行 + $result = $this->shjMailSendService->executeMailSend($mailAddress, $backupMailAddress, $mailTemplateId); + + // 【処理4】処理結果を返却する + if ($result['result'] === 0) { + $endTime = now(); + $this->info('SHJ-7 メール送信処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ-7 メール送信処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result['result'], + 'error_info' => $result['error_info'] + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-7 メール送信処理でエラーが発生しました: ' . $result['error_info']); + Log::error('SHJ-7 メール送信処理エラー', [ + 'result' => $result['result'], + 'error_info' => $result['error_info'] + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-7 メール送信処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-7 メール送信処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + +} diff --git a/app/Console/Commands/ShjNineCommand.php b/app/Console/Commands/ShjNineCommand.php new file mode 100644 index 0000000..d95871c --- /dev/null +++ b/app/Console/Commands/ShjNineCommand.php @@ -0,0 +1,190 @@ +shjNineService = $shjNineService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. パラメータ取得と検証 + * 2. 集計対象日設定 + * 3. 売上集計処理実行 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-9 売上集計処理を開始します。'); + + // 引数取得 + $type = $this->argument('type'); + $targetDate = $this->argument('target_date'); + + Log::info('SHJ-9 売上集計処理開始', [ + 'start_time' => $startTime, + 'type' => $type, + 'target_date' => $targetDate + ]); + + // パラメータ検証 + if (!$this->validateParameters($type, $targetDate)) { + $this->error('パラメータが不正です。'); + return self::FAILURE; + } + + // 集計対象日設定 + $aggregationDate = $this->determineAggregationDate($type, $targetDate); + + $this->info("集計種別: {$type}"); + $this->info("集計対象日: {$aggregationDate}"); + + // SHJ-9処理実行 + $result = $this->shjNineService->executeEarningsAggregation($type, $aggregationDate); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-9 売上集計処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}"); + + Log::info('SHJ-9 売上集計処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-9 売上集計処理でエラーが発生しました: ' . $result['message']); + Log::error('SHJ-9 売上集計処理エラー', [ + 'error' => $result['message'], + 'details' => $result['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-9 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-9 売上集計処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * パラメータの妥当性を検証 + * + * @param string $type 集計種別 + * @param string|null $targetDate 対象日 + * @return bool 検証結果 + */ + private function validateParameters(string $type, ?string $targetDate): bool + { + // 集計種別チェック(SHJ-9 は日次のみ対応) + if ($type !== 'daily') { + $this->error('SHJ-9 は日次集計(daily)のみ対応しています。月次/年次は SHJ-10 を使用してください。'); + return false; + } + + // 対象日形式チェック(指定されている場合) + if ($targetDate && !$this->isValidDateFormat($targetDate)) { + $this->error('対象日の形式が正しくありません(YYYY-MM-DD形式で指定してください)。'); + return false; + } + + return true; + } + + /** + * 集計対象日を決定 + * + * @param string $type 集計種別(daily 固定) + * @param string|null $targetDate 指定日 + * @return string 集計対象日 + */ + private function determineAggregationDate(string $type, ?string $targetDate): string + { + if ($targetDate) { + return $targetDate; + } + + // パラメータ指定がない場合は昨日(本日の1日前) + return now()->subDay()->format('Y-m-d'); + } + + /** + * 日付形式の検証 + * + * @param string $date 日付文字列 + * @return bool 有効な日付形式かどうか + */ + private function isValidDateFormat(string $date): bool + { + // YYYY-MM-DD形式の正規表現チェック + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { + return false; + } + + // 実際の日付として有効かチェック + $dateParts = explode('-', $date); + return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); + } +} diff --git a/app/Console/Commands/ShjOneCommand.php b/app/Console/Commands/ShjOneCommand.php new file mode 100644 index 0000000..adaf0a1 --- /dev/null +++ b/app/Console/Commands/ShjOneCommand.php @@ -0,0 +1,124 @@ +shjOneService = $shjOneService; + } + + /** + * Execute the console command. + */ + public function handle() + { + $startTime = now(); + $processName = config('shj1.batch_log.process_name'); + + // 【処理0】パラメーターを取得する + $userId = $this->argument('user_id'); + $parkId = $this->argument('park_id'); + + $parameters = [ + 'user_id' => $userId, + 'park_id' => $parkId + ]; + + $this->info("=== SHJ-1 本人確認自動処理 開始 ==="); + $this->info("利用者ID: {$userId}"); + $this->info("駐輪場ID: {$parkId}"); + + try { + // SHJ-1 メイン処理を実行 + $result = $this->shjOneService->execute($userId, $parkId); + + // 処理結果の表示 + $this->displayResult($result); + + // バッチログはShjOneServiceが自動的にSHJ-8経由で作成 + + $this->info("=== SHJ-1 本人確認自動処理 完了 ==="); + + // システムエラーまたは身元確認失敗の場合は exit code 1 + $isSuccess = $result['system_success'] && ($result['identity_result'] ?? '') === 'OK'; + return $isSuccess ? 0 : 1; + + } catch (Exception $e) { + $this->error("SHJ-1処理中にエラーが発生しました: " . $e->getMessage()); + + // エラー時もShjOneServiceが自動的にバッチログを作成 + + Log::error('SHJ-1処理例外エラー', [ + 'user_id' => $userId, + 'park_id' => $parkId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return 1; + } + } + + /** + * 処理結果を表示 + */ + private function displayResult(array $result): void + { + $this->line(''); + $this->info('=== 処理結果 ==='); + + if ($result['system_success']) { + $this->info("✓ 処理実行成功: " . $result['message']); + if (isset($result['identity_result'])) { + $identityStatus = $result['identity_result'] === 'OK' ? '✓ 本人確認OK' : '✗ 本人確認NG'; + $this->line(" 本人確認結果: " . $identityStatus); + } + } else { + $this->error("✗ 処理実行失敗: " . $result['message']); + } + + if (isset($result['details'])) { + foreach ($result['details'] as $key => $value) { + $this->line(" {$key}: {$value}"); + } + } + + if (isset($result['stats'])) { + $this->line(''); + $this->info('=== 統計情報 ==='); + foreach ($result['stats'] as $key => $value) { + $this->line(" {$key}: {$value}"); + } + } + } + +} diff --git a/app/Console/Commands/ShjSixCommand.php b/app/Console/Commands/ShjSixCommand.php new file mode 100644 index 0000000..fc89db4 --- /dev/null +++ b/app/Console/Commands/ShjSixCommand.php @@ -0,0 +1,129 @@ +shjSixService = $shjSixService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. サーバ死活監視(DBアクセス) + * 2. デバイス管理マスタを取得する + * 3. デバイス毎のハードウェア状態を取得する + * 4. プリンタ制御プログラムログを取得する + * 5. バッチ処理ログを作成する + * ※ 異常検出時は共通A処理(メール通知)を実行 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-6 サーバ死活監視処理を開始します。'); + + Log::info('SHJ-6 サーバ死活監視処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-6監視処理実行 + $result = $this->shjSixService->executeServerMonitoring(); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-6 サーバ死活監視処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("監視結果: {$result['monitoring_summary']}"); + + // 警告がある場合は表示 + if (!empty($result['warnings'])) { + $this->warn('警告が検出されました:'); + foreach ($result['warnings'] as $warning) { + $this->warn("- {$warning}"); + } + } + + Log::info('SHJ-6 サーバ死活監視処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-6 サーバ死活監視処理でエラーが発生しました: ' . $result['message']); + + // エラー詳細があれば表示 + if (!empty($result['error_details'])) { + $this->error('エラー詳細:'); + foreach ($result['error_details'] as $detail) { + $this->error("- {$detail}"); + } + } + + Log::error('SHJ-6 サーバ死活監視処理エラー', [ + 'error' => $result['message'], + 'details' => $result['error_details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-6 サーバ死活監視処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-6 サーバ死活監視処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjTenCommand.php b/app/Console/Commands/ShjTenCommand.php new file mode 100644 index 0000000..f179fc5 --- /dev/null +++ b/app/Console/Commands/ShjTenCommand.php @@ -0,0 +1,292 @@ +shjTenService = $shjTenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. パラメータ取得と検証 + * 2. 財政年度期間設定 + * 3. 売上集計処理実行 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-10 売上集計処理を開始します。'); + + // 引数取得 + $type = $this->argument('type'); + $target = $this->argument('target'); + + // パラメータ指定なしの場合、デフォルト値を設定 + if (empty($target)) { + $target = $this->getDefaultTarget($type); + $this->info("パラメーター指定なし:デフォルト値 {$target} を使用します。"); + } + + Log::info('SHJ-10 売上集計処理開始', [ + 'start_time' => $startTime, + 'type' => $type, + 'target' => $target + ]); + + // パラメータ検証 + if (!$this->validateParameters($type, $target)) { + $this->error('パラメータが不正です。'); + return self::FAILURE; + } + + // 財政年度期間設定 + $fiscalPeriod = $this->determineFiscalPeriod($type, $target); + + $this->info("集計種別: {$type}"); + $this->info("集計対象: {$target}"); + $this->info("財政期間: {$fiscalPeriod['start_date']} ~ {$fiscalPeriod['end_date']}"); + + // SHJ-10処理実行 + $result = $this->shjTenService->executeFiscalEarningsAggregation($type, $target, $fiscalPeriod); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-10 売上集計処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}"); + + Log::info('SHJ-10 売上集計処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-10 売上集計処理でエラーが発生しました: ' . $result['message']); + Log::error('SHJ-10 売上集計処理エラー', [ + 'error' => $result['message'], + 'details' => $result['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-10 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-10 売上集計処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * パラメーター指定なしの場合のデフォルト値を取得 + * + * 仕様書: + * - 年次の場合:前期(財政年度4月開始) + * - 月次の場合:本日の1ヶ月前 + * + * @param string $type 集計種別 + * @return string デフォルトの集計対象 + */ + private function getDefaultTarget(string $type): string + { + $fiscalStartMonth = 4; // 財政年度開始月(4月) + + if ($type === 'yearly') { + // 前期を計算 + // 現在の財政年度を求め、その前の年度を返す + $now = now(); + $currentYear = (int)$now->format('Y'); + $currentMonth = (int)$now->format('m'); + + // 現在の財政年度を計算(4月以降なら今年、3月以前なら去年) + $currentFiscalYear = $currentMonth >= $fiscalStartMonth ? $currentYear : $currentYear - 1; + + // 前期 = 現在の財政年度 - 1 + $previousFiscalYear = $currentFiscalYear - 1; + + return (string)$previousFiscalYear; + + } elseif ($type === 'monthly') { + // 本日の1ヶ月前 + $lastMonth = now()->subMonth(); + return $lastMonth->format('Y/m'); + } + + return ''; + } + + /** + * パラメータの妥当性を検証 + * + * @param string $type 集計種別 + * @param string $target 集計対象 + * @return bool 検証結果 + */ + private function validateParameters(string $type, string $target): bool + { + // 集計種別チェック + $allowedTypes = ['yearly', 'monthly']; + if (!in_array($type, $allowedTypes)) { + $this->error('集計種別は yearly, monthly のいずれかを指定してください。'); + return false; + } + + // 集計対象形式チェック + if ($type === 'yearly') { + // 年度形式チェック (例: 2019) + if (!preg_match('/^\d{4}$/', $target)) { + $this->error('年次集計の場合、年度を4桁の数字で指定してください。(例: 2019)'); + return false; + } + } elseif ($type === 'monthly') { + // 年月形式チェック (例: 2019/01) + if (!preg_match('/^\d{4}\/\d{2}$/', $target)) { + $this->error('月次集計の場合、年月をYYYY/MM形式で指定してください。(例: 2019/01)'); + return false; + } + + // 月の範囲チェック (01-12) + $parts = explode('/', $target); + $month = (int)$parts[1]; + if ($month < 1 || $month > 12) { + $this->error('月は01から12までの範囲で指定してください。'); + return false; + } + } + + return true; + } + + /** + * 財政年度期間を決定 + * + * 財政年度は4月開始(Config設定可能) + * - yearly 2019: 2019年4月1日 ~ 2020年3月31日 + * - monthly 2019/01: 2019年1月1日 ~ 2019年1月31日 + * + * 仕様書: + * - yearly: 売上日付 = パラメーター.集計対象年(期) → 年度の1月1日で表現 + * - monthly: 売上日付 = パラメーター.集計対象月 → 該当月の1日で表現 + * + * @param string $type 集計種別 + * @param string $target 集計対象 + * @return array 財政期間情報 + */ + private function determineFiscalPeriod(string $type, string $target): array + { + $fiscalStartMonth = 4; // 財政年度開始月(4月) + + if ($type === 'yearly') { + $year = (int)$target; + + // 財政年度期間計算 + $startDate = sprintf('%04d-%02d-01', $year, $fiscalStartMonth); + $endDate = sprintf('%04d-%02d-%02d', $year + 1, $fiscalStartMonth - 1, + date('t', strtotime(sprintf('%04d-%02d-01', $year + 1, $fiscalStartMonth - 1)))); + + // 売上日付: 仕様書では年度(例:2019)、DBはDATE型なので年度の1月1日で表現 + $earningsDate = sprintf('%04d-01-01', $year); + + return [ + 'type' => 'yearly', + 'fiscal_year' => $year, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'earnings_date' => $earningsDate, + 'summary_type' => 1, // 年次 + 'target_label' => "{$year}年度" + ]; + + } elseif ($type === 'monthly') { + $parts = explode('/', $target); + $year = (int)$parts[0]; + $month = (int)$parts[1]; + + // 指定月の期間計算 + $startDate = sprintf('%04d-%02d-01', $year, $month); + $endDate = sprintf('%04d-%02d-%02d', $year, $month, + date('t', strtotime($startDate))); + + // 該当する財政年度を計算 + $fiscalYear = $month >= $fiscalStartMonth ? $year : $year - 1; + + // 売上日付: 仕様書では年月(例:2019/01)、DBはDATE型なので該当月の1日で表現 + $earningsDate = sprintf('%04d-%02d-01', $year, $month); + + return [ + 'type' => 'monthly', + 'fiscal_year' => $fiscalYear, + 'target_year' => $year, + 'target_month' => $month, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'earnings_date' => $earningsDate, + 'summary_type' => 2, // 月次 + 'target_label' => "{$year}年{$month}月" + ]; + } + + throw new \InvalidArgumentException("不正な集計種別: {$type}"); + } +} diff --git a/app/Console/Commands/ShjThirteenCommand.php b/app/Console/Commands/ShjThirteenCommand.php new file mode 100644 index 0000000..dc0eb95 --- /dev/null +++ b/app/Console/Commands/ShjThirteenCommand.php @@ -0,0 +1,154 @@ +shjThirteenService = $shjThirteenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 引数取得・検証 + * 2. ShjThirteenService実行 + * 3. 結果表示 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-13 契約台数追加処理を開始します。'); + + // 引数取得 + $contractData = [ + 'park_id' => (int) $this->argument('park_id'), + 'psection_id' => (int) $this->argument('psection_id'), + 'ptype_id' => (int) $this->argument('ptype_id'), + 'zone_id' => (int) $this->argument('zone_id'), + 'contract_id' => $this->option('contract_id'), + ]; + + Log::info('SHJ-13 契約台数追加処理開始', [ + 'start_time' => $startTime, + 'contract_data' => $contractData, + ]); + + $this->info("駐輪場ID: {$contractData['park_id']}"); + $this->info("車種区分ID: {$contractData['psection_id']}"); + $this->info("駐輪分類ID: {$contractData['ptype_id']}"); + $this->info("ゾーンID: {$contractData['zone_id']}"); + if ($contractData['contract_id']) { + $this->info("契約ID: {$contractData['contract_id']}"); + } + + // SHJ-13処理実行 + $this->info('【処理実行】契約台数追加処理を実行しています...'); + $result = $this->shjThirteenService->execute($contractData); + + // 処理結果確認・表示 + if ($result['result'] === 0) { + $endTime = now(); + $this->info('SHJ-13 契約台数追加処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ-13 契約台数追加処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'contract_data' => $contractData, + ]); + + // 成功時の結果出力 + $this->line('処理結果: 0'); // 0 = 正常終了 + $this->line('異常情報: '); // 正常時は空文字 + + return self::SUCCESS; + } else { + $this->error('SHJ-13 契約台数追加処理でエラーが発生しました。'); + $this->error("エラーコード: {$result['error_code']}"); + $this->error("エラーメッセージ: {$result['error_message']}"); + + Log::error('SHJ-13 契約台数追加処理エラー', [ + 'contract_data' => $contractData, + 'error_result' => $result, + ]); + + // エラー時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: ' . $result['error_message']); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-13 契約台数追加処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-13 契約台数追加処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + // 例外時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: システムエラー: ' . $e->getMessage()); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjThreeCommand.php b/app/Console/Commands/ShjThreeCommand.php new file mode 100644 index 0000000..1af4430 --- /dev/null +++ b/app/Console/Commands/ShjThreeCommand.php @@ -0,0 +1,108 @@ +shjThreeService = $shjThreeService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 駐輪場マスタ情報取得 + * 2. 各駐輪場の実行タイミングチェック + * 3. 定期更新対象者取得とリマインダー送信 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-3 定期更新リマインダー処理を開始します。'); + + Log::info('SHJ-3 定期更新リマインダー処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-3メイン処理実行 + $result = $this->shjThreeService->executeReminderProcess(); + + $endTime = now(); + $this->info('SHJ-3 定期更新リマインダー処理が完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + // 処理結果表示 + $this->line('=== 処理結果 ==='); + $this->line("対象駐輪場数: {$result['processed_parks_count']}"); + $this->line("対象者総数: {$result['total_target_users']}"); + $this->line("メール送信成功: {$result['mail_success_count']}件"); + $this->line("メール送信失敗: {$result['mail_error_count']}件"); + $this->line("オペレーターキュー追加: {$result['operator_queue_count']}件"); + + Log::info('SHJ-3 定期更新リマインダー処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return $result['success'] ? self::SUCCESS : self::FAILURE; + + } catch (\Exception $e) { + $this->error('SHJ-3 定期更新リマインダー処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + Log::error('SHJ-3 定期更新リマインダー処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjTwelveCommand.php b/app/Console/Commands/ShjTwelveCommand.php new file mode 100644 index 0000000..7290206 --- /dev/null +++ b/app/Console/Commands/ShjTwelveCommand.php @@ -0,0 +1,217 @@ +shjTwelveService = $shjTwelveService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 定期契約マスタより未払い者を取得する + * 2. 取得件数判定 + * 3. 未払い者への通知、またはオペレーターキュー追加処理 + * 4. バッチ処理ログを作成する + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-12 未払い者通知処理を開始します。'); + Log::info('SHJ-12 未払い者通知処理開始', [ + 'start_time' => $startTime + ]); + + // 【処理1】定期契約マスタより未払い者を取得する + $this->info('【処理1】定期契約マスタより未払い者を取得しています...'); + $unpaidUsers = $this->shjTwelveService->getUnpaidUsers(); + + // 【判断1】取得件数判定 + $unpaidCount = count($unpaidUsers); + $this->info("取得件数: {$unpaidCount}件"); + + if ($unpaidCount === 0) { + // 未払い者なしの結果を設定する + $this->info('未払い者なしのため処理を終了します。'); + + // バッチ処理ログを作成(SHJ-8仕様に合わせ job_name と status_comment を追加) + $this->shjTwelveService->createBatchLog( + 'success', + [ + 'job_name' => 'SHJ-12未払い者アラート', + 'status_comment' => '未払い者なし' + ], + '未払い者なし', + 0, + 0, + 0 + ); + + Log::info('SHJ-12 未払い者通知処理完了(対象なし)', [ + 'end_time' => now(), + 'duration_seconds' => $startTime->diffInSeconds(now()) + ]); + + return self::SUCCESS; + } + + // 【処理2】未払い者への通知、またはオペレーターキュー追加処理 + $this->info('【処理2】未払い者への通知処理を実行しています...'); + $processResult = $this->shjTwelveService->processUnpaidUserNotifications($unpaidUsers); + + // 処理結果確認 + if ($processResult['success']) { + $endTime = now(); + $this->info('SHJ-12 未払い者通知処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("処理対象件数: {$unpaidCount}件"); + $this->info("メール送信成功: {$processResult['mail_success_count']}件"); + $this->info("メール送信失敗: {$processResult['mail_error_count']}件"); + $this->info("キュー登録成功: {$processResult['queue_success_count']}件"); + $this->info("キュー登録失敗: {$processResult['queue_error_count']}件"); + + // 【処理3】バッチ処理ログを作成する + $statusComment = $processResult['status_comment']; + $batchComments = $processResult['batch_comments']; + $totalSuccessCount = $processResult['mail_success_count'] + $processResult['queue_success_count']; + $totalErrorCount = $processResult['mail_error_count'] + $processResult['queue_error_count']; + + // status_commentに詳細エラー情報を付加 + $finalMessage = $statusComment; + if (!empty($batchComments)) { + $finalMessage .= "\n" . $batchComments; + } + + // SHJ-8仕様に合わせ、job_name と status_comment を parameters に追加 + $batchLogParameters = array_merge($processResult['parameters'], [ + 'job_name' => 'SHJ-12未払い者アラート', + 'status_comment' => $statusComment + ]); + + $this->shjTwelveService->createBatchLog( + 'success', + $batchLogParameters, + $finalMessage, + $unpaidCount, + $totalSuccessCount, + $totalErrorCount + ); + + Log::info('SHJ-12 未払い者通知処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'processed_count' => $unpaidCount, + 'mail_success_count' => $processResult['mail_success_count'], + 'mail_error_count' => $processResult['mail_error_count'], + 'queue_success_count' => $processResult['queue_success_count'], + 'queue_error_count' => $processResult['queue_error_count'] + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-12 未払い者通知処理でエラーが発生しました: ' . $processResult['message']); + + // エラー時のバッチログ作成 + $statusComment = $processResult['status_comment'] ?? 'システムエラー'; + $batchComments = $processResult['batch_comments'] ?? ''; + $finalMessage = $statusComment; + if (!empty($batchComments)) { + $finalMessage .= "\n" . $batchComments; + } + + // SHJ-8仕様に合わせ、job_name と status_comment を parameters に追加 + $batchLogParameters = array_merge($processResult['parameters'] ?? [], [ + 'job_name' => 'SHJ-12未払い者アラート', + 'status_comment' => $statusComment + ]); + + $this->shjTwelveService->createBatchLog( + 'error', + $batchLogParameters, + $finalMessage, + $unpaidCount, + ($processResult['mail_success_count'] ?? 0) + ($processResult['queue_success_count'] ?? 0), + ($processResult['mail_error_count'] ?? 0) + ($processResult['queue_error_count'] ?? 0) + ); + + Log::error('SHJ-12 未払い者通知処理エラー', [ + 'error' => $processResult['message'], + 'details' => $processResult['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-12 未払い者通知処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + // 例外時のバッチログ作成(SHJ-8仕様に合わせ job_name と status_comment を追加) + $this->shjTwelveService->createBatchLog( + 'error', + [ + 'job_name' => 'SHJ-12未払い者アラート', + 'status_comment' => 'システムエラー' + ], + 'システムエラー: ' . $e->getMessage(), + 0, + 0, + 1 + ); + + Log::error('SHJ-12 未払い者通知処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/TestMailCommand.php b/app/Console/Commands/TestMailCommand.php new file mode 100644 index 0000000..784e257 --- /dev/null +++ b/app/Console/Commands/TestMailCommand.php @@ -0,0 +1,163 @@ +mailSendService = $mailSendService; + } + + /** + * コマンド実行 + * + * @return int + */ + public function handle() + { + // パラメータ取得 + $email = $this->argument('email') ?? 'wyf_0506@hotmail.com'; + $backupEmail = $this->option('backup') ?? ''; + $templateId = (int) $this->option('template'); + $useLaravel = $this->option('laravel'); + + // 確認画面 + $this->info('=== SHJ-7 メール送信テスト ==='); + $this->line(''); + $this->line("送信先メールアドレス: {$email}"); + $this->line("予備メールアドレス : " . ($backupEmail ?: '(なし)')); + $this->line("テンプレートID : {$templateId}"); + $this->line("送信方式 : " . ($useLaravel ? 'Laravel Mail' : 'mb_send_mail')); + $this->line(''); + + // 環境チェック + $mailDriver = config('mail.default'); + $this->warn("現在のメール設定: MAIL_MAILER={$mailDriver}"); + + if ($useLaravel) { + $this->info("✓ Laravel Mail を使用します(.envの設定に従います)"); + if ($mailDriver === 'log') { + $this->warn('⚠ メールはログファイルにのみ記録され、実際には送信されません'); + $this->warn('⚠ 実際に送信するには .env で MAIL_MAILER=smtp 等に変更してください'); + } else { + $this->info("✓ メールは実際に送信されます({$mailDriver})"); + } + } else { + $this->warn('⚠ mb_send_mail() を使用します(php.iniのSMTP設定が必要)'); + $this->comment(' Laravel Mail を使用する場合は --laravel オプションを追加してください'); + } + + $this->line(''); + + // 実行確認 + if (!$this->confirm('メール送信を実行しますか?', true)) { + $this->info('キャンセルしました。'); + return self::SUCCESS; + } + + // メール送信実行 + $this->line(''); + $this->info('メール送信を開始します...'); + + try { + $startTime = now(); + + // SHJ-7サービス呼び出し(Laravel Mail または mb_send_mail) + if ($useLaravel) { + $result = $this->mailSendService->executeMailSendWithLaravel( + $email, + $backupEmail, + $templateId + ); + } else { + $result = $this->mailSendService->executeMailSend( + $email, + $backupEmail, + $templateId + ); + } + + $endTime = now(); + $duration = $startTime->diffInSeconds($endTime); + + $this->line(''); + $this->info('=== 実行結果 ==='); + + if ($result['result'] === 0) { + $this->info('✓ メール送信成功'); + $this->line("処理時間: {$duration}秒"); + + if ($mailDriver === 'log') { + $this->line(''); + $this->comment('※ ログファイルを確認してください:'); + $this->comment(' storage/logs/laravel.log'); + } else { + $this->line(''); + $this->comment("※ {$email} のメールボックスを確認してください"); + } + + return self::SUCCESS; + } else { + $this->error('✗ メール送信失敗'); + $this->error("エラー情報: {$result['error_info']}"); + $this->line(''); + $this->comment('ログファイルで詳細を確認してください: storage/logs/laravel.log'); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('✗ 予期しないエラーが発生しました'); + $this->error("エラー: {$e->getMessage()}"); + + Log::error('test:mail コマンドエラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Exceptions/ApiException.php b/app/Exceptions/ApiException.php new file mode 100644 index 0000000..053ca5c --- /dev/null +++ b/app/Exceptions/ApiException.php @@ -0,0 +1,62 @@ +errorCode = $errorCode; + $this->httpStatus = $httpStatus; + } + + /** + * エラーコード取得 + */ + public function getErrorCode(): string + { + return $this->errorCode; + } + + /** + * HTTPステータス取得 + */ + public function getHttpStatus(): int + { + return $this->httpStatus; + } + + /** + * JSONレスポンスを生成 + */ + public function render(): JsonResponse + { + return response()->json([ + 'error' => [ + 'code' => $this->errorCode, + 'message' => $this->getMessage() + ] + ], $this->httpStatus); + } +} diff --git a/app/Http/Controllers/Api/UserInformationHistoryController.php b/app/Http/Controllers/Api/UserInformationHistoryController.php new file mode 100644 index 0000000..7d4c9f6 --- /dev/null +++ b/app/Http/Controllers/Api/UserInformationHistoryController.php @@ -0,0 +1,113 @@ +service = $service; + } + + /** + * API 7 - 一覧取得 + * GET /api/user-information-history + * + * @param UserInformationHistoryIndexRequest $request + * @return JsonResponse + */ + public function index(UserInformationHistoryIndexRequest $request): JsonResponse + { + try { + $result = $this->service->getList($request->validated()); + + return response()->json([ + 'result' => 'OK', + 'data' => $result['data'], + 'pagination' => $result['pagination'], + ], 200); + } catch (ApiException $e) { + return $e->render(); + } + } + + /** + * API 7 - 単一取得 + * GET /api/user-information-history/{id} + * + * @param int $id + * @return JsonResponse + */ + public function show(int $id): JsonResponse + { + try { + $data = $this->service->getById($id); + + return response()->json([ + 'result' => 'OK', + 'data' => $data, + ], 200); + } catch (ApiException $e) { + return $e->render(); + } + } + + /** + * API 8 - 新規追加 + * POST /api/user-information-history + * + * @param UserInformationHistoryStoreRequest $request + * @return JsonResponse + */ + public function store(UserInformationHistoryStoreRequest $request): JsonResponse + { + try { + $data = $this->service->create($request->validated()); + + return response()->json(array_merge( + ['result' => 'OK'], + $data + ), 201); + } catch (ApiException $e) { + return $e->render(); + } + } + + /** + * API 9 - 更新 + * PUT /api/user-information-history/{id} + * + * @param UserInformationHistoryUpdateRequest $request + * @param int $id + * @return JsonResponse + */ + public function update(UserInformationHistoryUpdateRequest $request, int $id): JsonResponse + { + try { + $data = $this->service->update($id, $request->validated()); + + return response()->json(array_merge( + ['result' => 'OK'], + $data + ), 200); + } catch (ApiException $e) { + return $e->render(); + } + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +header('X-API-Key'); + + // APIキー未指定 + if (empty($apiKey)) { + return response()->json([ + 'error' => [ + 'code' => 'E01', + 'message' => '認証エラー: APIキーが指定されていません。' + ] + ], 401); + } + + // 有効なAPIキーリストを取得 + $validApiKeys = config('api.valid_keys', []); + + // APIキー検証 + if (!in_array($apiKey, $validApiKeys, true)) { + return response()->json([ + 'error' => [ + 'code' => 'E01', + 'message' => '認証エラー: APIキーが無効です。' + ] + ], 401); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Api/UserInformationHistoryIndexRequest.php b/app/Http/Requests/Api/UserInformationHistoryIndexRequest.php new file mode 100644 index 0000000..cb195a6 --- /dev/null +++ b/app/Http/Requests/Api/UserInformationHistoryIndexRequest.php @@ -0,0 +1,73 @@ + 'nullable|integer|min:1', + 'entry_date_from' => 'nullable|date_format:Y-m-d', + 'entry_date_to' => 'nullable|date_format:Y-m-d|after_or_equal:entry_date_from', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:100', + ]; + } + + /** + * バリデーションエラーメッセージ + */ + public function messages(): array + { + return [ + 'user_id.integer' => 'E03: user_id の値が不正です。整数で指定してください。', + 'user_id.min' => 'E03: user_id の値が不正です。1以上の整数で指定してください。', + 'entry_date_from.date_format' => 'E04: entry_date_from の日付形式が不正です。YYYY-MM-DD形式で指定してください。', + 'entry_date_to.date_format' => 'E04: entry_date_to の日付形式が不正です。YYYY-MM-DD形式で指定してください。', + 'entry_date_to.after_or_equal' => 'E04: entry_date_to は entry_date_from 以降の日付を指定してください。', + 'page.integer' => 'E03: page の値が不正です。整数で指定してください。', + 'page.min' => 'E03: page の値が不正です。1以上の整数で指定してください。', + 'per_page.integer' => 'E03: per_page の値が不正です。整数で指定してください。', + 'per_page.min' => 'E03: per_page の値が不正です。1以上の整数で指定してください。', + 'per_page.max' => 'E03: per_page の値が不正です。最大値は100です。', + ]; + } + + /** + * バリデーション失敗時の処理 + */ + protected function failedValidation(Validator $validator) + { + $errors = $validator->errors()->first(); + + // エラーコード抽出 + preg_match('/^(E\d{2}):/', $errors, $matches); + $errorCode = $matches[1] ?? 'E99'; + $message = preg_replace('/^E\d{2}: /', '', $errors); + + throw new HttpResponseException( + response()->json([ + 'error' => [ + 'code' => $errorCode, + 'message' => $message + ] + ], 400) + ); + } +} diff --git a/app/Http/Requests/Api/UserInformationHistoryStoreRequest.php b/app/Http/Requests/Api/UserInformationHistoryStoreRequest.php new file mode 100644 index 0000000..fdf6a6e --- /dev/null +++ b/app/Http/Requests/Api/UserInformationHistoryStoreRequest.php @@ -0,0 +1,84 @@ + 'required|integer', + 'entry_date' => 'required|date_format:Y-m-d', + 'user_information_history' => 'required|string|max:255', + ]; + } + + /** + * バリデーションエラーメッセージ + */ + public function messages(): array + { + return [ + 'user_id.required' => 'E02: user_id が未設定です。', + 'user_id.integer' => 'E02: user_id は整数で指定してください。', + 'entry_date.required' => 'E04: entry_date が未設定です。', + 'entry_date.date_format' => 'E05: entry_date の形式が不正です。YYYY-MM-DD形式で指定してください。', + 'user_information_history.required' => 'E06: user_information_history が未設定です。', + 'user_information_history.string' => 'E06: user_information_history は文字列で指定してください。', + 'user_information_history.max' => 'E07: user_information_history が255文字を超えています。', + ]; + } + + /** + * 追加バリデーション + */ + public function withValidator(Validator $validator) + { + $validator->after(function ($validator) { + // user_idのエラーがない場合のみ存在チェック + if (!$validator->errors()->has('user_id') && $this->user_id) { + if (!User::where('user_id', $this->user_id)->exists()) { + $validator->errors()->add('user_id', 'E03: 指定された user_id が userテーブルに存在しません。'); + } + } + }); + } + + /** + * バリデーション失敗時の処理 + */ + protected function failedValidation(Validator $validator) + { + $errors = $validator->errors()->first(); + + // エラーコード抽出 + preg_match('/^(E\d{2}):/', $errors, $matches); + $errorCode = $matches[1] ?? 'E99'; + $message = preg_replace('/^E\d{2}: /', '', $errors); + + throw new HttpResponseException( + response()->json([ + 'error' => [ + 'code' => $errorCode, + 'message' => $message + ] + ], 400) + ); + } +} diff --git a/app/Http/Requests/Api/UserInformationHistoryUpdateRequest.php b/app/Http/Requests/Api/UserInformationHistoryUpdateRequest.php new file mode 100644 index 0000000..45db8a8 --- /dev/null +++ b/app/Http/Requests/Api/UserInformationHistoryUpdateRequest.php @@ -0,0 +1,93 @@ + 'sometimes|integer', + 'entry_date' => 'sometimes|date_format:Y-m-d', + 'user_information_history' => 'sometimes|string|max:255', + ]; + } + + /** + * バリデーションエラーメッセージ + */ + public function messages(): array + { + return [ + 'user_id.integer' => 'E03: user_id は整数で指定してください。', + 'entry_date.date_format' => 'E04: entry_date の形式が不正です。YYYY-MM-DD形式で指定してください。', + 'user_information_history.string' => 'E05: user_information_history は文字列で指定してください。', + 'user_information_history.max' => 'E05: user_information_history が255文字を超えています。', + ]; + } + + /** + * 追加バリデーション + */ + public function withValidator(Validator $validator) + { + $validator->after(function ($validator) { + // 少なくとも1つの更新項目が必要 + $updateFields = array_filter( + $this->only(['user_id', 'entry_date', 'user_information_history']), + function ($value) { + return $value !== null; + } + ); + + if (empty($updateFields)) { + $validator->errors()->add('_general', 'E06: 更新項目が1つも指定されていません。'); + } + + // user_idの存在チェック + if (!$validator->errors()->has('user_id') && $this->has('user_id') && $this->user_id !== null) { + if (!User::where('user_id', $this->user_id)->exists()) { + $validator->errors()->add('user_id', 'E03: 指定された user_id が userテーブルに存在しません。'); + } + } + }); + } + + /** + * バリデーション失敗時の処理 + */ + protected function failedValidation(Validator $validator) + { + $errors = $validator->errors()->first(); + + // エラーコード抽出 + preg_match('/^(E\d{2}):/', $errors, $matches); + $errorCode = $matches[1] ?? 'E99'; + $message = preg_replace('/^E\d{2}: /', '', $errors); + + throw new HttpResponseException( + response()->json([ + 'error' => [ + 'code' => $errorCode, + 'message' => $message + ] + ], 400) + ); + } +} diff --git a/app/Jobs/ProcessSettlementJob.php b/app/Jobs/ProcessSettlementJob.php new file mode 100644 index 0000000..1b02d52 --- /dev/null +++ b/app/Jobs/ProcessSettlementJob.php @@ -0,0 +1,142 @@ +settlementTransactionId = $settlementTransactionId; + $this->context = $context; + } + + /** + * ジョブを実行 + * + * SHJ-4Bサービスを使用して決済トランザクション処理を実行 + * + * @return void + */ + public function handle() + { + $startTime = now(); + + try { + Log::info('SHJ-4B ProcessSettlementJob開始', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'context' => $this->context, + 'start_time' => $startTime, + ]); + + // SHJ-4Bサービスを使用して決済トランザクション処理を実行 + // バッチログはShjFourBServiceが自動的にSHJ-8経由で作成 + $shjFourBService = app(ShjFourBService::class); + $result = $shjFourBService->processSettlementTransaction( + $this->settlementTransactionId, + $this->context + ); + + // 処理結果のログ記録 + if ($result['success']) { + Log::info('SHJ-4B ProcessSettlementJob完了', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'execution_time' => now()->diffInSeconds($startTime), + 'result' => $result, + ]); + } else { + // ビジネスロジック上の問題(エラーではない) + Log::warning('SHJ-4B ProcessSettlementJob要手動対応', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'result' => $result, + ]); + } + + } catch (\Throwable $e) { + Log::error('SHJ-4B ProcessSettlementJob失敗', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + // エラー時もShjFourBServiceが自動的にバッチログを作成 + // ジョブを失敗させて再試行を促す + throw $e; + } + } + + /** + * ジョブが失敗した場合の処理 + * + * @param \Throwable $exception + * @return void + */ + public function failed(\Throwable $exception) + { + Log::error('SHJ-4B ProcessSettlementJob最終失敗', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'context' => $this->context, + 'error' => $exception->getMessage(), + 'attempts' => $this->attempts(), + ]); + + // バッチログはShjFourBServiceが既に作成済み + // 最終失敗時の追加処理があればここに記述 + // 例:管理者への通知、障害キューへの登録など + } +} diff --git a/app/Mail/ReservationCancelledMail.php b/app/Mail/ReservationCancelledMail.php new file mode 100644 index 0000000..79409d7 --- /dev/null +++ b/app/Mail/ReservationCancelledMail.php @@ -0,0 +1,31 @@ +user_name = $user_name; + $this->reserve = $reserve; + } + + public function build() + { + return $this->subject('So-Manager:空き待ちのキャンセルが完了しました') + ->view('emails.reservation_cancelled') + ->with([ + 'user_name' => $this->user_name, + 'reserve' => $this->reserve, + ]); + } +} diff --git a/app/Mail/UserEditVerifyMail.php b/app/Mail/UserEditVerifyMail.php new file mode 100644 index 0000000..8eac99f --- /dev/null +++ b/app/Mail/UserEditVerifyMail.php @@ -0,0 +1,28 @@ +verifyUrl = $verifyUrl; + $this->user = $user; + } + + public function build() + { + return $this + ->subject('【So-Manager】ユーザー情報変更のご確認') + ->view('emails.user_edit_verify'); + } +} diff --git a/app/Mail/WithdrawCompleteMail.php b/app/Mail/WithdrawCompleteMail.php new file mode 100644 index 0000000..411807b --- /dev/null +++ b/app/Mail/WithdrawCompleteMail.php @@ -0,0 +1,34 @@ +user_name = $user_name; + $this->user_primemail = $user_primemail; + $this->user_quitday = $user_quitday; + } + + public function build() + { + return $this->subject('退会完了のお知らせ') + ->view('emails.withdraw_complete') + ->with([ + 'user_name' => $this->user_name, + 'user_primemail' => $this->user_primemail, + 'user_quitday' => $this->user_quitday, + ]); + } +} diff --git a/app/Models/BatJobLog.php b/app/Models/BatJobLog.php new file mode 100644 index 0000000..68dd471 --- /dev/null +++ b/app/Models/BatJobLog.php @@ -0,0 +1,70 @@ + 'integer', + 'device_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * タイムスタンプを使用 + * + * @var bool + */ + public $timestamps = true; + + /** + * deviceとのリレーション + */ + public function device() + { + return $this->belongsTo(Device::class, 'device_id', 'device_id'); + } +} diff --git a/app/Models/City.php b/app/Models/City.php new file mode 100644 index 0000000..ccfbd51 --- /dev/null +++ b/app/Models/City.php @@ -0,0 +1,36 @@ +when($operatorId, fn ($q) => $q->where('operator_id', $operatorId)) + ->orderBy('city_name') + ->pluck('city_name', 'city_id') + ->toArray(); + } + + +} diff --git a/app/Models/Device.php b/app/Models/Device.php new file mode 100644 index 0000000..0bc9983 --- /dev/null +++ b/app/Models/Device.php @@ -0,0 +1,44 @@ + 'integer', + 'device_workstart' => 'date', + 'device_replace' => 'date', + 'operator_id' => 'integer', + ]; + + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + + public static function getList(): array + { + return static::orderBy('device_subject')->pluck('device_subject', 'device_id')->toArray(); + } +} diff --git a/app/Models/EarningsSummary.php b/app/Models/EarningsSummary.php new file mode 100644 index 0000000..5e0c636 --- /dev/null +++ b/app/Models/EarningsSummary.php @@ -0,0 +1,298 @@ + 'integer', + 'park_id' => 'integer', + 'psection_id' => 'integer', + 'enable_months' => 'integer', + 'regular_new_count' => 'integer', + 'regular_new_amount' => 'decimal:2', + 'regular_new_reduction_count' => 'integer', + 'regular_new_reduction_amount' => 'decimal:2', + 'regular_update_count' => 'integer', + 'regular_update_amount' => 'decimal:2', + 'regular_update_reduction_count' => 'integer', + 'regular_update_reduction_amount' => 'decimal:2', + 'lumpsum_count' => 'integer', + 'lumpsum' => 'decimal:2', + 'refunds' => 'decimal:2', + 'other_income' => 'decimal:2', + 'other_spending' => 'decimal:2', + 'reissue_count' => 'integer', + 'reissue_amount' => 'decimal:2', + 'operator_id' => 'integer', + 'summary_start_date' => 'date', + 'summary_end_date' => 'date', + 'earnings_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 日付属性 + * + * @var array + */ + protected $dates = [ + 'summary_start_date', + 'summary_end_date', + 'earnings_date', + 'created_at', + 'updated_at' + ]; + + /** + * 集計タイプの定数 + */ + const TYPE_DAILY = '日次'; + const TYPE_MONTHLY = '月次'; + const TYPE_YEARLY = '年次'; + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * 車種区分との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function psection() + { + return $this->belongsTo(Psection::class, 'psection_id', 'psection_id'); + } + + /** + * 指定期間の売上集計データを取得 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @param string|null $summaryType 集計タイプ + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getEarningsByPeriod(int $parkId, string $startDate, string $endDate, ?string $summaryType = null) + { + $query = self::where('park_id', $parkId) + ->whereBetween('earnings_date', [$startDate, $endDate]); + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + return $query->with(['park', 'psection']) + ->orderBy('earnings_date') + ->orderBy('psection_id') + ->get(); + } + + /** + * 駐輪場別の売上合計を取得 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @return array 売上合計データ + */ + public static function getEarningsTotalByPark(int $parkId, string $startDate, string $endDate): array + { + $result = self::where('park_id', $parkId) + ->whereBetween('earnings_date', [$startDate, $endDate]) + ->selectRaw(' + SUM(regular_new_count) as total_new_count, + SUM(regular_new_amount) as total_new_amount, + SUM(regular_update_count) as total_update_count, + SUM(regular_update_amount) as total_update_amount, + SUM(lumpsum_count) as total_lumpsum_count, + SUM(lumpsum) as total_lumpsum, + SUM(refunds) as total_refunds, + SUM(reissue_count) as total_reissue_count, + SUM(reissue_amount) as total_reissue_amount + ') + ->first(); + + return $result ? $result->toArray() : []; + } + + /** + * 最新の集計日を取得 + * + * @param int|null $parkId 駐輪場ID(省略時は全体) + * @param string|null $summaryType 集計タイプ + * @return string|null 最新集計日 + */ + public static function getLatestEarningsDate(?int $parkId = null, ?string $summaryType = null): ?string + { + $query = self::query(); + + if ($parkId) { + $query->where('park_id', $parkId); + } + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + $latest = $query->max('earnings_date'); + + return $latest ? Carbon::parse($latest)->format('Y-m-d') : null; + } + + /** + * 期間の売上データを削除 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @param string|null $summaryType 集計タイプ + * @return int 削除件数 + */ + public static function deleteEarningsByPeriod(int $parkId, string $startDate, string $endDate, ?string $summaryType = null): int + { + $query = self::where('park_id', $parkId) + ->where('summary_start_date', $startDate) + ->where('summary_end_date', $endDate); + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + return $query->delete(); + } + + /** + * 売上集計データの作成 + * + * @param array $data 売上データ + * @return EarningsSummary 作成されたモデル + */ + public static function createEarningsSummary(array $data): EarningsSummary + { + $defaultData = [ + 'regular_new_count' => 0, + 'regular_new_amount' => 0.00, + 'regular_new_reduction_count' => 0, + 'regular_new_reduction_amount' => 0.00, + 'regular_update_count' => 0, + 'regular_update_amount' => 0.00, + 'regular_update_reduction_count' => 0, + 'regular_update_reduction_amount' => 0.00, + 'lumpsum_count' => 0, + 'lumpsum' => 0.00, + 'refunds' => 0.00, + 'other_income' => 0.00, + 'other_spending' => 0.00, + 'reissue_count' => 0, + 'reissue_amount' => 0.00, + 'operator_id' => 0 + ]; + + $mergedData = array_merge($defaultData, $data); + + return self::create($mergedData); + } + + /** + * 売上合計の計算 + * + * @return float 売上合計 + */ + public function getTotalEarningsAttribute(): float + { + return $this->regular_new_amount + + $this->regular_update_amount + + $this->lumpsum + + $this->reissue_amount + + $this->other_income - + $this->other_spending - + $this->refunds; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'EarningsSummary[ID:%d, Park:%d, Type:%s, Date:%s]', + $this->earnings_summary_id, + $this->park_id, + $this->summary_type, + $this->earnings_date ? $this->earnings_date->format('Y-m-d') : 'N/A' + ); + } +} diff --git a/app/Models/HardwareCheckLog.php b/app/Models/HardwareCheckLog.php new file mode 100644 index 0000000..63d1d31 --- /dev/null +++ b/app/Models/HardwareCheckLog.php @@ -0,0 +1,229 @@ + 'integer', + 'device_id' => 'integer', + 'status' => 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * ステータスの定数 + */ + const STATUS_NORMAL = 1; // 正常 + const STATUS_WARNING = 2; // 警告 + const STATUS_ERROR = 3; // エラー + const STATUS_UNKNOWN = 0; // 不明 + + /** + * デバイスとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function device() + { + return $this->belongsTo(Device::class, 'device_id', 'device_id'); + } + + /** + * 指定デバイスの最新ハードウェア状態を取得 + * + * @param int $deviceId デバイスID + * @return HardwareCheckLog|null 最新ログ + */ + public static function getLatestStatusByDevice(int $deviceId): ?HardwareCheckLog + { + return self::where('device_id', $deviceId) + ->orderBy('created_at', 'desc') + ->first(); + } + + /** + * 全デバイスの最新ハードウェア状態を取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLatestStatusForAllDevices() + { + return self::select('device_id') + ->selectRaw('MAX(created_at) as latest_created_at') + ->groupBy('device_id') + ->with(['device']) + ->get() + ->map(function ($log) { + return self::where('device_id', $log->device_id) + ->where('created_at', $log->latest_created_at) + ->with(['device']) + ->first(); + }) + ->filter(); + } + + /** + * 異常状態のデバイスを取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getAbnormalDevices() + { + $latestLogs = self::getLatestStatusForAllDevices(); + + return $latestLogs->filter(function ($log) { + return $log->status !== self::STATUS_NORMAL; + }); + } + + /** + * 指定期間内のログを取得 + * + * @param int $deviceId デバイスID + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLogsByPeriod(int $deviceId, string $startTime, string $endTime) + { + return self::where('device_id', $deviceId) + ->whereBetween('created_at', [$startTime, $endTime]) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * ハードウェア状態ログを作成 + * + * @param int $deviceId デバイスID + * @param int $status ステータス + * @param string $statusComment ステータスコメント + * @param int|null $operatorId オペレータID + * @return HardwareCheckLog 作成されたログ + */ + public static function createLog( + int $deviceId, + int $status, + string $statusComment = '', + ?int $operatorId = null + ): HardwareCheckLog { + return self::create([ + 'device_id' => $deviceId, + 'status' => $status, + 'status_comment' => $statusComment, + 'operator_id' => $operatorId ?? 0 + ]); + } + + /** + * ステータス名を取得 + * + * @param int $status ステータス + * @return string ステータス名 + */ + public static function getStatusName(int $status): string + { + switch ($status) { + case self::STATUS_NORMAL: + return '正常'; + case self::STATUS_WARNING: + return '警告'; + case self::STATUS_ERROR: + return 'エラー'; + case self::STATUS_UNKNOWN: + return '不明'; + default: + return "ステータス{$status}"; + } + } + + /** + * 現在のステータス名を取得 + * + * @return string ステータス名 + */ + public function getStatusNameAttribute(): string + { + return self::getStatusName($this->status); + } + + /** + * 正常状態かどうかを判定 + * + * @return bool 正常状態かどうか + */ + public function isNormal(): bool + { + return $this->status === self::STATUS_NORMAL; + } + + /** + * 異常状態かどうかを判定 + * + * @return bool 異常状態かどうか + */ + public function isAbnormal(): bool + { + return $this->status !== self::STATUS_NORMAL; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'HardwareCheckLog[ID:%d, Device:%d, Status:%s, Time:%s]', + $this->hardware_check_log_id, + $this->device_id, + $this->getStatusNameAttribute(), + $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' + ); + } +} diff --git a/app/Models/JurisdictionParking.php b/app/Models/JurisdictionParking.php new file mode 100644 index 0000000..0d67a37 --- /dev/null +++ b/app/Models/JurisdictionParking.php @@ -0,0 +1,97 @@ + 'integer', + 'ope_id' => 'integer', + 'park_id' => 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * オペレータとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function ope() + { + return $this->belongsTo(Ope::class, 'ope_id', 'ope_id'); + } + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * 指定駐輪場を管轄するオペレータIDリストを取得 + * + * @param int $parkId 駐輪場ID + * @return array オペレータIDの配列 + */ + public static function getOperatorIdsByPark(int $parkId): array + { + return self::where('park_id', $parkId) + ->pluck('ope_id') + ->toArray(); + } +} + diff --git a/app/Models/MailTemplate.php b/app/Models/MailTemplate.php new file mode 100644 index 0000000..12d348d --- /dev/null +++ b/app/Models/MailTemplate.php @@ -0,0 +1,95 @@ + 'boolean', + 'use_flag' => 'boolean', + ]; + + /** + * 使用プログラムIDでメールテンプレート情報を取得 + * + * 仕様書に基づく検索条件: + * - 使用プログラムID = 入力パラメーター + * - 使用フラグ = 1 + * + * @param int $pgId 使用プログラムID + * @return MailTemplate|null メールテンプレート情報 + */ + public static function getByProgramId(int $pgId): ?MailTemplate + { + return self::where('pg_id', $pgId) + ->where('use_flag', 1) + ->first(); + } + + /** + * エリアマネージャー同報が有効かチェック + * + * @return bool エリアマネージャー同報フラグ + */ + public function isManagerCcEnabled(): bool + { + return (bool) $this->mgr_cc_flag; + } + + /** + * BCCアドレスを取得 + * + * @return string|null BCCアドレス + */ + public function getBccAddress(): ?string + { + return $this->bcc_adrs; + } + + /** + * メール件名を取得 + * + * @return string|null メール件名 + */ + public function getSubject(): ?string + { + return $this->subject; + } + + /** + * メール本文を取得 + * + * @return string|null メール本文 + */ + public function getText(): ?string + { + return $this->text; + } +} \ No newline at end of file diff --git a/app/Models/Manager.php b/app/Models/Manager.php new file mode 100644 index 0000000..1253ac8 --- /dev/null +++ b/app/Models/Manager.php @@ -0,0 +1,228 @@ + 'integer', + 'manager_parkid' => 'integer', + 'manager_device1' => 'integer', + 'manager_device2' => 'integer', + 'manager_alert1' => 'boolean', + 'manager_alert2' => 'boolean', + 'manager_quit_flag' => 'boolean', + 'operator_id' => 'integer', + 'manager_quitday' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 所属駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'manager_parkid', 'park_id'); + } + + /** + * 登録オペレータとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function operator() + { + return $this->belongsTo(Ope::class, 'operator_id', 'ope_id'); + } + + /** + * アクティブな管理者のみを取得するスコープ + * (退職フラグ = 0) + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('manager_quit_flag', 0); + } + + /** + * メールアドレスが設定されている管理者のみを取得するスコープ + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeHasEmail($query) + { + return $query->whereNotNull('manager_mail') + ->where('manager_mail', '!=', ''); + } + + /** + * 指定駐輪場の管理者を取得するスコープ + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $parkId 駐輪場ID + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeByPark($query, int $parkId) + { + return $query->where('manager_parkid', $parkId); + } + + /** + * メール送信対象の駐輪場管理者を取得 + * + * 条件: + * - 退職フラグ = 0(在職中) + * - メールアドレスが設定されている + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getMailTargetManagers() + { + return self::active() + ->hasEmail() + ->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) + ->get() + ->map(function ($manager) { + return [ + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid + ]; + }); + } + + /** + * 指定駐輪場のメール送信対象管理者を取得 + * + * @param int $parkId 駐輪場ID + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getMailTargetManagersByPark(int $parkId) + { + return self::active() + ->hasEmail() + ->byPark($parkId) + ->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) + ->get() + ->map(function ($manager) { + return [ + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid + ]; + }); + } + + /** + * 退職しているかどうかを判定 + * + * @return bool 退職しているかどうか + */ + public function isQuit(): bool + { + return (bool) $this->manager_quit_flag; + } + + /** + * アクティブ(在職中)かどうかを判定 + * + * @return bool 在職中かどうか + */ + public function isActive(): bool + { + return !$this->isQuit(); + } + + /** + * メールアドレスが設定されているかどうかを判定 + * + * @return bool メールアドレスが設定されているかどうか + */ + public function hasEmail(): bool + { + return !empty($this->manager_mail); + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'Manager[ID:%d, Name:%s, Park:%d, Email:%s]', + $this->manager_id, + $this->manager_name, + $this->manager_parkid, + $this->manager_mail ?? 'N/A' + ); + } +} + diff --git a/app/Models/Ope.php b/app/Models/Ope.php new file mode 100644 index 0000000..a94327a --- /dev/null +++ b/app/Models/Ope.php @@ -0,0 +1,177 @@ +ope_pass; + } + + /** + * passwordフィールドアクセサー(読み取り用) + * Laravel 12対応:Laravel認証システムがpasswordフィールドを読み取る際にope_passの値を返す + * Laravel 5.7との差異:旧システムでは不要だった仮想フィールド対応 + * + * @return string|null + */ + public function getPasswordAttribute() + { + // データベースのope_passフィールドの値を返す + return $this->attributes['ope_pass'] ?? null; + } + + /** + * passwordフィールドミューテータ(書き込み用) + * Laravel 12対応:Laravel認証システムがpasswordフィールドを更新する際にope_passフィールドに保存 + * Laravel 5.7との差異:旧システムでは不要だった仮想フィールド対応 + * + * @param string $value パスワード値 + * @return void + */ + public function setPasswordAttribute($value) + { + // passwordフィールドへの書き込みをope_passフィールドにリダイレクト + $this->attributes['ope_pass'] = $value; + } + + /** + * 属性設定のオーバーライド + * Remember Tokenの設定を無効化(旧システムでは使用していない) + * + * @param string $key + * @param mixed $value + */ + public function setAttribute($key, $value) + { + $isRememberTokenAttribute = $key == $this->getRememberTokenName(); + if (!$isRememberTokenAttribute) { + parent::setAttribute($key, $value); + } + } + + /** + * オペレータ検索機能 + * Laravel 5.7から継承したメソッド(検索・ソート・ページネーション) + * + * @param array $inputs + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection + */ + public static function search($inputs) + { + $list = self::query(); + + // POST検索条件の処理 + if ($inputs['isMethodPost']) { + // 検索条件があればここに追加 + } + + // ソート処理 + if ($inputs['sort']) { + $list->orderBy($inputs['sort'], $inputs['sort_type']); + } + + // エクスポート用かページネーション用かの判定 + if ($inputs['isExport']) { + $list = $list->get(); + } else { + $list = $list->paginate(\App\Utils::item_per_page); + } + + return $list; + } + + /** + * プライマリキーでオペレータを取得 + * + * @param mixed $pk + * @return \App\Models\Ope|null + */ + public static function getByPk($pk) + { + return self::find($pk); + } + + /** + * プライマリキーでオペレータを削除 + * + * @param array $arr + * @return int + */ + public static function deleteByPk($arr) + { + return self::whereIn('ope_id', $arr)->delete(); + } + + /** + * オペレータのリストを取得(セレクトボックス用) + * Laravel 5.7から継承したメソッド + * + * @return \Illuminate\Support\Collection + */ + public static function getList() + { + return self::pluck('ope_name', 'ope_id'); + } +} \ No newline at end of file diff --git a/app/Models/OperatorQue.php b/app/Models/OperatorQue.php new file mode 100644 index 0000000..289c831 --- /dev/null +++ b/app/Models/OperatorQue.php @@ -0,0 +1,87 @@ + '本人確認(社会人)', + 2 => '本人確認(学生)', + 3 => 'タグ発送', + 4 => '予約告知通知', + 5 => '定期更新通知', + 6 => '返金処理', + 7 => '再発行リミット超過', + 8 => '支払い催促', + 9 => 'シール発行催促', + 10 => 'シール再発行', + 11 => '名寄せフリガナ照合エラー', + 12 => '本人確認(減免更新)', + 13 => '本人確認(学生更新)', + 101 => 'サーバーエラー', + 102 => 'プリンタエラー', + 103 => 'スキャナーエラー', + 104 => 'プリンタ用紙残少警告', + ]; + + /** キューステータス */ + public const QueStatus = [ + 1 => 'キュー発生', + 2 => 'キュー作業中', + 3 => 'キュー作業済', + 4 => '返金済', + ]; + + public function getQueClassLabel(): string + { + return self::QueClass[$this->que_class] ?? 'キュー種別未設定'; + } + + public function getQueStatusLabel(): string + { + return self::QueStatus[$this->que_status] ?? (string)$this->que_status; + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function park() + { + return $this->belongsTo(Park::class, 'park_id'); + } + + public function contract() + { + return $this->belongsTo(Contract::class, 'contract_id'); + } + + public function operator() + { + return $this->belongsTo(User::class, 'operator_id'); + } + + public function getUser() { return $this->user; } + public function getPark() { return $this->park; } +} diff --git a/app/Models/Park.php b/app/Models/Park.php new file mode 100644 index 0000000..80060ef --- /dev/null +++ b/app/Models/Park.php @@ -0,0 +1,143 @@ +orderBy($inputs['sort'], $inputs['sort_type']); + } + if ($inputs['isExport']){ + $list = $list->get(); + }else{ + $list = $list->paginate(Utils::item_per_page); + } + return $list; + } + + public static function getByPk($pk) + { + return self::find($pk); + } + + public static function deleteByPk($arr) + { + return self::whereIn('park_id', $arr)->delete(); + } + + public static function boot() + { + parent::boot(); + self::creating(function (Park $model) { + $model->operator_id = Auth::user()->ope_id ?? null; + }); + } + + /** + * GET 閉設フラグ + */ + public function getParkCloseFlagDisplay() { + if($this->park_close_flag == 1) { + return '閉設'; + } + else if($this->park_close_flag == 0) { + return '開設'; + } + return ''; + } + + public function getCity() + { + // city_id => city_id (City モデル要有 city_id PK) + return $this->belongsTo(City::class, 'city_id', 'city_id')->first(); + } + + public static function getList(){ + return self::pluck('park_name','park_id'); + } + + public static function getIdByName($park_name){ + return self::where('park_name',$park_name)->pluck('park_id')->first(); + } + + /** + * 料金設定との関連付け + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function prices() + { + return $this->hasMany(PriceA::class, 'park_id', 'park_id'); + } + +} diff --git a/app/Models/PriceA.php b/app/Models/PriceA.php new file mode 100644 index 0000000..162d4e9 --- /dev/null +++ b/app/Models/PriceA.php @@ -0,0 +1,62 @@ +belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * 車種分類 + */ + public function ptype() + { + return $this->belongsTo(Ptype::class, 'price_ptypeid', 'ptype_id'); + } + + /** + * 定期契約 + */ + public function contracts() + { + return $this->hasMany(RegularContract::class, 'price_parkplaceid', 'price_parkplaceid'); + } +} + + + + diff --git a/app/Models/PrintJobLog.php b/app/Models/PrintJobLog.php new file mode 100644 index 0000000..4097168 --- /dev/null +++ b/app/Models/PrintJobLog.php @@ -0,0 +1,251 @@ + 'integer', + 'park_id' => 'integer', + 'user_id' => 'integer', + 'contract_id' => 'integer', + 'error_code' => 'integer', + 'created_at' => 'datetime' + ]; + + /** + * エラーコードの定数 + */ + const ERROR_CODE_THRESHOLD = 100; // エラー判定閾値(>=100がエラー) + + /** + * updateされない設定(created_atのみ) + * + * @var bool + */ + public $timestamps = false; + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * ユーザーとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'user_id'); + } + + /** + * 契約との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function contract() + { + return $this->belongsTo(RegularContract::class, 'contract_id', 'contract_id'); + } + + /** + * 過去15分間のエラーログを取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getRecentErrorLogs() + { + $fifteenMinutesAgo = Carbon::now()->subMinutes(15); + + return self::where('created_at', '>=', $fifteenMinutesAgo) + ->where('error_code', '>=', self::ERROR_CODE_THRESHOLD) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 指定期間内のエラーログを取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getErrorLogsByPeriod(string $startTime, string $endTime) + { + return self::whereBetween('created_at', [$startTime, $endTime]) + ->where('error_code', '>=', self::ERROR_CODE_THRESHOLD) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 指定期間内の全ログを取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLogsByPeriod(string $startTime, string $endTime) + { + return self::whereBetween('created_at', [$startTime, $endTime]) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * プリンタジョブログを作成 + * + * @param array $data ログデータ + * @return PrintJobLog 作成されたログ + */ + public static function createLog(array $data): PrintJobLog + { + $defaultData = [ + 'park_id' => null, + 'user_id' => null, + 'contract_id' => null, + 'process_name' => '', + 'job_name' => '', + 'status' => '', + 'error_code' => 0, + 'status_comment' => '', + 'created_at' => now() + ]; + + $mergedData = array_merge($defaultData, $data); + + return self::create($mergedData); + } + + /** + * エラーログかどうかを判定 + * + * @return bool エラーログかどうか + */ + public function isError(): bool + { + return $this->error_code >= self::ERROR_CODE_THRESHOLD; + } + + /** + * 正常ログかどうかを判定 + * + * @return bool 正常ログかどうか + */ + public function isNormal(): bool + { + return $this->error_code < self::ERROR_CODE_THRESHOLD; + } + + /** + * エラーレベルを取得 + * + * @return string エラーレベル + */ + public function getErrorLevel(): string + { + if ($this->error_code >= 200) { + return '重大エラー'; + } elseif ($this->error_code >= self::ERROR_CODE_THRESHOLD) { + return 'エラー'; + } else { + return '正常'; + } + } + + /** + * エラーログの統計情報を取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return array 統計情報 + */ + public static function getErrorStatistics(string $startTime, string $endTime): array + { + $errorLogs = self::getErrorLogsByPeriod($startTime, $endTime); + $totalLogs = self::getLogsByPeriod($startTime, $endTime); + + $errorByCode = $errorLogs->groupBy('error_code')->map->count(); + $errorByProcess = $errorLogs->groupBy('process_name')->map->count(); + + return [ + 'total_logs' => $totalLogs->count(), + 'error_logs' => $errorLogs->count(), + 'error_rate' => $totalLogs->count() > 0 ? + round(($errorLogs->count() / $totalLogs->count()) * 100, 2) : 0, + 'error_by_code' => $errorByCode->toArray(), + 'error_by_process' => $errorByProcess->toArray(), + 'period' => [ + 'start' => $startTime, + 'end' => $endTime + ] + ]; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'PrintJobLog[ID:%d, Process:%s, ErrorCode:%d, Time:%s]', + $this->job_log_id, + $this->process_name, + $this->error_code, + $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' + ); + } +} diff --git a/app/Models/Psection.php b/app/Models/Psection.php new file mode 100644 index 0000000..88ac97d --- /dev/null +++ b/app/Models/Psection.php @@ -0,0 +1,144 @@ + 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + + // 主キーが自動増分でない場合はfalseに設定 + public $incrementing = false; + // タイムスタンプ管理しない場合はfalseに設定 + public $timestamps = false; + + + + /** + * 売上集計との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function earningsSummaries() + { + return $this->hasMany(EarningsSummary::class, 'psection_id', 'psection_id'); + } + + /** + * 定期契約との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function regularContracts() + { + return $this->hasMany(RegularContract::class, 'psection_id', 'psection_id'); + } + + /** + * アクティブな車種区分一覧を取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getActivePsections() + { + return self::orderBy('psection_id')->get(); + } + + /** + * 車種区分名で検索 + * + * @param string $subject 車種区分名 + * @return Psection|null + */ + public static function findBySubject(string $subject): ?Psection + { + return self::where('psection_subject', $subject)->first(); + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'Psection[ID:%d, Subject:%s]', + $this->psection_id, + $this->psection_subject + ); + } + + + + + + + + + + // 新規作成時にoperator_idを自動設定(operator_idカラムがある場合のみ) + public static function boot() + { + parent::boot(); + self::creating(function (Psection $model) { + // ログインしている場合のみセット + if (\Auth::check()) { + $model->operator_id = Auth::user()->ope_id; + } + }); + } + + // 車種区分リストを取得(プルダウン用) + public static function getList() + { + return self::orderBy('psection_id')->pluck('psection_subject', 'psection_id'); + } +} diff --git a/app/Models/Ptype.php b/app/Models/Ptype.php new file mode 100644 index 0000000..aeb6fe6 --- /dev/null +++ b/app/Models/Ptype.php @@ -0,0 +1,68 @@ +delete(); + } + + /** + * 主キーで1件取得 + */ + public static function getByPk($pk) + { + return self::find($pk); + } + protected $table = 'ptype'; + protected $primaryKey = 'ptype_id'; + public $timestamps = true; + + public const CREATED_AT = 'created_at'; + public const UPDATED_AT = 'updated_at'; + + protected $fillable = [ + 'ptype_subject', + 'ptype_remarks', + 'operator_id', + ]; + + public static function search($inputs) + { + $list = self::query(); + if (!empty($inputs['isMethodPost'])) { + // 必要に応じて検索条件を追加 + } + if (!empty($inputs['sort'])) { + $list->orderBy($inputs['sort'], $inputs['sort_type'] ?? 'asc'); + } + if (!empty($inputs['isExport'])) { + return $list->get(); + } else { + return $list->paginate(\App\Models\Utils::item_per_page); + } + } + + public static function getList() + { + return self::pluck('ptype_subject', 'ptype_id'); + } + + + public function prices() + { + return $this->hasMany(PriceA::class, 'price_ptypeid', 'ptype_id'); + } +} + + + + diff --git a/app/Models/RegularContract.php b/app/Models/RegularContract.php new file mode 100644 index 0000000..1c3f96c --- /dev/null +++ b/app/Models/RegularContract.php @@ -0,0 +1,108 @@ +orderBy($inputs['sort'], $inputs['sort_type']); + } + if ($inputs['isExport']){ + $list = $list->get(); + }else{ + $list = $list->paginate(\App\Utils::item_per_page); // Utilsクラスの定数を使用 + } + return $list; + } + + public static function getByPk($pk) + { + return self::find($pk); + } + + public static function deleteByPk($arr) + { + return self::whereIn('contract_id', $arr)->delete(); + } + + //TODO 定期契約ID not found in database specs + + //TODO 解約/契約不可フラグ not found in database specs + public function userName() + { + return $this->belongsTo(\App\Models\User::class,'user_id','user_seq')->first(); + } + public function getUserType() + { + return $this->belongsTo(\App\Models\Usertype::class,'user_categoryid','user_categoryid')->first(); + } + public function getPark() + { + return $this->belongsTo(\App\Models\Park::class,'park_id','park_id')->first(); + } + public function getPrice() + { + return $this->belongsTo(\App\Models\Price::class,'price_parkplaceid','price_parkplaceid')->first(); + } +// public function getSettlement() +// { +// return $this->belongsTo(SettlementTransaction::class,'settlement_transaction_id','settlement_transaction_id')->first(); +// } + + public function getOpe() + { + return $this->belongsTo(\App\Models\Ope::class,'ope_id','ope_id')->first(); + } +} \ No newline at end of file diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..e4054e4 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,85 @@ + 'integer', + 'printable_alert_flag' => 'boolean', + 'printable_number' => 'integer', + 'printable_alert_number' => 'integer', + 'printer_keep_alive' => 'integer', + 'operator_id' => 'integer', + 'auto_change_date' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 設定情報を取得(通常はID=1の単一レコード) + * + * @return Setting|null + */ + public static function getSettings(): ?Setting + { + return self::first(); + } +} + diff --git a/app/Models/SettlementTransaction.php b/app/Models/SettlementTransaction.php new file mode 100644 index 0000000..d615b18 --- /dev/null +++ b/app/Models/SettlementTransaction.php @@ -0,0 +1,27 @@ + */ + use HasFactory, Notifiable; + + protected $table = 'user'; + protected $primaryKey = 'user_seq'; + public $timestamps = true; + + // 本人確認チェックフラグの定数 + const USER_ID_CARD_CHK_FLG = [ + 0 => '未確認', + 1 => '確認済み' + ]; + + // 身分証明書種別の定数 + const USER_IDCARD = [ + '免許証' => '免許証', + '健康保険証' => '健康保険証', + 'パスポート' => 'パスポート', + '学生証' => '学生証', + 'その他' => 'その他' + ]; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'user_id', + 'member_id', + 'user_pass', + 'user_manual_regist_flag', + 'user_mailing_flag', + 'contract_number', + 'user_tag_serial', + 'user_tag_serial_64', + 'qr_code', + 'tag_qr_flag', + 'user_aid', + 'user_park_number', + 'user_place_qrid', + 'user_categoryid', + 'user_name', + 'user_phonetic', + 'user_gender', + 'user_birthdate', + 'user_age', + 'ward_residents', + 'user_mobile', + 'user_homephone', + 'user_primemail', + 'user_submail', + 'user_regident_zip', + 'user_regident_pre', + 'user_regident_city', + 'user_regident_add', + 'user_relate_zip', + 'user_relate_pre', + 'user_relate_city', + 'user_relate_add', + 'user_workplace', + 'user_school', + 'user_graduate', + 'user_reduction', + 'user_idcard', + 'user_idcard_chk_flag', + 'user_chk_day', + 'user_chk_opeid', + 'user_tag_issue', + 'issue_permission', + 'user_quit_flag', + 'user_quitday', + 'user_remarks', + 'photo_filename1', + 'photo_filename2', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'user_pass', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'user_birthdate' => 'date', + 'user_chk_day' => 'datetime', + 'user_quitday' => 'date', + 'user_manual_regist_flag' => 'boolean', + 'user_mailing_flag' => 'boolean', + 'tag_qr_flag' => 'boolean', + 'user_idcard_chk_flag' => 'integer', // 修正: boolean -> integer + 'issue_permission' => 'boolean', + 'user_quit_flag' => 'boolean', + ]; + } + + /** + * ユーザー検索 + */ + public static function search($inputs) + { + $query = self::query(); + + // 検索条件の適用 + if (!empty($inputs['user_id'])) { + $query->where('user_id', 'like', '%' . $inputs['user_id'] . '%'); + } + if (!empty($inputs['member_id'])) { + $query->where('member_id', 'like', '%' . $inputs['member_id'] . '%'); + } + if (!empty($inputs['user_tag_serial'])) { + $query->where('user_tag_serial', 'like', '%' . $inputs['user_tag_serial'] . '%'); + } + if (!empty($inputs['user_phonetic'])) { + $query->where('user_phonetic', 'like', '%' . $inputs['user_phonetic'] . '%'); + } + if (!empty($inputs['phone'])) { + $query->where(function($q) use ($inputs) { + $q->where('user_mobile', 'like', '%' . $inputs['phone'] . '%') + ->orWhere('user_homephone', 'like', '%' . $inputs['phone'] . '%'); + }); + } + if (isset($inputs['black_list']) && $inputs['black_list'] !== '') { + $query->where('user_quit_flag', $inputs['black_list']); + } + if (isset($inputs['ward_residents']) && $inputs['ward_residents'] !== '') { + $query->where('ward_residents', $inputs['ward_residents']); + } + if (!empty($inputs['user_tag_serial_64'])) { + $query->where('user_tag_serial_64', 'like', '%' . $inputs['user_tag_serial_64'] . '%'); + } + + // ソート + if (!empty($inputs['sort'])) { + $sortType = !empty($inputs['sort_type']) ? $inputs['sort_type'] : 'asc'; + $query->orderBy($inputs['sort'], $sortType); + } else { + $query->orderBy('user_seq', 'desc'); + } + + // エクスポート用の場合はページネーションしない + if (!empty($inputs['isExport'])) { + return $query->get(); + } + + // ページネーション(Utilsクラスの定数を使用) + return $query->paginate(\App\Utils::item_per_page); + } + + /** + * シーケンスでユーザーを取得 + */ + public static function getUserBySeq($seq) + { + return self::where('user_seq', $seq)->first(); + } + + /** + * シーケンス配列でユーザーを削除 + */ + public static function deleteUsersBySeq($seqArray) + { + try { + return self::whereIn('user_seq', $seqArray)->delete(); + } catch (\Exception $e) { + return false; + } + } + + /** + * ユーザータイプとのリレーション + */ + public function getUserType() + { + return $this->belongsTo(Usertype::class, 'user_categoryid', 'id'); + } + + public static function getList() + { + return self::pluck('user_name', 'user_seq'); + } + + public static function getUserPhone() + { + return self::select('user_seq', 'user_name', 'user_mobile', 'user_homephone')->get(); + } +} diff --git a/app/Models/UserInformationHistory.php b/app/Models/UserInformationHistory.php new file mode 100644 index 0000000..7edfb54 --- /dev/null +++ b/app/Models/UserInformationHistory.php @@ -0,0 +1,66 @@ + 'integer', + 'user_id' => 'integer', + 'entry_date' => 'date:Y-m-d', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * ユーザーとの関連 + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'user_id'); + } + + /** + * APIレスポンス用の配列形式に変換 + */ + public function toApiArray(): array + { + return [ + 'user_information_history_id' => $this->user_information_history_id, + 'user_id' => $this->user_id, + 'entry_date' => $this->entry_date?->format('Y-m-d'), + 'user_information_history' => $this->user_information_history, + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +} diff --git a/app/Models/Usertype.php b/app/Models/Usertype.php new file mode 100644 index 0000000..ac73bd2 --- /dev/null +++ b/app/Models/Usertype.php @@ -0,0 +1,66 @@ +operator_id = Auth::user()->ope_id; + }); + } + + public static function search($inputs) + { + $list = self::query(); + if ($inputs['isMethodPost']) { + + } + // Sort + if ($inputs['sort']) { + $list->orderBy($inputs['sort'], $inputs['sort_type']); + } + if ($inputs['isExport']){ + $list = $list->get(); + }else{ + $list = $list->paginate(Utils::item_per_page); + } + return $list; + } + + public static function getByPk($pk) + { + return self::find($pk); + } + + public static function deleteByPk($arr) + { + return self::whereIn('user_categoryid', $arr)->delete(); + } + + //TODO 利用者分類ID not found in database specs + + //TODO 利用者分類名 not found in database specs + public static function getList(){ + return self::pluck('print_name','user_categoryid'); + } + +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +app->singleton(GoogleVisionService::class); + $this->app->singleton(GoogleMapsService::class); + $this->app->singleton(ShjOneService::class); + + // Commands + $this->commands([ + ShjOneCommand::class, + ]); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + // + } +} diff --git a/app/Services/GoogleMapsService.php b/app/Services/GoogleMapsService.php new file mode 100644 index 0000000..9376c00 --- /dev/null +++ b/app/Services/GoogleMapsService.php @@ -0,0 +1,185 @@ +apiKey = config('shj1.apis.google_maps.api_key'); + $this->baseUrl = config('shj1.apis.google_maps.base_url'); + $this->config = config('shj1.distance.google_maps_config'); + } + + /** + * 2点間の距離を計算(Google Maps Distance Matrix API使用) + * + * @param string $origin 出発地(住所) + * @param string $destination 目的地(住所) + * @return array ['distance_meters' => int, 'distance_text' => string, 'status' => string] + * @throws Exception + */ + public function calculateDistance(string $origin, string $destination): array + { + try { + $url = "{$this->baseUrl}/distancematrix/json"; + + $params = [ + 'origins' => $origin, + 'destinations' => $destination, + 'units' => $this->config['units'] ?? 'metric', + 'mode' => $this->config['mode'] ?? 'walking', + 'language' => $this->config['language'] ?? 'ja', + 'region' => $this->config['region'] ?? 'jp', + 'key' => $this->apiKey, + ]; + + // タイムアウト短縮(従来30秒→15秒) + $response = Http::timeout(15)->get($url, $params); + + if (!$response->successful()) { + throw new Exception("Google Maps API request failed: " . $response->status()); + } + + $data = $response->json(); + + // APIエラーチェック + if ($data['status'] !== 'OK') { + throw new Exception("Google Maps API error: " . $data['status']); + } + + $element = $data['rows'][0]['elements'][0]; + + if ($element['status'] !== 'OK') { + throw new Exception("Distance calculation failed: " . $element['status']); + } + + return [ + 'distance_meters' => $element['distance']['value'], + 'distance_text' => $element['distance']['text'], + 'duration_text' => $element['duration']['text'] ?? null, + 'status' => 'success' + ]; + + } catch (Exception $e) { + Log::error('Google Maps distance calculation error', [ + 'origin' => $origin, + 'destination' => $destination, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 住所から座標を取得(Geocoding API使用) + * + * @param string $address 住所 + * @return array ['lat' => float, 'lng' => float] + * @throws Exception + */ + public function geocodeAddress(string $address): array + { + try { + $url = "{$this->baseUrl}/geocode/json"; + + $params = [ + 'address' => $address, + 'language' => $this->config['language'] ?? 'ja', + 'region' => $this->config['region'] ?? 'jp', + 'key' => $this->apiKey, + ]; + + $response = Http::timeout(30)->get($url, $params); + + if (!$response->successful()) { + throw new Exception("Google Geocoding API request failed: " . $response->status()); + } + + $data = $response->json(); + + if ($data['status'] !== 'OK') { + throw new Exception("Google Geocoding API error: " . $data['status']); + } + + $location = $data['results'][0]['geometry']['location']; + + return [ + 'lat' => $location['lat'], + 'lng' => $location['lng'] + ]; + + } catch (Exception $e) { + Log::error('Google Maps geocoding error', [ + 'address' => $address, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 座標から直線距離を計算(Haversine formula) + * Google Maps APIが利用できない場合のフォールバック + * + * @param float $lat1 緯度1 + * @param float $lon1 経度1 + * @param float $lat2 緯度2 + * @param float $lon2 経度2 + * @return int 距離(メートル) + */ + public function calculateStraightLineDistance(float $lat1, float $lon1, float $lat2, float $lon2): int + { + $earthRadius = 6371000; // 地球の半径(メートル) + + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + + $a = sin($dLat / 2) * sin($dLat / 2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLon / 2) * sin($dLon / 2); + + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + $distance = $earthRadius * $c; + + return round($distance); + } + + /** + * 距離詳細文字列を生成(SHJ-1の要求仕様に準拠) + * + * @param int $parkId 駐輪場ID + * @param int $distanceMeters 距離(メートル) + * @param string $method 計算方法 + * @return string + */ + public function generateDistanceDetailString(int $parkId, int $distanceMeters, string $method = 'google_maps'): string + { + return $parkId . "/" . $parkId . "/二点間距離:" . $distanceMeters . "m (" . $method . ")"; + } + + /** + * APIキーが設定されているかチェック + * + * @return bool + */ + public function isApiKeyConfigured(): bool + { + $dummyKey = env('GOOGLE_MAPS_API_KEY') === 'dummy_google_maps_api_key_replace_with_real_one'; + return !empty($this->apiKey) && !$dummyKey; + } +} diff --git a/app/Services/GoogleVisionService.php b/app/Services/GoogleVisionService.php new file mode 100644 index 0000000..4a15b41 --- /dev/null +++ b/app/Services/GoogleVisionService.php @@ -0,0 +1,332 @@ +apiKey = config('shj1.apis.google_vision.api_key'); + $this->projectId = config('shj1.apis.google_vision.project_id'); + } + + /** + * 画像からテキストを抽出(OCR処理) + * + * @param string $imageFilename 画像ファイル名 + * @return string 抽出されたテキスト + * @throws Exception + */ + public function extractTextFromImage(string $imageFilename): string + { + try { + Log::info('GoogleVision OCR処理開始', [ + 'filename' => $imageFilename + ]); + + // 画像ファイルを取得 + $imageData = $this->getImageData($imageFilename); + + // Vision API リクエスト実行 + $detectedText = $this->callVisionApi($imageData); + + Log::info('GoogleVision OCR処理完了', [ + 'filename' => $imageFilename, + 'text_length' => strlen($detectedText), + 'text_preview' => substr($detectedText, 0, 100) . '...', + 'text_full' => $detectedText, // ← 完整のOCR結果を追加 + 'debug_marker' => 'NEW_CODE_EXECUTED_' . time() // ← デバッグマーカー追加 + ]); + + return $detectedText; + + } catch (Exception $e) { + Log::error('GoogleVision OCR処理エラー', [ + 'filename' => $imageFilename, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + /** + * 画像データを取得(Laravel Storage使用) + */ + private function getImageData(string $filename): string + { + $disk = config('shj1.storage.photo_disk'); + + Log::info('GoogleVision 画像ファイル読取開始', [ + 'filename' => $filename, + 'disk' => $disk + ]); + + if (!Storage::disk($disk)->exists($filename)) { + Log::error('GoogleVision 画像ファイルが見つかりません', [ + 'filename' => $filename, + 'disk' => $disk + ]); + throw new Exception("画像ファイルが見つかりません: {$filename}"); + } + + $imageContent = Storage::disk($disk)->get($filename); + + if (empty($imageContent)) { + Log::error('GoogleVision 画像ファイルが空です', [ + 'filename' => $filename + ]); + throw new Exception("画像ファイルが空です: {$filename}"); + } + + Log::info('GoogleVision 画像ファイル読取成功', [ + 'filename' => $filename, + 'size' => strlen($imageContent) + ]); + + return base64_encode($imageContent); + } + + /** + * Google Cloud Vision API呼び出し + */ + private function callVisionApi(string $base64Image): string + { + if (!$this->isApiKeyConfigured()) { + throw new Exception("Google Vision API キーが設定されていません"); + } + + $url = "{$this->baseUrl}/images:annotate"; + + $requestBody = [ + 'requests' => [ + [ + 'image' => [ + 'content' => $base64Image + ], + 'features' => [ + [ + 'type' => 'TEXT_DETECTION', + 'maxResults' => 1 + ] + ], + 'imageContext' => [ + 'languageHints' => ['ja'] // 日本語優先 + ] + ] + ] + ]; + + // タイムアウト短縮(従来30秒→15秒) + $response = Http::timeout(15) + ->withHeaders([ + 'Content-Type' => 'application/json', + ]) + ->post($url . '?key=' . $this->apiKey, $requestBody); + + if (!$response->successful()) { + throw new Exception("Google Vision API request failed: " . $response->status()); + } + + $data = $response->json(); + + // APIエラーチェック + if (isset($data['responses'][0]['error'])) { + $error = $data['responses'][0]['error']; + throw new Exception("Google Vision API error: " . $error['message']); + } + + // テキスト抽出結果を取得 + $textAnnotations = $data['responses'][0]['textAnnotations'] ?? []; + + if (empty($textAnnotations)) { + return ''; // テキストが検出されなかった場合 + } + + // 最初のアノテーションが全体のテキスト + return $textAnnotations[0]['description'] ?? ''; + } + + /** + * 文字列の類似度を計算 + * + * @param string $expected 期待する文字列 + * @param string $detected 検出された文字列 + * @return float 類似度(0-100) + */ + public function calculateSimilarity(string $expected, string $detected): float + { + // 前処理:空白文字削除、大文字小文字統一 + $expected = $this->normalizeText($expected); + $detected = $this->normalizeText($detected); + + if (empty($expected) || empty($detected)) { + return 0.0; + } + + // 複数の計算方法を試し、最高値を返す + $similarities = []; + + // 1. 標準的な類似度計算 + similar_text($expected, $detected, $percent); + $similarities[] = $percent; + + // 2. 文字含有率計算(OCRの分離文字対応) + $containsScore = $this->calculateContainsScore($expected, $detected); + $similarities[] = $containsScore; + + // 3. 部分文字列マッチング + $substringScore = $this->calculateSubstringScore($expected, $detected); + $similarities[] = $substringScore; + + // 最高スコアを返す + return max($similarities); + } + + /** + * 文字含有率スコア計算(姓名の分離文字対応) + */ + private function calculateContainsScore(string $expected, string $detected): float + { + if (empty($expected)) return 0.0; + + // 期待文字列の各文字が検出文字列に含まれるかチェック + $expectedChars = mb_str_split($expected, 1, 'UTF-8'); + $containedCount = 0; + + foreach ($expectedChars as $char) { + if (mb_strpos($detected, $char, 0, 'UTF-8') !== false) { + $containedCount++; + } + } + + return ($containedCount / count($expectedChars)) * 100; + } + + /** + * 部分文字列マッチングスコア計算 + */ + private function calculateSubstringScore(string $expected, string $detected): float + { + if (empty($expected)) return 0.0; + + // 期待文字列を2文字以上のチャンクに分割してマッチング + $chunks = []; + $expectedLength = mb_strlen($expected, 'UTF-8'); + + // 2文字チャンク + for ($i = 0; $i <= $expectedLength - 2; $i++) { + $chunks[] = mb_substr($expected, $i, 2, 'UTF-8'); + } + + // 3文字チャンク + for ($i = 0; $i <= $expectedLength - 3; $i++) { + $chunks[] = mb_substr($expected, $i, 3, 'UTF-8'); + } + + if (empty($chunks)) return 0.0; + + $matchedChunks = 0; + foreach ($chunks as $chunk) { + if (mb_strpos($detected, $chunk, 0, 'UTF-8') !== false) { + $matchedChunks++; + } + } + + return ($matchedChunks / count($chunks)) * 100; + } + + /** + * テキスト正規化(OCR認識結果の形式差異に対応) + */ + private function normalizeText(string $text): string + { + // 空白文字除去、改行除去 + $text = preg_replace('/\s+/', '', $text); + + // 全角英数字・記号を半角に変換 + $text = mb_convert_kana($text, 'rnask', 'UTF-8'); + + // 住所の同義語統一 + $text = str_replace(['東京市', '東京府'], '東京都', $text); + $text = str_replace(['大阪市', '大阪府'], '大阪', $text); + + // 数字の統一(全角→半角は上でやっているが、明示的に処理) + $text = str_replace(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], $text); + + // 記号の統一 + $text = str_replace(['ー', '−', '―'], '-', $text); + $text = str_replace(['の'], '', $text); // 「1番地の1」→「1番地1」 + + return $text; + } + + /** + * 部分マッチング検索 + * + * @param string $needle 検索する文字列 + * @param string $haystack 検索対象の文字列 + * @return bool + */ + public function containsText(string $needle, string $haystack): bool + { + $needle = $this->normalizeText($needle); + $haystack = $this->normalizeText($haystack); + + return mb_strpos($haystack, $needle) !== false; + } + + /** + * 画像形式の検証 + */ + public function isValidImageFormat(string $filename): bool + { + $allowedExtensions = config('shj1.storage.allowed_extensions'); + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + return in_array($extension, $allowedExtensions); + } + + /** + * APIキーが設定されているかチェック + */ + public function isApiKeyConfigured(): bool + { + $dummyKey = env('GOOGLE_VISION_API_KEY') === 'dummy_google_vision_api_key_replace_with_real_one'; + return !empty($this->apiKey) && !$dummyKey; + } + + /** + * Vision API レスポンスから信頼度を取得 + */ + public function getConfidenceScore(array $textAnnotation): float + { + if (!isset($textAnnotation['boundingPoly']['vertices'])) { + return 0.0; + } + + // 検出領域のサイズから信頼度を推定 + $vertices = $textAnnotation['boundingPoly']['vertices']; + if (count($vertices) < 4) { + return 0.0; + } + + // 単純な信頼度計算(実際の実装では、より詳細な分析が必要) + return 0.95; // デフォルト高信頼度 + } +} diff --git a/app/Services/ShjEightService.php b/app/Services/ShjEightService.php new file mode 100644 index 0000000..f720c1b --- /dev/null +++ b/app/Services/ShjEightService.php @@ -0,0 +1,270 @@ + 0|1, 'error_message' => string|null] + */ + public function execute( + int $deviceId, + ?string $processName, + ?string $jobName, + string $status, + string $statusComment, + string $createdDate, + string $updatedDate + ): array { + try { + Log::info('SHJ-8 バッチ処理ログ作成開始', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ]); + + // 【処理1】入力パラメーターをチェックする + $validationResult = $this->validateParameters( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $createdDate, + $updatedDate + ); + + // 【判断1】チェック結果 + if (!$validationResult['valid']) { + // パラメーターNG + $errorMessage = $validationResult['error_message']; + + Log::warning('SHJ-8 パラメーターチェックNG', [ + 'error_message' => $errorMessage + ]); + + // 【処理3】異常終了の結果を返却 + return [ + 'result' => 1, + 'error_message' => $errorMessage + ]; + } + + // 【処理2】バッチ処理ログを登録する + $batJobLog = $this->createBatchJobLog( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $createdDate, + $updatedDate + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了', [ + 'job_log_id' => $batJobLog->job_log_id, + 'device_id' => $batJobLog->device_id, + 'process_name' => $batJobLog->process_name, + 'job_name' => $batJobLog->job_name + ]); + + // 【処理3】正常終了の結果を返却 + return [ + 'result' => 0 + ]; + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // 例外発生時の処理結果を返却 + return [ + 'result' => 1, + 'error_code' => $e->getCode(), + 'error_message' => $e->getMessage(), + 'stack_trace' => $e->getTraceAsString() + ]; + } + } + + /** + * 【処理1】入力パラメーターをチェックする + * + * 修正版:7項目チェック(status_comment追加) + * 1. デバイスID: 必須、device表に存在チェック + * 2. プロセス名: 「プロセス名」「ジョブ名」いずれか必須 + * 3. ジョブ名: 「プロセス名」「ジョブ名」いずれか必須 + * 4. ステータス: 必須 + * 5. ステータスコメント: 必須、≤255文字 + * 6. 登録日時: 日付型(yyyy/mm/dd形式) + * 7. 更新日時: 日付型(yyyy/mm/dd形式) + * + * @param int $deviceId デバイスID + * @param string|null $processName プロセス名 + * @param string|null $jobName ジョブ名 + * @param string $status ステータス + * @param string $statusComment ステータスコメント + * @param string $createdDate 登録日時 + * @param string $updatedDate 更新日時 + * @return array 検証結果 ['valid' => bool, 'error_message' => string|null] + */ + private function validateParameters( + int $deviceId, + ?string $processName, + ?string $jobName, + string $status, + string $statusComment, + string $createdDate, + string $updatedDate + ): array { + $errors = []; + + // 1. デバイスIDチェック (必須、存在チェック) + if ($deviceId <= 0) { + $errors[] = "パラメーターNG:デバイスID/{$deviceId}"; + } elseif (!Device::where('device_id', $deviceId)->exists()) { + $errors[] = "パラメーターNG:デバイスID/{$deviceId}"; + } + + // 2. プロセス名とジョブ名のいずれか必須チェック + if (empty($processName) && empty($jobName)) { + $errors[] = "パラメーターNG:プロセス名/<空>"; + $errors[] = "パラメーターNG:ジョブ名/<空>"; + } + + // 3. ステータス必須チェック + if (empty($status)) { + $errors[] = "パラメーターNG:ステータス/<空>"; + } + + // 4. ステータスコメント必須チェック、255文字以内 + if (empty($statusComment)) { + $errors[] = "パラメーターNG:ステータスコメント/<空>"; + } elseif (mb_strlen($statusComment) > 255) { + $errors[] = "パラメーターNG:ステータスコメント/長さ超過(" . mb_strlen($statusComment) . "文字)"; + } + + // 5. 登録日時の日付型チェック + if (!$this->isValidDateFormat($createdDate)) { + $errors[] = "パラメーターNG:登録日時/{$createdDate}"; + } + + // 6. 更新日時の日付型チェック + if (!$this->isValidDateFormat($updatedDate)) { + $errors[] = "パラメーターNG:更新日時/{$updatedDate}"; + } + + // エラーがある場合 + if (!empty($errors)) { + // 複数のエラーがある場合は全角カンマ(、)で連結 + $errorMessage = implode('、', $errors); + + return [ + 'valid' => false, + 'error_message' => $errorMessage + ]; + } + + // 正常終了 + return [ + 'valid' => true, + 'error_message' => null + ]; + } + + /** + * 日付形式の検証 + * + * yyyy/mm/dd形式かチェック + * + * @param string $date 日付文字列 + * @return bool 有効な日付形式かどうか + */ + private function isValidDateFormat(string $date): bool + { + // yyyy/mm/dd形式の正規表現チェック + if (!preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $date)) { + return false; + } + + // 実際の日付として有効かチェック + $dateParts = explode('/', $date); + return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); + } + + /** + * 【処理2】バッチ処理ログを登録する + * + * bat_job_logテーブルにINSERT + * 修正版:ステータスコメントは呼び出し元から受け取る(固定値廃止) + * + * @param int $deviceId デバイスID + * @param string|null $processName プロセス名 + * @param string|null $jobName ジョブ名 + * @param string $status ステータス + * @param string $statusComment ステータスコメント(業務固有) + * @param string $createdDate 登録日時 (yyyy/mm/dd形式) + * @param string $updatedDate 更新日時 (yyyy/mm/dd形式) + * @return BatJobLog 作成されたバッチジョブログ + */ + private function createBatchJobLog( + int $deviceId, + ?string $processName, + ?string $jobName, + string $status, + string $statusComment, + string $createdDate, + string $updatedDate + ): BatJobLog { + // 日付文字列をdatetime型に変換(現在時刻を使用) + $createdDatetime = Carbon::createFromFormat('Y/m/d', $createdDate); + $updatedDatetime = Carbon::createFromFormat('Y/m/d', $updatedDate); + + // bat_job_logテーブルに登録(status_commentは呼び出し元から受け取った値を使用) + $batJobLog = BatJobLog::create([ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'created_at' => $createdDatetime, + 'updated_at' => $updatedDatetime + ]); + + return $batJobLog; + } +} diff --git a/app/Services/ShjElevenService.php b/app/Services/ShjElevenService.php new file mode 100644 index 0000000..acc7c23 --- /dev/null +++ b/app/Services/ShjElevenService.php @@ -0,0 +1,807 @@ +shjEightService = $shjEightService; + } + + /** + * バッチ処理用の有効なデバイスIDを取得 + * + * deviceテーブルから最初の有効なdevice_idを取得する。 + * データが存在しない場合は例外をスローする。 + * + * @return int 有効なdevice_id + * @throws \Exception deviceテーブルにレコードが存在しない場合 + */ + private function getBatchDeviceId(): int + { + $device = Device::orderBy('device_id')->first(); + + if (!$device) { + throw new \Exception('deviceテーブルにレコードが存在しません。SHJ-8バッチログ作成に必要なデバイスIDを取得できません。'); + } + + Log::debug('SHJ-11 バッチ用デバイスID取得', [ + 'device_id' => $device->device_id + ]); + + return $device->device_id; + } + + /** + * 現在使用中のprice主表名を取得する + * + * setting.web_master から価格マスタテーブル名を決定 + * web_master の値('_a' または '_b')から 'price_a' または 'price_b' を返す + * + * @return string price主表名('price_a' または 'price_b') + * @throws \Exception setting取得失敗時 + */ + private function getPriceTableName(): string + { + try { + $setting = Setting::getSettings(); + + if (!$setting || empty($setting->web_master)) { + throw new \Exception('setting.web_masterの取得に失敗しました'); + } + + // web_master の値:'_a' または '_b' + $webMaster = $setting->web_master; + + // 'price_' + ('_a' → 'a' / '_b' → 'b') + $priceTable = 'price_' . ltrim($webMaster, '_'); + + Log::info('SHJ-11 price主表名決定', [ + 'web_master' => $webMaster, + 'price_table' => $priceTable + ]); + + return $priceTable; + + } catch (\Exception $e) { + Log::error('SHJ-11 price主表名取得エラー', [ + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * 【処理1】集計単位每个の契約台数を算出する + * + * 集計単位: 駐輪場ID + 車種区分ID + 駐輪分類ID + ゾーンID + * + * SQL仕様に基づく複雑なJOIN処理: + * - regular_contract (T1) + * - park (T2) + * - psection (T4) + * - price_a/price_b (T5) ※web_master設定により動的に決定 + * - ptype (T3) + * + * @return array 契約台数集計結果 + */ + public function calculateContractCounts(): array + { + try { + // setting.web_master から使用するprice主表を決定 + $priceTable = $this->getPriceTableName(); + + $query = DB::table('regular_contract as T1') + ->select([ + 'T1.park_id', // 駐輪場ID + 'T2.park_name', // 駐輪場名 + 'T5.psection_id', // 車種区分ID + 'T4.psection_subject', // 車種区分名 + 'T5.ptype_id', // 駐輪分類ID + 'T3.ptype_subject', // 駐輪分類名 + 'T1.zone_id', // ゾーンID + DB::raw('count(T1.contract_id) as cnt') // 契約台数 + ]) + // park テーブルとの JOIN + ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') + // psection テーブルとの JOIN + ->join('psection as T4', 'T1.psection_id', '=', 'T4.psection_id') + // price_a/price_b テーブルとの複合条件 JOIN(動的テーブル名) + ->join(DB::raw($priceTable . ' as T5'), function($join) { + $join->on('T1.park_id', '=', 'T5.park_id') + ->on('T1.price_parkplaceid', '=', 'T5.price_parkplaceid') + ->on('T1.psection_id', '=', 'T5.psection_id'); + }) + // ptype テーブルとの JOIN + ->join('ptype as T3', 'T5.ptype_id', '=', 'T3.ptype_id') + ->where([ + ['T1.contract_flag', '=', 1], // 授受フラグ = 1 + ['T2.park_close_flag', '=', 0], // 閉設フラグ = 0 + ]) + // 契約有効期間内の条件(仕様書指定:'%y.%m.%d'形式) + ->whereRaw("date_format(now(), '%y.%m.%d') BETWEEN T1.contract_periods AND T1.contract_periode") + // zone_idがNULLのレコードは除外(ゾーンマスタ更新不可のため) + ->whereNotNull('T1.zone_id') + ->groupBy([ + 'T1.park_id', + 'T2.park_name', + 'T5.psection_id', + 'T4.psection_subject', + 'T5.ptype_id', + 'T3.ptype_subject', + 'T1.zone_id' + ]) + ->get(); + + Log::info('SHJ-11 契約台数算出完了', [ + 'count' => $query->count(), + 'price_table' => $priceTable, + 'sql_conditions' => [ + 'contract_flag' => 1, + 'park_close_flag' => 0, + 'date_format' => '%y.%m.%d', + 'contract_period_check' => 'BETWEEN contract_periods AND contract_periode' + ] + ]); + + return $query->toArray(); + + } catch (\Exception $e) { + Log::error('SHJ-11 契約台数算出エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } + + /** + * 【処理2・3】ゾーンマスタ管理処理 + * + * 処理フロー(仕様書準拠): + * 処理1の取得レコード数分繰り返し: + * 【処理2】ゾーンマスタを取得する + * 【判断2】取得判定 + * - ゾーンマスタなし → INSERT(トランザクション内) → commit → 【処理4】SHJ-8(トランザクション外) → 次へ + * - ゾーンマスタあり → 【判断3】契約台数チェック → 【処理3】UPDATE(トランザクション内) → commit → 【処理4】SHJ-8(トランザクション外) → 次へ + * + * ※SHJ-8ログは各レコード処理後に確実に記録するため、トランザクション外で実行 + * + * @param array $contractCounts 契約台数集計結果 + * @return array 処理結果 + */ + public function processZoneManagement(array $contractCounts): array + { + $createdZones = 0; + $updatedZones = 0; + $overCapacityCount = 0; + $batchLogErrors = []; + + try { + + // 処理1の取得レコード数分繰り返し + foreach ($contractCounts as $contractData) { + try { + // 【防御的チェック】必須キーがNULLの場合はスキップ + if (empty($contractData->zone_id) || empty($contractData->psection_id) || empty($contractData->ptype_id)) { + Log::warning('SHJ-11 必須キー欠落のためスキップ', [ + 'park_id' => $contractData->park_id ?? null, + 'zone_id' => $contractData->zone_id ?? null, + 'psection_id' => $contractData->psection_id ?? null, + 'ptype_id' => $contractData->ptype_id ?? null + ]); + + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id ?? null, + 'zone_id' => $contractData->zone_id ?? null, + 'error' => '必須キー欠落(zone_id/psection_id/ptype_id)' + ]; + + continue; + } + + // 各レコードごとに独立したトランザクションを開始 + DB::beginTransaction(); + + // 【処理2】ゾーンマスタを取得する + $zoneData = $this->getZoneData($contractData); + + // 【判断2】取得判定 + if (!$zoneData) { + // ゾーンマスタなし → INSERT + $createResult = $this->createZoneData($contractData); + + if (!$createResult['success']) { + // INSERT失敗時のエラー処理 + DB::rollBack(); + + Log::error('SHJ-11 ゾーンマスタINSERT失敗', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => $createResult['message'] + ]); + + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => 'INSERT失敗: ' . $createResult['message'] + ]; + + // INSERT失敗時も次の繰り返しへ + continue; + } + + // INSERTトランザクションをcommit + DB::commit(); + + $createdZones++; + + // status_comment作成(INSERT分岐)仕様書フォーマット + $statusComment = $this->formatInsertStatusComment($contractData); + + // 【処理4】bat_job_logに直接書き込み(トランザクション外・INSERT分岐) + $batchLogResult = $this->writeBatJobLog( + $contractData, + $statusComment, + false // 限界台数超過なし + ); + + // bat_job_log書き込み結果チェック + if (!$batchLogResult['success']) { + Log::warning('bat_job_log書き込み失敗(INSERT分岐)', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error_message' => $batchLogResult['error_message'] ?? null + ]); + + // bat_job_log書き込み失敗をbatch_log_errorsに記録 + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => 'bat_job_log書き込み失敗(INSERT分岐): ' . ($batchLogResult['error_message'] ?? 'unknown error') + ]; + } + + // INSERT分岐は【処理4】後に次の繰り返しへ + continue; + } + + // ゾーンマスタあり → 【判断3】契約台数チェック + $isOverCapacity = $this->checkCapacityLimit($contractData, $zoneData); + if ($isOverCapacity) { + $overCapacityCount++; + Log::warning('SHJ-11 限界台数超過検出', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'current_count' => $contractData->cnt, + 'limit_capacity' => $zoneData->zone_tolerance ?? 0 + ]); + } + + // 【処理3】契約台数を反映する(UPDATE) + $updateResult = $this->updateZoneContractCount($contractData); + + if (!$updateResult['success']) { + // UPDATE失敗時のエラー処理 + DB::rollBack(); + + Log::error('SHJ-11 ゾーンマスタUPDATE失敗', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => $updateResult['message'] + ]); + + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => 'UPDATE失敗: ' . $updateResult['message'] + ]; + + // UPDATE失敗時も次の繰り返しへ + continue; + } + + // UPDATEトランザクションをcommit + DB::commit(); + + $updatedZones++; + + // status_comment作成(UPDATE分岐)仕様書フォーマット + $capacityAlert = $isOverCapacity ? '限界収容台数を超えています。' : ''; + $statusComment = $this->formatUpdateStatusComment( + $contractData, + $zoneData, + $capacityAlert + ); + + // 【処理4】bat_job_logに直接書き込み(トランザクション外・UPDATE分岐) + $batchLogResult = $this->writeBatJobLog( + $contractData, + $statusComment, + $isOverCapacity + ); + + // bat_job_log書き込み結果チェック + if (!$batchLogResult['success']) { + Log::warning('bat_job_log書き込み失敗(UPDATE分岐)', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error_message' => $batchLogResult['error_message'] ?? null + ]); + + // bat_job_log書き込み失敗をbatch_log_errorsに記録 + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => 'bat_job_log書き込み失敗(UPDATE分岐): ' . ($batchLogResult['error_message'] ?? 'unknown error') + ]; + } + + } catch (\Exception $e) { + // 例外時はロールバック(アクティブなトランザクションがある場合のみ) + if (DB::transactionLevel() > 0) { + DB::rollBack(); + } + + // 個別レコードエラーもログ記録して次へ進む + $batchLogErrors[] = [ + 'park_id' => $contractData->park_id ?? null, + 'zone_id' => $contractData->zone_id ?? null, + 'error' => $e->getMessage() + ]; + + Log::warning('SHJ-11 個別レコード処理エラー', [ + 'park_id' => $contractData->park_id ?? null, + 'zone_id' => $contractData->zone_id ?? null, + 'error' => $e->getMessage() + ]); + + // エラーでも次の繰り返しへ続行 + continue; + } + } + + return [ + 'success' => true, + 'created_zones' => $createdZones, + 'updated_zones' => $updatedZones, + 'over_capacity_count' => $overCapacityCount, + 'batch_log_errors' => $batchLogErrors, + 'message' => '現在契約台数集計処理完了' + ]; + + } catch (\Exception $e) { + // 外層エラー時のロールバック + // ※各レコード処理は独立トランザクションのため、 + // ここでのrollBackは不要だが安全のため実行 + if (DB::transactionLevel() > 0) { + DB::rollBack(); + } + + Log::error('SHJ-11 ゾーンマスタ管理処理全体エラー', [ + 'error' => $e->getMessage(), + 'created_zones' => $createdZones, + 'updated_zones' => $updatedZones + ]); + + return [ + 'success' => false, + 'created_zones' => $createdZones, + 'updated_zones' => $updatedZones, + 'over_capacity_count' => $overCapacityCount, + 'batch_log_errors' => $batchLogErrors, + 'message' => 'ゾーンマスタ管理処理エラー: ' . $e->getMessage(), + 'details' => $e->getTraceAsString() + ]; + } + } + + /** + * ゾーンマスタデータ取得 + * + * 集計単位に対応するゾーンマスタを取得 + * + * @param object $contractData 契約台数集計データ + * @return object|null ゾーンマスタデータ + */ + private function getZoneData($contractData) + { + try { + return DB::table('zone') + ->select([ + 'zone_id', + 'park_id', + 'ptype_id', + 'psection_id', + 'zone_name', + 'zone_number', // 現在契約台数 + 'zone_standard', // 標準収容台数 + 'zone_tolerance' // 限界収容台数 + ]) + ->where([ + ['park_id', '=', $contractData->park_id], + ['psection_id', '=', $contractData->psection_id], + ['ptype_id', '=', $contractData->ptype_id], + ['zone_id', '=', $contractData->zone_id] + ]) + ->first(); + + } catch (\Exception $e) { + Log::error('SHJ-11 ゾーンマスタ取得エラー', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id, + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * ゾーンマスタ新規作成 + * + * 仕様書に基づくINSERT処理 + * + * @param object $contractData 契約台数集計データ + * @return array 作成結果 + */ + private function createZoneData($contractData): array + { + try { + // 新規ゾーンマスタのデフォルト値設定 + $newZoneData = [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id, + 'ptype_id' => $contractData->ptype_id, + 'psection_id' => $contractData->psection_id, + 'zone_name' => (string)$contractData->zone_id, // 仕様書: JOB1.ゾーンID + 'zone_number' => $contractData->cnt, // 算出した契約台数 + 'zone_standard' => null, // デフォルトnull + 'zone_tolerance' => null, // デフォルトnull + 'zone_sort' => null, // デフォルトnull + 'delete_flag' => 0, // 削除フラグOFF + 'created_at' => now(), + 'updated_at' => now(), + 'ope_id' => 9999999 // 仕様書指定値 + ]; + + DB::table('zone')->insert($newZoneData); + + Log::info('SHJ-11 ゾーンマスタ新規作成完了', [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id, + 'contract_count' => $contractData->cnt + ]); + + return [ + 'success' => true, + 'zone_data' => (object) $newZoneData, + 'message' => 'ゾーンマスタを新規作成しました' + ]; + + } catch (\Exception $e) { + Log::error('SHJ-11 ゾーンマスタ新規作成エラー', [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'zone_data' => null, + 'message' => 'ゾーンマスタ新規作成エラー: ' . $e->getMessage() + ]; + } + } + + /** + * 契約台数限界チェック + * + * 【判断3】契約台数 > 限界収容台数の判定 + * + * @param object $contractData 契約台数集計データ + * @param object|null $zoneData ゾーンマスタデータ + * @return bool 限界台数超過フラグ + */ + private function checkCapacityLimit($contractData, $zoneData = null): bool + { + if (!$zoneData || is_null($zoneData->zone_tolerance)) { + // ゾーンデータが存在しないか、限界収容台数が未設定の場合は超過判定しない + return false; + } + + // 契約台数 > 限界収容台数の判定 + return $contractData->cnt > $zoneData->zone_tolerance; + } + + /** + * ゾーンマスタ契約台数更新 + * + * 【処理3】現在契約台数をゾーンマスタに反映 + * + * @param object $contractData 契約台数集計データ + * @return array 更新結果 + */ + private function updateZoneContractCount($contractData): array + { + try { + $updateData = [ + 'zone_number' => $contractData->cnt, // 現在契約台数を更新 + 'updated_at' => now(), // 更新日時 + 'ope_id' => 9999999 // 更新オペレータID(INSERT時と同様、DB型int unsignedに対応) + ]; + + $updated = DB::table('zone') + ->where([ + ['park_id', '=', $contractData->park_id], + ['psection_id', '=', $contractData->psection_id], + ['ptype_id', '=', $contractData->ptype_id], + ['zone_id', '=', $contractData->zone_id] + ]) + ->update($updateData); + + if ($updated > 0) { + Log::info('SHJ-11 ゾーンマスタ更新完了', [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id, + 'new_contract_count' => $contractData->cnt + ]); + + return [ + 'success' => true, + 'message' => 'ゾーンマスタ契約台数を更新しました' + ]; + } else { + Log::warning('SHJ-11 ゾーンマスタ更新対象なし', [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id + ]); + + return [ + 'success' => false, + 'message' => 'ゾーンマスタ更新対象が見つかりません' + ]; + } + + } catch (\Exception $e) { + Log::error('SHJ-11 ゾーンマスタ更新エラー', [ + 'zone_id' => $contractData->zone_id, + 'park_id' => $contractData->park_id, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => 'ゾーンマスタ更新エラー: ' . $e->getMessage() + ]; + } + } + + /** + * INSERT分岐用ステータスコメント作成(JOB2-STEP1) + * + * 仕様書フォーマット: + * 「JOB1.駐輪場名」+"/"+「JOB1.駐輪分類名」+"/"+「JOB1.車種区分名」+"/" + * +「JOB1.ゾーン名」+"/契約台数"+「JOB1.cnt」+": マスタなしのため登録" + * + * @param object $contractData 契約台数集計データ + * @return string フォーマット済みstatus_comment + */ + private function formatInsertStatusComment($contractData): string + { + return $contractData->park_name . '/' + . $contractData->ptype_subject . '/' + . $contractData->psection_subject . '/' + . $contractData->zone_id . '/契約台数' + . $contractData->cnt . ': マスタなしのため登録'; + } + + /** + * UPDATE分岐用ステータスコメント作成(JOB3) + * + * 仕様書フォーマット: + * 「JOB1.駐輪場名」+"/"+「JOB1.駐輪分類名」+"/"+「JOB1.車種区分名」+"/"+「JOB1.ゾーン名」 + * +"/限界収容台数:"+「JOB2.限界収容台数」 + * +"/現在契約台数(更新前):"+「JOB2.現在契約台数」 + * +"/現在契約台数(更新後):"+「JOB1.cnt」+"/" + * + [台数アラート] + * + * @param object $contractData 契約台数集計データ + * @param object $zoneData ゾーンマスタデータ + * @param string $capacityAlert 台数アラート(「限界収容台数を超えています。」または空文字) + * @return string フォーマット済みstatus_comment + */ + private function formatUpdateStatusComment( + $contractData, + $zoneData, + string $capacityAlert + ): string { + $comment = $contractData->park_name . '/' + . $contractData->ptype_subject . '/' + . $contractData->psection_subject . '/' + . $contractData->zone_id + . '/限界収容台数:' . ($zoneData->zone_tolerance ?? 0) + . '/現在契約台数(更新前):' . ($zoneData->zone_number ?? 0) + . '/現在契約台数(更新後):' . $contractData->cnt . '/'; + + if (!empty($capacityAlert)) { + $comment .= $capacityAlert; + } + + return $comment; + } + + /** + * 【処理4】個別レコードのバッチ処理ログを作成する + * + * SHJ-11は業務固有のstatus_commentを記録するため、SHJ-8を使わずbat_job_logに直接書き込む + * + * bat_job_log登録内容: + * 1. デバイスID + * 2. プロセス名: SHJ-11 + * 3. ジョブ名: SHJ-11現在契約台数集計 + * 4. ステータス: success + * 5. ステータスコメント: 業務固有(駐輪場名/駐輪分類名/車種区分名/ゾーンID[台数アラート]) + * 6. 登録日時: 現在の日付 + * 7. 更新日時: 現在の日付 + * + * @param object $contractData 契約台数集計データ + * @param string $statusComment status_comment(台数アラート含む) + * @param bool $isOverCapacity 限界台数超過フラグ + * @return array 処理結果 ['success' => bool, 'error_message' => string|null] + */ + private function writeBatJobLog( + $contractData, + string $statusComment, + bool $isOverCapacity + ): array { + try { + // SHJ-8バッチ処理ログ作成パラメータ設定 + $deviceId = $this->getBatchDeviceId(); // deviceテーブルから有効なIDを取得 + $processName = 'SHJ-11'; + $jobName = 'SHJ-11現在契約台数集計'; + $status = 'success'; + $today = now()->format('Y/m/d'); + + Log::info('SHJ-8バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $today, + $today + ); + + Log::info('SHJ-8バッチ処理ログ作成完了', [ + 'park_id' => $contractData->park_id, + 'zone_id' => $contractData->zone_id + ]); + + return [ + 'success' => true, + 'error_message' => null + ]; + + } catch (\Exception $e) { + Log::error('bat_job_log書き込みエラー', [ + 'park_id' => $contractData->park_id ?? null, + 'zone_id' => $contractData->zone_id ?? null, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // エラーが発生してもメイン処理は継続 + return [ + 'success' => false, + 'error_message' => $e->getMessage() + ]; + } + } + + /** + * 【判断1】取得件数=0時のバッチログ作成 + * + * 取得件数が0件の場合に呼び出される特別なバッチログ作成 + * bat_job_logに直接書き込み + * + * bat_job_log登録内容: + * 1. デバイスID + * 2. プロセス名: SHJ-11 + * 3. ジョブ名: SHJ-11現在契約台数集計 + * 4. ステータス: success + * 5. ステータスコメント: 全駐輪場契約なし + * 6. 登録日時: 現在の日付 + * 7. 更新日時: 現在の日付 + * + * @return array 処理結果 ['success' => bool, 'error_message' => string|null] + */ + public function writeBatJobLogForNoContracts(): array + { + try { + // SHJ-8バッチ処理ログ作成パラメータ設定 + $deviceId = $this->getBatchDeviceId(); // deviceテーブルから有効なIDを取得 + $processName = 'SHJ-11'; + $jobName = 'SHJ-11現在契約台数集計'; + $status = 'success'; + $statusComment = '全駐輪場契約なし'; + $today = now()->format('Y/m/d'); + + Log::info('SHJ-8バッチ処理ログ作成(対象なし)', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $today, + $today + ); + + Log::info('SHJ-8バッチ処理ログ作成完了(対象なし)'); + + return [ + 'success' => true, + 'error_message' => null + ]; + + } catch (\Exception $e) { + Log::error('bat_job_log書き込みエラー(対象なし)', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error_message' => $e->getMessage() + ]; + } + } +} diff --git a/app/Services/ShjFiveService.php b/app/Services/ShjFiveService.php new file mode 100644 index 0000000..629aa1f --- /dev/null +++ b/app/Services/ShjFiveService.php @@ -0,0 +1,693 @@ +shjEightService = $shjEightService; + } + /** + * SHJ-5 メイン処理を実行 + * + * 処理フロー: + * 1. 駐輪場の空き状況を取得する + * 2. 空き状況判定 + * 3. 空き待ち者の情報を取得する + * 4. 取得件数判定 + * 5. 空き待ち者への通知、またはオペレーターキュー追加処理 + * 6. バッチ処理ログを作成する + * + * @return array 処理結果 + */ + public function executeParkVacancyNotification(): array + { + try { + $startTime = now(); + Log::info('SHJ-5 空き待ち通知処理開始'); + + // 処理統計 + $processedParksCount = 0; + $vacantParksCount = 0; + $totalWaitingUsers = 0; + $notificationSuccessCount = 0; + $operatorQueueCount = 0; + $mailErrors = []; // メール異常終了件数専用 + $errors = []; // 全体エラー収集用 + $allQueueItems = []; // 全オペレーターキュー作成用データ + + // 【処理1】駐輪場の空き状況を取得する + $parkVacancyList = $this->getParkVacancyStatus(); + Log::info('駐輪場空き状況取得完了', [ + 'total_parks' => count($parkVacancyList) + ]); + + // 各駐輪場に対する処理 + foreach ($parkVacancyList as $parkVacancyData) { + // 配列をオブジェクトに変換 + $parkVacancy = (object) $parkVacancyData; + $processedParksCount++; + + Log::info('駐輪場処理開始', [ + 'park_id' => $parkVacancy->park_id, + 'park_name' => $parkVacancy->park_name, + 'psection_id' => $parkVacancy->psection_id, + 'ptype_id' => $parkVacancy->ptype_id, + 'vacant_count' => $parkVacancy->vacant_count + ]); + + // 【判断1】空き状況判定 + if ($parkVacancy->vacant_count < 1) { + Log::info('空きなし - 処理スキップ', [ + 'park_id' => $parkVacancy->park_id, + 'vacant_count' => $parkVacancy->vacant_count + ]); + continue; + } + + $vacantParksCount++; + + // 【処理2】空き待ち者の情報を取得する + $waitingUsers = $this->getWaitingUsersInfo( + $parkVacancy->park_id, + $parkVacancy->psection_id, + $parkVacancy->ptype_id + ); + + // 【判断2】取得件数判定 + if (empty($waitingUsers)) { + Log::info('空き待ち者なし', [ + 'park_id' => $parkVacancy->park_id + ]); + continue; + } + + $totalWaitingUsers += count($waitingUsers); + Log::info('空き待ち者情報取得完了', [ + 'park_id' => $parkVacancy->park_id, + 'waiting_users_count' => count($waitingUsers) + ]); + + // 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 + $notificationResult = $this->processWaitingUsersNotification( + $waitingUsers, + $parkVacancy + ); + + $notificationSuccessCount += $notificationResult['notification_success_count']; + $operatorQueueCount += $notificationResult['operator_queue_count']; + + if (!empty($notificationResult['errors'])) { + $mailErrors = array_merge($mailErrors, $notificationResult['errors']); + $errors = array_merge($errors, $notificationResult['errors']); + } + + // オペレーターキュー作成用データを収集 + if (!empty($notificationResult['queue_items'])) { + $allQueueItems = array_merge($allQueueItems ?? [], $notificationResult['queue_items']); + } + } + + // 【処理4】仕様書準拠:先に呼び出し、成功時のみ内部変数を更新 + $queueErrorCount = 0; // キュー登録異常終了件数(累計) + $queueSuccessCount = 0; // キュー登録正常終了件数(累計) + + foreach ($allQueueItems as $queueItem) { + // 仕様書準拠:在呼叫前先計算"如果這次成功會是第幾件",確保記錄反映最新件數 + $predictedSuccessCount = $queueSuccessCount + 1; + $predictedErrorCount = $queueErrorCount; // 暂时保持当前错误计数 + + $queueResult = $this->addToOperatorQueue( + $queueItem['waiting_user'], + $queueItem['park_vacancy'], + $queueItem['batch_comment'], + $notificationSuccessCount, // 最終メール正常終了件数 + $predictedSuccessCount, // 預測成功時的件數(包含本次) + count($mailErrors), // 現在のメール異常終了件数(動態計算) + $predictedErrorCount // 現在のキュー登録異常終了件数 + ); + + // 仕様書:根据实际结果决定是否采用预测值 + if ($queueResult['success']) { + $queueSuccessCount = $predictedSuccessCount; // 采用预测的成功计数 + } else { + $queueErrorCount++; // 失败时递增错误计数 + + // 仕様書:包含具体错误消息,满足"エラーメッセージ/スタックトレースを保持"要求 + $errorDetail = $queueResult['error'] ?? 'Unknown error'; + $queueErrorInfo = sprintf('キュー登録失敗:予約ID:%d - %s', + $queueItem['waiting_user']->reserve_id ?? 0, + $errorDetail + ); + $errors[] = $queueErrorInfo; // 加入总错误统计(包含具体原因) + + Log::error('オペレーターキュー作成失敗', [ + 'user_id' => $queueItem['waiting_user']->user_id, + 'reserve_id' => $queueItem['waiting_user']->reserve_id ?? 0, + 'error' => $errorDetail + ]); + } + } + + $endTime = now(); + $duration = $startTime->diffInSeconds($endTime); + + Log::info('SHJ-5 空き待ち通知処理完了', [ + 'duration_seconds' => $duration, + 'processed_parks_count' => $processedParksCount, + 'vacant_parks_count' => $vacantParksCount, + 'total_waiting_users' => $totalWaitingUsers, + 'notification_success_count' => $notificationSuccessCount, + 'operator_queue_success_count' => $queueSuccessCount, // 仕様書:正常完了件数 + 'queue_error_count' => $queueErrorCount, + 'mail_error_count' => count($mailErrors), // メール異常終了件数(分離) + 'total_error_count' => count($errors) // 全体エラー件数 + ]); + + // 仕様書に基づく内部変数.ステータスコメント生成 + $statusComment = sprintf( + 'メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', + $notificationSuccessCount, + count($mailErrors), // メール異常終了件数(キュー失敗を除外) + $queueSuccessCount ?? 0, // 実際のキュー登録成功件数 + $queueErrorCount ?? 0 // 実際のキュー登録失敗件数 + ); + + // SHJ-8 バッチ処理ログ作成 + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-5', + 'SHJ-5空き待ち通知', + 'success', + $statusComment, + $today, + $today + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了'); + } catch (Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage() + ]); + } + + return [ + 'success' => true, + 'message' => 'SHJ-5 空き待ち通知処理が正常に完了しました', + 'processed_parks_count' => $processedParksCount, + 'vacant_parks_count' => $vacantParksCount, + 'total_waiting_users' => $totalWaitingUsers, + 'notification_success_count' => $notificationSuccessCount, + 'operator_queue_count' => $queueSuccessCount ?? 0, // 仕様書:正常完了件数を使用 + 'error_count' => count($errors), + 'errors' => $errors, + 'duration_seconds' => $duration, + 'status_comment' => $statusComment // SHJ-8用の完全なステータスコメント + ]; + + } catch (Exception $e) { + Log::error('SHJ-5 空き待ち通知処理でエラーが発生', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => 'SHJ-5 空き待ち通知処理でエラーが発生: ' . $e->getMessage(), + 'error_details' => $e->getMessage() + ]; + } + } + + /** + * 【処理1】駐輪場の空き状況を取得する + * + * 仕様書に基づくSQL(SQL-1): + * - 「駐輪場マスタ」と「ゾーンマスタ」より空き状況を取得する + * - zone表から zone_number(現在契約台数)、zone_tolerance(限界収容台数)を取得 + * - 空き台数 = 限界収容台数 - 現在契約台数 + * + * @return array 駐輪場空き状況リスト + */ + private function getParkVacancyStatus(): array + { + try { + // 仕様書SQL-1に基づくクエリ:park + zone + ptype + psection + $vacancyList = DB::table('park as T1') + ->select([ + 'T1.park_id', + 'T1.park_name', + 'T1.park_ruby', + 'T2.ptype_id', + 'T3.ptype_subject', + 'T2.psection_id', + 'T4.psection_subject', + DB::raw('sum(T2.zone_number) as sum_zone_number'), // 現在契約台数 + DB::raw('sum(T2.zone_standard) as sum_zone_standard'), // 標準収容台数 + DB::raw('sum(T2.zone_tolerance) as sum_zone_tolerance') // 限界収容台数 + ]) + ->join('zone as T2', 'T1.park_id', '=', 'T2.park_id') + ->join('ptype as T3', 'T2.ptype_id', '=', 'T3.ptype_id') + ->join('psection as T4', 'T2.psection_id', '=', 'T4.psection_id') + ->where([ + ['T1.park_close_flag', '=', 0], // 駐輪場開設 + ['T2.delete_flag', '=', 0], // ゾーン有効 + ]) + ->groupBy(['T1.park_id', 'T2.ptype_id', 'T2.psection_id']) + ->orderBy('T1.park_ruby') + ->orderBy('T3.floor_sort') + ->orderBy('T2.psection_id') + ->get() + ->map(function($record) { + // 【JOB1-STEP1】空き台数 = 限界収容台数 - 現在契約台数 + $vacant_count = max(0, $record->sum_zone_tolerance - $record->sum_zone_number); + + return (object)[ + 'park_id' => $record->park_id, + 'park_name' => $record->park_name, + 'park_ruby' => $record->park_ruby, + 'ptype_id' => $record->ptype_id, + 'ptype_subject' => $record->ptype_subject, + 'psection_id' => $record->psection_id, + 'psection_subject' => $record->psection_subject, + 'sum_zone_number' => $record->sum_zone_number, // JOB1 現在契約台数 + 'sum_zone_standard' => $record->sum_zone_standard, // JOB1 標準収容台数 + 'sum_zone_tolerance' => $record->sum_zone_tolerance, // JOB1 限界収容台数 + 'vacant_count' => $vacant_count, // 内部変数.空き台数 + ]; + }) + ->filter(function($zone) { + // 【判断1】空き状況判定:空きがあるもののみ + return $zone->vacant_count > 0; + }) + ->values(); + + Log::info('駐輪場空き状況取得完了(仕様書SQL-1準拠)', [ + 'total_records' => count($vacancyList), + 'vacant_records' => $vacancyList->filter(function($v) { + return $v->vacant_count > 0; + })->count() + ]); + + return $vacancyList->toArray(); + + } catch (Exception $e) { + Log::error('駐輪場空き状況取得エラー', [ + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * 【処理2】空き待ち者の情報を取得する + * + * 仕様書JOB2に基づくSQL: + * - 空きが発生している「駐輪場ID」「駐輪分類ID」「車種区分ID」で空き待ちしている利用者を抽出する + * - reserve表 + user表のみ(仕様書準拠) + * - JOIN条件:T1.user_id = T2.user_seq(重要!) + * + * @param int $parkId 駐輪場ID(JOB1.駐輪場ID) + * @param int $psectionId 車種区分ID(JOB1.車種区分ID) + * @param int $ptypeId 駐輪分類ID(JOB1.駐輪分類ID) + * @return array 空き待ち者情報リスト + */ + private function getWaitingUsersInfo(int $parkId, int $psectionId, int $ptypeId): array + { + try { + // 仕様書JOB2 SQL準拠:reserve + user のみ + $waitingUsers = DB::table('reserve as T1') + ->select([ + 'T1.user_id', // 利用者ID + 'T1.reserve_id', // 定期予約ID + 'T2.user_name', // 利用者名 + 'T2.user_manual_regist_flag', // 手動登録フラグ + 'T2.user_primemail', // メールアドレス + 'T2.user_submail', // 予備メールアドレス + 'T1.reserve_manual', // 手動通知 + // 以下は処理に必要な追加フィールド + 'T1.park_id', + 'T1.psection_id', + 'T1.ptype_id', + 'T1.reserve_date', + ]) + // 仕様書準拠:T1.user_id = T2.user_seq(user表のPK) + ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') + ->where([ + ['T1.park_id', '=', $parkId], // JOB1.駐輪場ID + ['T1.psection_id', '=', $psectionId], // JOB1.車種区分ID + ['T1.ptype_id', '=', $ptypeId], // JOB1.駐輪分類ID + ['T1.valid_flag', '=', 1], // 有効フラグ = 1 + ['T2.user_quit_flag', '<>', 1] // 退会フラグ <> 1 + ]) + ->whereNull('T1.contract_id') // 定期契約 is null + ->orderBy('T1.reserve_date', 'asc') // 予約日時順 + ->get() + ->toArray(); + + Log::info('空き待ち者情報取得完了(仕様書JOB2準拠)', [ + 'park_id' => $parkId, + 'psection_id' => $psectionId, + 'ptype_id' => $ptypeId, + 'waiting_users_count' => count($waitingUsers) + ]); + + return $waitingUsers; + + } catch (Exception $e) { + Log::error('空き待ち者情報取得エラー', [ + 'park_id' => $parkId, + 'psection_id' => $psectionId, + 'ptype_id' => $ptypeId, + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 + * + * 仕様書に基づく分岐処理: + * - 手動通知フラグ判定(reserve_manual) + * - メール送信成功時のreserve.sent_date更新 + * - 失敗時のオペレーターキュー追加(最終統計で処理) + * + * @param array $waitingUsers 空き待ち者リスト + * @param object $parkVacancy 駐輪場空き情報 + * @return array 通知処理結果 + */ + private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array + { + $notificationSuccessCount = 0; + $operatorQueueCount = 0; + $errors = []; + $queueItems = []; // オペレーターキュー作成用データ収集 + + try { + // 空きがある分だけ処理(先着順) + $availableSpots = min($parkVacancy->vacant_count, count($waitingUsers)); + + for ($i = 0; $i < $availableSpots; $i++) { + $waitingUserData = $waitingUsers[$i]; + // 配列をオブジェクトに変換 + $waitingUser = (object) $waitingUserData; + + try { + // 【仕様判断】手動通知フラグチェック + if ($waitingUser->reserve_manual == 1) { + // 手動通知 → オペレーターキュー作成データ収集 + $batchComment = '手動通知フラグ設定のため予約ID:' . $waitingUser->reserve_id; + $queueItems[] = [ + 'waiting_user' => $waitingUser, + 'park_vacancy' => $parkVacancy, + 'batch_comment' => $batchComment + ]; + $operatorQueueCount++; + + Log::info('手動通知フラグによりオペレーターキュー登録予定', [ + 'user_id' => $waitingUser->user_id, + 'reserve_id' => $waitingUser->reserve_id + ]); + } else { + // 自動通知 → メール送信を試行 + $mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy); + + if ($mailResult['success']) { + // メール送信成功 → reserve.sent_date更新 + $this->updateReserveSentDate($waitingUser->reserve_id); + $notificationSuccessCount++; + + Log::info('空き待ち通知メール送信成功', [ + 'user_id' => $waitingUser->user_id, + 'reserve_id' => $waitingUser->reserve_id, + 'park_id' => $parkVacancy->park_id + ]); + } else { + // メール送信失敗 → オペレーターキュー作成データ収集 + $shjSevenError = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー'; + $batchComment = $shjSevenError . '予約ID:' . $waitingUser->reserve_id; + $queueItems[] = [ + 'waiting_user' => $waitingUser, + 'park_vacancy' => $parkVacancy, + 'batch_comment' => $batchComment + ]; + $operatorQueueCount++; + $errors[] = $shjSevenError; + } + } + + } catch (Exception $e) { + Log::error('空き待ち者通知処理エラー', [ + 'user_id' => $waitingUser->user_id, + 'reserve_id' => $waitingUser->reserve_id, + 'error' => $e->getMessage() + ]); + + // エラー発生時もオペレーターキュー作成データ収集 + $batchComment = 'システムエラー:' . $e->getMessage() . '予約ID:' . $waitingUser->reserve_id; + $queueItems[] = [ + 'waiting_user' => $waitingUser, + 'park_vacancy' => $parkVacancy, + 'batch_comment' => $batchComment + ]; + $operatorQueueCount++; + $errors[] = $e->getMessage(); + } + } + + return [ + 'notification_success_count' => $notificationSuccessCount, + 'operator_queue_count' => $operatorQueueCount, + 'errors' => $errors, + 'queue_items' => $queueItems // 後でキュー作成用 + ]; + + } catch (Exception $e) { + Log::error('空き待ち者通知処理全体エラー', [ + 'park_id' => $parkVacancy->park_id, + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * 空き待ち通知メールを送信 + * + * 仕様書JOB3に基づくSHJ-7呼び出し: + * - パラメータ1: JOB1.メールアドレス(user_primemail) + * - パラメータ2: JOB1.予備メールアドレス(user_submail) + * - パラメータ3: メールID = 201 + * - 追加: reserve_id=%reserve_id%&expiry=%expiry%(予約ID と 無効日) + * + * @param object $waitingUser 空き待ち者情報 + * @param object $parkVacancy 駐輪場空き情報 + * @return array 送信結果 + */ + private function sendVacancyNotificationMail(object $waitingUser, object $parkVacancy): array + { + try { + // ShjMailSendServiceを利用してメール送信 + $mailService = app(ShjMailSendService::class); + + // 仕様書JOB3準拠:メールID = 201 + $mailTemplateId = 201; + + // 仕様書No1/No2に基づく主メール・副メール設定 + $mainEmail = $waitingUser->user_primemail ?? ''; + $subEmail = $waitingUser->user_submail ?? ''; + + // 仕様書準拠:reserve_id と expiry(当月末日、休日考慮しない)を追加 + $expiry = now()->endOfMonth()->format('Y-m-d'); // 当月末日 + $additionalParams = [ + 'reserve_id' => $waitingUser->reserve_id, + 'expiry' => $expiry + ]; + + // メール送信実行(仕様書JOB3準拠) + $mailResult = $mailService->executeMailSend( + $mainEmail, + $subEmail, + $mailTemplateId, + $additionalParams + ); + + // SHJ-7の結果を標準形式に変換(result: 0=成功, 1=失敗) + $success = ($mailResult['result'] ?? 1) === 0; + + Log::info('空き待ち通知メール送信試行完了', [ + 'user_id' => $waitingUser->user_id, + 'main_email' => $mainEmail, + 'sub_email' => $subEmail, + 'mail_template_id' => $mailTemplateId, + 'result' => $mailResult['result'] ?? 1, + 'success' => $success + ]); + + return [ + 'success' => $success, + 'result' => $mailResult['result'] ?? 1, + 'error' => $mailResult['error_info'] ?? null, + 'message' => $success ? 'メール送信成功' : ($mailResult['error_info'] ?? 'メール送信失敗') + ]; + + } catch (Exception $e) { + Log::error('空き待ち通知メール送信エラー', [ + 'user_id' => $waitingUser->user_id, + 'main_email' => $waitingUser->user_primemail ?? '', + 'sub_email' => $waitingUser->user_submail ?? '', + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + /** + * reserve.sent_date及びvalid_flag更新 + * + * 仕様書準拠:メール送信成功時にreserve.sent_dateとvalid_flag=0を同時更新 + * 重複通知を防ぎ、処理済みマークを設定 + * + * @param int $reserveId 予約ID + * @return void + */ + private function updateReserveSentDate(int $reserveId): void + { + try { + DB::table('reserve') + ->where('reserve_id', $reserveId) + ->update([ + 'sent_date' => now()->format('Y-m-d H:i:s'), + 'valid_flag' => 0, // 仕様書:メール送信成功時に0に更新 + 'updated_at' => now() + ]); + + Log::info('reserve.sent_date及びvalid_flag更新完了', [ + 'reserve_id' => $reserveId, + 'sent_date' => now()->format('Y-m-d H:i:s'), + 'valid_flag' => 0 + ]); + + } catch (Exception $e) { + Log::error('reserve.sent_date及びvalid_flag更新エラー', [ + 'reserve_id' => $reserveId, + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * オペレーターキューに追加 + * + * 仕様書に基づくキュー登録: + * - que_comment: 空文字列 + * - que_status_comment: 仕様書完全準拠形式(統計情報含む) + * - operator_id: 9999999固定 + * + * @param object $waitingUser 空き待ち者情報 + * @param object $parkVacancy 駐輪場空き情報 + * @param string $batchComment 内部変数.バッチコメント + * @param int $mailSuccessCount メール正常終了件数 + * @param int $queueSuccessCount キュー登録正常終了件数 + * @param int $mailErrorCount メール異常終了件数 + * @param int $queueErrorCount キュー登録異常終了件数 + * @return array 追加結果 + */ + private function addToOperatorQueue(object $waitingUser, object $parkVacancy, string $batchComment, int $mailSuccessCount, int $queueSuccessCount, int $mailErrorCount, int $queueErrorCount): array + { + try { + // 仕様書完全準拠:駐輪場名/駐輪分類名/車種区分名/空き台数…/対象予約ID…/内部変数.バッチコメント/内部変数.メール正常終了件数…メール異常終了件数…キュー登録正常終了件数…キュー登録異常終了件数… + $statusComment = sprintf( + '%s/%s/%s/空き台数:%d台/対象予約ID:%d/%s/メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', + $parkVacancy->park_name ?? '', // JOB1から取得 + $parkVacancy->ptype_subject ?? '', // 駐輪分類名(JOB1から取得) + $parkVacancy->psection_subject ?? '', // 車種区分名(JOB1から取得) + $parkVacancy->vacant_count ?? 0, + $waitingUser->reserve_id ?? 0, + $batchComment, // 内部変数.バッチコメント + $mailSuccessCount, // 内部変数.メール正常終了件数 + $mailErrorCount, + $queueSuccessCount, + $queueErrorCount + ); + + OperatorQue::create([ + 'que_class' => 4, // 予約告知通知 + 'user_id' => $waitingUser->user_id, + 'contract_id' => null, + 'park_id' => $waitingUser->park_id, + 'que_comment' => '', // 仕様書:空文字列 + 'que_status' => 1, // キュー発生 + 'que_status_comment' => $statusComment, // 仕様書:完全準拠形式 + 'work_instructions' => '空き待ち者への連絡をお願いします。', + 'operator_id' => 9999999, // 仕様書:固定値9999999 + ]); + + Log::info('オペレーターキュー追加成功', [ + 'user_id' => $waitingUser->user_id, + 'park_id' => $waitingUser->park_id, + 'reserve_id' => $waitingUser->reserve_id, + 'que_class' => 4, + 'operator_id' => 9999999, + 'batch_comment' => $batchComment, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'status_comment' => $statusComment + ]); + + return ['success' => true]; + + } catch (Exception $e) { + Log::error('オペレーターキュー追加エラー', [ + 'user_id' => $waitingUser->user_id, + 'park_id' => $waitingUser->park_id, + 'reserve_id' => $waitingUser->reserve_id ?? null, + 'batch_comment' => $batchComment, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } +} diff --git a/app/Services/ShjFourBService.php b/app/Services/ShjFourBService.php new file mode 100644 index 0000000..e9fea8a --- /dev/null +++ b/app/Services/ShjFourBService.php @@ -0,0 +1,1090 @@ +shjEightService = $shjEightService; + $this->mailSendService = $mailSendService; + } + + /** + * 決済トランザクション処理メイン実行 + * + * SHJ-4Bの3段階判断処理を実装: + * 【判断0】対象契約取得判定 + * 【判断1】授受状態チェック + * 【判断2】金額チェック + * + * @param int $settlementTransactionId 決済トランザクションID + * @param array $context 追加のコンテキスト情報 + * @return array 処理結果 + */ + public function processSettlementTransaction(int $settlementTransactionId, array $context = []): array + { + $startTime = now(); + + Log::info('SHJ-4B 決済トランザクション処理開始', [ + 'settlement_transaction_id' => $settlementTransactionId, + 'context' => $context, + 'start_time' => $startTime, + ]); + + try { + // 【前処理】決済トランザクション取得 + $settlement = $this->getSettlementTransaction($settlementTransactionId); + + // 【処理1】定期契約マスタの対象レコード取得 + // 【判断0】取得判定(登録済み判定を含む) + $contractResult = $this->judgeTargetContract($settlement); + + if (!$contractResult['found']) { + // 対象レコードなしの場合 + $result = $this->handleNoTargetRecord($settlement, $contractResult); + $this->createBatchLog($settlement, null, null, true, $result['message']); + return $result; + } + + if ($contractResult['already_processed']) { + // 登録済みの場合 + $result = $this->handleAlreadyProcessed($settlement, $contractResult); + $contract = $contractResult['contract']; + $this->createBatchLog($settlement, $contract, null, true, $result['message']); + return $result; + } + + $contract = $contractResult['contract']; + + // 【判断1】授受状態チェック + $statusResult = $this->judgeReceiptStatus($settlement, $contract); + + if (!$statusResult['valid']) { + // 授受状態が異常な場合 + $result = $this->handleInvalidStatus($settlement, $contract, $statusResult); + $this->createBatchLog($settlement, $contract, null, true, $result['message']); + return $result; + } + + // 【判断2】金額チェック + $amountResult = $this->judgeAmountComparison($settlement, $contract); + + // 【処理3】契約更新処理実行 + $updateResult = $this->executeContractUpdate($settlement, $contract, $amountResult); + + // 副作用処理実行 + $sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult); + + $result = [ + 'success' => true, + 'settlement_transaction_id' => $settlementTransactionId, + 'contract_id' => $contract->contract_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'amount_comparison' => $amountResult['comparison'], + 'contract_updated' => $updateResult['updated'], + 'side_effects' => $sideEffectResult, + 'execution_time' => now()->diffInSeconds($startTime), + ]; + + Log::info('SHJ-4B 決済トランザクション処理完了', $result); + + // 【処理6】バッチ処理ログ作成(SHJ-8呼び出し) + $mailCommentSuffix = $sideEffectResult['user_mail']['batch_comment_suffix'] ?? null; + $this->createBatchLog($settlement, $contract, $amountResult, true, null, $mailCommentSuffix); + + return $result; + + } catch (\Throwable $e) { + Log::error('SHJ-4B 決済トランザクション処理失敗', [ + 'settlement_transaction_id' => $settlementTransactionId, + 'context' => $context, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + // エラー時のバッチログ作成 + try { + $settlement = SettlementTransaction::find($settlementTransactionId); + if ($settlement) { + $this->createBatchLog($settlement, null, null, false, $e->getMessage()); + } + } catch (\Throwable $logError) { + Log::error('SHJ-4B バッチログ作成エラー', ['error' => $logError->getMessage()]); + } + + throw $e; + } + } + + /** + * 決済トランザクション取得 + * + * @param int $settlementTransactionId + * @return SettlementTransaction + * @throws \RuntimeException + */ + private function getSettlementTransaction(int $settlementTransactionId): SettlementTransaction + { + $settlement = SettlementTransaction::find($settlementTransactionId); + + if (!$settlement) { + throw new \RuntimeException("SettlementTransaction not found: {$settlementTransactionId}"); + } + + Log::info('SHJ-4B 決済トランザクション取得成功', [ + 'settlement_transaction_id' => $settlementTransactionId, + 'contract_payment_number' => $settlement->contract_payment_number, + 'settlement_amount' => $settlement->settlement_amount, + 'pay_date' => $settlement->pay_date, + ]); + + return $settlement; + } + + /** + * 【処理1】定期契約マスタの対象レコード取得 + * 【判断0】取得判定(登録済み判定を含む) + * + * @param SettlementTransaction $settlement + * @return array + */ + private function judgeTargetContract(SettlementTransaction $settlement): array + { + Log::info('SHJ-4B 処理1: 定期契約マスタの対象レコード取得開始', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_payment_number' => $settlement->contract_payment_number, + ]); + + // 文档要求のSQL構造に基づく対象レコード取得 + // regular_contract T1 inner join park T2 inner join price_a T4 + $contractQuery = DB::table('regular_contract as T1') + ->select([ + 'T1.contract_id', + 'T1.old_contract_id', + 'T1.park_id', + 'T1.user_id', + 'T1.contract_flag', + 'T1.billing_amount', + 'T4.price_ptypeid as ptype_id', + 'T1.psection_id', + 'T1.zone_id', + 'T1.update_flag', + 'T1.reserve_id', + 'T1.contract_payment_number', + 'T1.contract_payment_day', + 'T1.contract_periods', + 'T1.contract_periode', + 'T1.contract_created_at', + 'T1.contract_cancel_flag', + 'T2.park_name', + 'T4.price_month', + 'T4.price' + ]) + ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') + ->join('price_a as T4', function($join) { + $join->on('T1.price_parkplaceid', '=', 'T4.price_parkplaceid') + ->on('T1.park_id', '=', 'T4.park_id'); // 文档要求の第二条件追加 + }) + ->where('T1.contract_payment_number', $settlement->contract_payment_number) + ->where('T1.contract_cancel_flag', '!=', 1) // 解約されていない + ->whereNotNull('T1.contract_flag') // 状態が設定済み + ->first(); + + if (!$contractQuery) { + Log::warning('SHJ-4B 判断0: 対象レコードなし', [ + 'contract_payment_number' => $settlement->contract_payment_number, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + ]); + + return [ + 'found' => false, + 'contract' => null, + 'reason' => '対象レコードなし', + 'message' => "契約番号に一致する有効な定期契約が見つかりません: {$settlement->contract_payment_number}", + ]; + } + + // 登録済み判定 + $isAlreadyProcessed = $this->checkAlreadyProcessed($contractQuery, $settlement); + + if ($isAlreadyProcessed['processed']) { + Log::info('SHJ-4B 判断0: 登録済み検出', [ + 'contract_id' => $contractQuery->contract_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'reason' => $isAlreadyProcessed['reason'], + ]); + + return [ + 'found' => true, + 'contract' => $contractQuery, + 'already_processed' => true, + 'reason' => '登録済み', + 'message' => "この決済は既に処理済みです: " . $isAlreadyProcessed['reason'], + ]; + } + + Log::info('SHJ-4B 判断0: 対象契約取得成功', [ + 'contract_id' => $contractQuery->contract_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'billing_amount' => $contractQuery->billing_amount, + 'contract_flag' => $contractQuery->contract_flag, + 'park_name' => $contractQuery->park_name, + 'price_month' => $contractQuery->price_month, + ]); + + return [ + 'found' => true, + 'contract' => $contractQuery, + 'already_processed' => false, + 'reason' => '対象契約取得成功', + 'message' => "契約ID {$contractQuery->contract_id} を取得しました", + ]; + } + + /** + * 登録済み判定 + * + * 複数の条件で既に処理済みかを判定 + * + * @param object $contract + * @param SettlementTransaction $settlement + * @return array + */ + private function checkAlreadyProcessed($contract, SettlementTransaction $settlement): array + { + // 条件1: contract_payment_dayが既に設定済みで、今回の支払日以降 + if (!empty($contract->contract_payment_day)) { + $existingPaymentDate = Carbon::parse($contract->contract_payment_day); + $currentPaymentDate = Carbon::parse($settlement->pay_date); + + if ($existingPaymentDate->gte($currentPaymentDate)) { + return [ + 'processed' => true, + 'reason' => "既に支払日 {$existingPaymentDate->format('Y-m-d')} が設定済み", + ]; + } + } + + // 条件2: 同一の決済条件(contract_payment_number + pay_date + settlement_amount)が + // 既に他のsettlement_transactionで処理済み + $existingTransaction = SettlementTransaction::where('contract_payment_number', $settlement->contract_payment_number) + ->where('pay_date', $settlement->pay_date) + ->where('settlement_amount', $settlement->settlement_amount) + ->where('settlement_transaction_id', '!=', $settlement->settlement_transaction_id) + ->first(); + + if ($existingTransaction) { + return [ + 'processed' => true, + 'reason' => "同一条件の決済トランザクション {$existingTransaction->settlement_transaction_id} が既に存在", + ]; + } + + // 条件3: bat_job_logで同一決済の処理完了記録があるか + // status_commentに決済トランザクションIDが含まれているかチェック + $existingBatchLog = BatJobLog::where('process_name', 'SHJ-4B') + ->where('status', 'success') + ->where('status_comment', 'like', '%settlement_transaction_id:' . $settlement->settlement_transaction_id . '%') + ->exists(); + + if ($existingBatchLog) { + return [ + 'processed' => true, + 'reason' => "bat_job_logに処理完了記録が存在", + ]; + } + + return [ + 'processed' => false, + 'reason' => '未処理', + ]; + } + + /** + * 【判断1】授受状態チェック + * + * @param SettlementTransaction $settlement + * @param object $contract + * @return array + */ + private function judgeReceiptStatus(SettlementTransaction $settlement, $contract): array + { + // 授受状態の基本チェック + $statusChecks = [ + 'settlement_status' => $settlement->status === 'received', + 'pay_date_exists' => !empty($settlement->pay_date), + 'settlement_amount_valid' => $settlement->settlement_amount > 0, + 'contract_not_cancelled' => $contract->contract_cancel_flag != 1, + ]; + + $allValid = array_reduce($statusChecks, function($carry, $check) { + return $carry && $check; + }, true); + + if (!$allValid) { + $failedChecks = array_keys(array_filter($statusChecks, function($check) { + return !$check; + })); + + Log::warning('SHJ-4B 判断1: 授受状態異常', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'failed_checks' => $failedChecks, + 'status_checks' => $statusChecks, + ]); + + return [ + 'valid' => false, + 'reason' => '授受状態異常', + 'failed_checks' => $failedChecks, + 'message' => '決済トランザクションまたは契約の状態が更新処理に適していません', + ]; + } + + Log::info('SHJ-4B 判断1: 授受状態正常', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'status_checks' => $statusChecks, + ]); + + return [ + 'valid' => true, + 'reason' => '授受状態正常', + 'status_checks' => $statusChecks, + 'message' => '授受状態チェックに合格しました', + ]; + } + + /** + * 【判断2】金額チェック + * + * @param SettlementTransaction $settlement + * @param object $contract + * @return array + */ + private function judgeAmountComparison(SettlementTransaction $settlement, $contract): array + { + // 文档要求:請求額=授受額の厳密比較 + $billingAmount = (int) $contract->billing_amount; // 整数として比較 + $settlementAmount = (int) $settlement->settlement_amount; // 整数として比較 + + $difference = $settlementAmount - $billingAmount; + + if ($difference === 0) { + $comparison = self::AMOUNT_MATCH; + $result = '正常(金額一致)'; + } elseif ($difference < 0) { + $comparison = self::AMOUNT_SHORTAGE; + $result = '授受過少'; + } else { + $comparison = self::AMOUNT_EXCESS; + $result = '授受超過'; + } + + Log::info('SHJ-4B 判断2: 金額チェック完了', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'billing_amount' => $billingAmount, + 'settlement_amount' => $settlementAmount, + 'difference' => $difference, + 'comparison' => $comparison, + 'result' => $result, + ]); + + return [ + 'comparison' => $comparison, + 'result' => $result, + 'billing_amount' => $billingAmount, + 'settlement_amount' => $settlementAmount, + 'difference' => $difference, + 'message' => "請求額: {$billingAmount}円, 授受額: {$settlementAmount}円, 結果: {$result}", + ]; + } + + /** + * 登録済み処理 + * + * @param SettlementTransaction $settlement + * @param array $contractResult + * @return array + */ + private function handleAlreadyProcessed(SettlementTransaction $settlement, array $contractResult): array + { + Log::info('SHJ-4B 登録済み処理', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contractResult['contract']->contract_id, + 'reason' => $contractResult['reason'], + ]); + + return [ + 'success' => true, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contractResult['contract']->contract_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'result' => 'already_processed', + 'reason' => $contractResult['reason'], + 'message' => $contractResult['message'], + 'skipped' => true, + ]; + } + + /** + * パターンA/B判断 + * + * 月を跨らない(パターンA)vs 月を跨る(パターンB)の判定 + * + * @param object $contract + * @param SettlementTransaction $settlement + * @return array + */ + private function judgeContractPattern($contract, SettlementTransaction $settlement): array + { + $payDate = Carbon::parse($settlement->pay_date); + $contractStart = !empty($contract->contract_periods) ? Carbon::parse($contract->contract_periods) : null; + $contractEnd = !empty($contract->contract_periode) ? Carbon::parse($contract->contract_periode) : null; + + // パターン判定ロジック + $isPatternB = false; // デフォルトはパターンA + $patternReason = 'パターンA(月を跨らない)'; + + if ($contractEnd) { + // 支払日が契約終了月の翌月以降の場合、パターンB(跨月) + if ($payDate->month > $contractEnd->month || $payDate->year > $contractEnd->year) { + $isPatternB = true; + $patternReason = 'パターンB(月を跨る)'; + } + } + + Log::info('SHJ-4B パターン判定', [ + 'contract_id' => $contract->contract_id, + 'pay_date' => $payDate->format('Y-m-d'), + 'contract_periods' => $contractStart?->format('Y-m-d'), + 'contract_periode' => $contractEnd?->format('Y-m-d'), + 'pattern' => $isPatternB ? 'B' : 'A', + 'reason' => $patternReason, + ]); + + return [ + 'pattern' => $isPatternB ? 'B' : 'A', + 'is_pattern_b' => $isPatternB, + 'reason' => $patternReason, + 'pay_date' => $payDate, + 'contract_start' => $contractStart, + 'contract_end' => $contractEnd, + ]; + } + + /** + * 新規契約判定 + * + * @param object $contract + * @return bool + */ + private function isNewContract($contract): bool + { + if (empty($contract->contract_created_at)) { + return false; + } + + $createdAt = Carbon::parse($contract->contract_created_at); + $thirtyDaysAgo = Carbon::now()->subDays(30); + + // 作成から30日以内を新規とみなす(調整可能) + $isNew = $createdAt->gte($thirtyDaysAgo); + + Log::info('SHJ-4B 新規契約判定', [ + 'contract_id' => $contract->contract_id, + 'contract_created_at' => $createdAt->format('Y-m-d H:i:s'), + 'is_new' => $isNew, + 'days_since_created' => $createdAt->diffInDays(Carbon::now()), + ]); + + return $isNew; + } + + /** + * 【処理3】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新 + * + * @param SettlementTransaction $settlement + * @param object $contract + * @param array $amountResult + * @return array + */ + private function executeContractUpdate( + SettlementTransaction $settlement, + $contract, + array $amountResult + ): array { + $updateData = []; + $updated = false; + + try { + // パターンA/B判定 + $pattern = $this->judgeContractPattern($contract, $settlement); + + DB::transaction(function() use ($settlement, $contract, $amountResult, $pattern, &$updateData, &$updated) { + // 基本更新項目 + $updateData = [ + 'contract_payment_day' => Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'), + 'contract_updated_at' => now(), + ]; + + // 金額比較結果に基づく contract_flag 設定 + switch ($amountResult['comparison']) { + case self::AMOUNT_MATCH: + $updateData['contract_flag'] = self::CONTRACT_FLAG_UPDATED; + $updateData['contract_money'] = $settlement->settlement_amount; + break; + + case self::AMOUNT_SHORTAGE: + case self::AMOUNT_EXCESS: + $updateData['contract_flag'] = self::CONTRACT_FLAG_ERROR; + $updateData['contract_money'] = $settlement->settlement_amount; + break; + } + + // パターンBの場合の特殊処理 + if ($pattern['is_pattern_b']) { + // 契約期間の延長処理等 + if ($pattern['contract_end']) { + $newEndDate = $pattern['contract_end']->addMonth(); + $updateData['contract_periode'] = $newEndDate->format('Y-m-d'); + } + } + + // 【定期契約マスタ更新】 + $affectedRows = DB::table('regular_contract') + ->where('contract_id', $contract->contract_id) + ->update($updateData); + + $updated = $affectedRows > 0; + + // 【定期予約マスタ更新】(reserve_idが設定されている場合) + if (!empty($contract->reserve_id)) { + $reserveUpdateData = [ + 'updated_at' => now(), + ]; + + // 金額一致の場合、予約を有効化 + if ($amountResult['comparison'] === self::AMOUNT_MATCH) { + $reserveUpdateData['valid_flag'] = 1; + + // パターンBの場合、予約期間も延長 + if ($pattern['is_pattern_b'] && $pattern['contract_end']) { + $reserveUpdateData['reserve_end'] = $pattern['contract_end']->format('Y-m-d'); + } + } + + $reserveAffectedRows = DB::table('reserve') + ->where('reserve_id', $contract->reserve_id) + ->update($reserveUpdateData); + + Log::info('SHJ-4B 定期予約マスタ更新完了', [ + 'reserve_id' => $contract->reserve_id, + 'contract_id' => $contract->contract_id, + 'reserve_update_data' => $reserveUpdateData, + 'reserve_affected_rows' => $reserveAffectedRows, + ]); + } + + Log::info('SHJ-4B 定期契約マスタ更新完了', [ + 'contract_id' => $contract->contract_id, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'update_data' => $updateData, + 'affected_rows' => $affectedRows, + 'pattern' => $pattern['pattern'], + ]); + }); + + return [ + 'updated' => $updated, + 'update_data' => $updateData, + 'message' => $updated ? '契約更新に成功しました' : '契約更新対象が見つかりませんでした', + ]; + + } catch (\Throwable $e) { + Log::error('SHJ-4B 契約更新処理失敗', [ + 'contract_id' => $contract->contract_id, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + /** + * 副作用処理実行 + * + * 決済授受および写真削除、新規連動等の処理 + * + * @param SettlementTransaction $settlement + * @param object $contract + * @param array $amountResult + * @param array $updateResult + * @return array + */ + private function executeSideEffects( + SettlementTransaction $settlement, + $contract, + array $amountResult, + array $updateResult + ): array { + $sideEffects = []; + + try { + // 【処理3】写真削除処理(金額一致かつ更新成功の場合) + if ($amountResult['comparison'] === self::AMOUNT_MATCH && $updateResult['updated']) { + $sideEffects['photo_deletion'] = $this->executePhotoDeletion($contract); + } + + // 【新規のみ】SHJ-13実行処理 + if ($updateResult['updated'] && $amountResult['comparison'] === self::AMOUNT_MATCH) { + $isNewContract = $this->isNewContract($contract); + if ($isNewContract) { + $sideEffects['shj13_trigger'] = $this->triggerShjThirteen($contract); + } + } + + // 【処理4】異常時のオペレーターキュー登録処理 + if ($amountResult['comparison'] !== self::AMOUNT_MATCH) { + $sideEffects['operator_queue'] = $this->registerToOperatorQueue($settlement, $contract, $amountResult); + } + + // 【処理5】利用者メール送信処理 + if ($updateResult['updated']) { + $sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult); + } + + Log::info('SHJ-4B 副作用処理完了', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'side_effects' => array_keys($sideEffects), + ]); + + return $sideEffects; + + } catch (\Throwable $e) { + Log::error('SHJ-4B 副作用処理失敗', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'error' => $e->getMessage(), + ]); + + // 副作用処理の失敗はメイン処理を止めない + return ['error' => $e->getMessage()]; + } + } + + /** + * 対象レコードなしの場合の処理 + * + * @param SettlementTransaction $settlement + * @param array $contractResult + * @return array + */ + private function handleNoTargetRecord(SettlementTransaction $settlement, array $contractResult): array + { + Log::warning('SHJ-4B 対象レコードなし処理', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'reason' => $contractResult['reason'], + ]); + + // TODO: 必要に応じて管理者通知やオペレーターキューへの登録 + + return [ + 'success' => true, // エラーではなく、正常な結果として扱う + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_payment_number' => $settlement->contract_payment_number, + 'result' => 'no_target', + 'reason' => $contractResult['reason'], + 'message' => $contractResult['message'], + 'action_required' => '管理者による手動確認が必要です', + ]; + } + + /** + * 授受状態異常の場合の処理 + * + * @param SettlementTransaction $settlement + * @param RegularContract $contract + * @param array $statusResult + * @return array + */ + private function handleInvalidStatus( + SettlementTransaction $settlement, + $contract, + array $statusResult + ): array { + Log::warning('SHJ-4B 授受状態異常処理', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'reason' => $statusResult['reason'], + 'failed_checks' => $statusResult['failed_checks'], + ]); + + // TODO: オペレーターキューへの登録や管理者通知 + + return [ + 'success' => false, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'result' => 'invalid_status', + 'reason' => $statusResult['reason'], + 'failed_checks' => $statusResult['failed_checks'], + 'message' => $statusResult['message'], + 'action_required' => 'オペレーターによる手動処理が必要です', + ]; + } + + /** + * 写真削除処理 + * + * @param object $contract + * @return array + */ + private function executePhotoDeletion($contract): array + { + // TODO: 実際の写真削除ロジックを実装 + // 現在はプレースホルダー + + Log::info('SHJ-4B 写真削除処理実行', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + ]); + + return [ + 'executed' => true, + 'method' => 'placeholder', + 'message' => '写真削除処理は実装予定です', + ]; + } + + /** + * SHJ-13実行処理(新規のみ) + * + * ShjThirteenServiceを使用した契約台数追加処理 + * + * @param object $contract + * @return array + */ + private function triggerShjThirteen($contract): array + { + Log::info('SHJ-4B SHJ-13実行処理', [ + 'contract_id' => $contract->contract_id, + 'park_id' => $contract->park_id, + 'psection_id' => $contract->psection_id, + 'ptype_id' => $contract->ptype_id, + 'zone_id' => $contract->zone_id, + ]); + + try { + // 契約データ準備 + $contractData = [ + 'contract_id' => $contract->contract_id, + 'park_id' => $contract->park_id, + 'psection_id' => $contract->psection_id, + 'ptype_id' => $contract->ptype_id, + 'zone_id' => $contract->zone_id, + ]; + + // ShjThirteenService実行 + $shjThirteenService = app(ShjThirteenService::class); + $result = $shjThirteenService->execute($contractData); + + Log::info('SHJ-4B SHJ-13実行完了', [ + 'contract_id' => $contract->contract_id, + 'result' => $result, + ]); + + return $result; + + } catch (\Throwable $e) { + Log::error('SHJ-4B SHJ-13実行エラー', [ + 'contract_id' => $contract->contract_id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return [ + 'result' => 1, + 'error_code' => $e->getCode() ?: 1999, + 'error_message' => $e->getMessage(), + 'stack_trace' => $e->getTraceAsString(), + ]; + } + } + + /** + * 【処理5】利用者メール送信処理 + * + * SHJ-4B仕様準拠: + * 1. 利用者マスタよりメールアドレス、予備メールアドレスを取得 + * 2. SHJ-7メール送信を呼び出し(使用プログラムID: 205) + * 3. 処理結果判定: + * - result = 0 (正常): バッチコメントに "/メール正常終了件数:1" を追加 + * - その他: バッチコメントに "/メール異常終了件数:1、" + error_info を追加 + * + * @param SettlementTransaction $settlement + * @param object $contract + * @param array $amountResult + * @return array 処理結果 ['success' => bool, 'mail_status' => string, 'batch_comment_suffix' => string] + */ + private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, array $amountResult): array + { + try { + // 【処理5】利用者マスタよりメールアドレス、予備メールアドレスを取得する + $user = User::select('user_name', 'user_primemail', 'user_submail') + ->where('user_seq', $contract->user_id) + ->first(); + + if (!$user) { + Log::error('SHJ-4B 利用者メール送信処理: 利用者情報取得失敗', [ + 'user_id' => $contract->user_id, + 'contract_id' => $contract->contract_id, + ]); + + return [ + 'success' => false, + 'mail_status' => 'user_not_found', + 'batch_comment_suffix' => '/メール異常終了件数:1、利用者情報取得失敗' + ]; + } + + $mailAddress = $user->user_primemail ?? ''; + $backupMailAddress = $user->user_submail ?? ''; + + Log::info('SHJ-4B 利用者メール送信処理開始', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'user_name' => $user->user_name, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'mail_address' => $mailAddress, + 'amount_comparison' => $amountResult['comparison'], + ]); + + // 共通処理「SHJ-7メール送信」を呼び出し + // 使用プログラムID: 205(仕様書準拠) + $mailResult = $this->mailSendService->executeMailSend( + $mailAddress, + $backupMailAddress, + 205 // SHJ-4B仕様: 使用プログラムID = 205 + ); + + // SHJ-7仕様準拠: result === 0 が正常、それ以外は異常 + if (($mailResult['result'] ?? 1) === 0) { + // 【正常終了】バッチコメントに "/メール正常終了件数:1" を設定 + Log::info('SHJ-4B 利用者メール送信成功', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'mail_address' => $mailAddress, + ]); + + return [ + 'success' => true, + 'mail_status' => 'sent', + 'batch_comment_suffix' => '/メール正常終了件数:1' + ]; + } else { + // 【異常終了】バッチコメントに "/メール異常終了件数:1、" + error_info を設定 + $errorInfo = $mailResult['error_info'] ?? 'メール送信失敗'; + + Log::error('SHJ-4B 利用者メール送信失敗', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'mail_address' => $mailAddress, + 'error_info' => $errorInfo, + ]); + + return [ + 'success' => false, + 'mail_status' => 'failed', + 'batch_comment_suffix' => "/メール異常終了件数:1、{$errorInfo}" + ]; + } + + } catch (\Exception $e) { + Log::error('SHJ-4B 利用者メール送信処理例外エラー', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return [ + 'success' => false, + 'mail_status' => 'exception', + 'batch_comment_suffix' => '/メール異常終了件数:1、システムエラー: ' . $e->getMessage() + ]; + } + } + + /** + * オペレーターキューへの登録 + * + * @param SettlementTransaction $settlement + * @param object $contract + * @param array $amountResult + * @return array + */ + private function registerToOperatorQueue( + SettlementTransaction $settlement, + $contract, + array $amountResult + ): array { + // TODO: OperatorQue モデルを使用したキューへの登録処理を実装 + + Log::info('SHJ-4B オペレーターキュー登録処理実行', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_id' => $contract->contract_id, + 'amount_comparison' => $amountResult['comparison'], + 'difference' => $amountResult['difference'], + ]); + + return [ + 'registered' => true, + 'method' => 'placeholder', + 'message' => 'オペレーターキュー登録処理は実装予定です', + ]; + } + + /** + * 【処理6】バッチ処理ログ作成 + * + * SHJ-8サービスを呼び出してbat_job_logに記録 + * + * @param SettlementTransaction $settlement + * @param object|null $contract + * @param array|null $amountResult + * @param bool $isSuccess + * @param string|null $errorMessage + * @param string|null $mailCommentSuffix メール送信結果コメント(例: "/メール正常終了件数:1" or "/メール異常終了件数:1、{error_info}") + * @return void + */ + private function createBatchLog( + SettlementTransaction $settlement, + $contract = null, + ?array $amountResult = null, + bool $isSuccess = true, + ?string $errorMessage = null, + ?string $mailCommentSuffix = null + ): void { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + // ステータスコメント生成(内部変数.バッチコメント) + if ($errorMessage) { + // エラー時 + $statusComment = "支払いステータスチェック:エラー(決済トランザクションID:{$settlement->settlement_transaction_id}) - {$errorMessage}"; + } elseif ($amountResult) { + // 正常処理時 + $contractId = $contract ? $contract->contract_id : 'N/A'; + + switch ($amountResult['comparison']) { + case self::AMOUNT_MATCH: + $statusComment = "支払いステータスチェック:OK(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、金額一致)"; + break; + case self::AMOUNT_SHORTAGE: + $statusComment = "支払いステータスチェック:請求金額より授受金額が少ないです(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、差額:{$amountResult['difference']}円)"; + break; + case self::AMOUNT_EXCESS: + $statusComment = "支払いステータスチェック:請求金額より授受金額が多いです(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、差額:{$amountResult['difference']}円)"; + break; + default: + $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId})"; + } + } else { + // その他のケース(対象なし、登録済み等) + $contractId = $contract ? $contract->contract_id : 'N/A'; + $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId})"; + } + + // メール送信結果をバッチコメントに追加 + if ($mailCommentSuffix) { + $statusComment .= $mailCommentSuffix; + } + + // SHJ-8サービス呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-4B', + 'SHJ-4支払いステータスチェック', + 'success', + $statusComment, + $today, + $today + ); + + Log::info('SHJ-4B バッチ処理ログ作成完了', [ + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'status_comment' => $statusComment, + ]); + + } catch (\Exception $e) { + Log::error('SHJ-4B バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + ]); + } + } +} diff --git a/app/Services/ShjFourCService.php b/app/Services/ShjFourCService.php new file mode 100644 index 0000000..4cf522c --- /dev/null +++ b/app/Services/ShjFourCService.php @@ -0,0 +1,418 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->shjEightService = $shjEightService; + } + + /** + * SHJ-4C 室割当処理メイン実行 + * + * 処理フロー: + * 【処理1】ゾーン情報取得 + * 【判断1】割当判定 + * 【処理2】バッチログ作成 + * 【処理3】処理結果返却 + * + * @param int $parkId 駐輪場ID + * @param int $ptypeId 駐輪分類ID + * @param int $psectionId 車種区分ID + * @return array 処理結果 + */ + public function executeRoomAllocation(int $parkId, int $ptypeId, int $psectionId): array + { + $statusComment = ''; + $status = 'success'; + + try { + Log::info('SHJ-4C 室割当処理開始', [ + 'park_id' => $parkId, + 'ptype_id' => $ptypeId, + 'psection_id' => $psectionId + ]); + + // 【処理1】ゾーン情報取得 + $zoneInfo = $this->getZoneInformation($parkId, $ptypeId, $psectionId); + + if (empty($zoneInfo)) { + $message = '対象のゾーン情報が見つかりません'; + $status = 'error'; + $statusComment = sprintf('エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)', + $message, $parkId, $ptypeId, $psectionId); + + // バッチログ作成 + $this->createBatchLog($status, $statusComment); + + // JOB3: ゾーンID, 車室番号, 異常情報を返却 + return [ + 'success' => false, + 'zone_id' => null, + 'pplace_no' => null, + 'error_info' => $message + ]; + } + + // 【判断1】割当判定処理 + $allocationResult = $this->performAllocationJudgment($zoneInfo, $parkId, $ptypeId, $psectionId); + + if (!$allocationResult['can_allocate']) { + // 割当NGの場合、対象事室番号を設定 + $this->setTargetRoomNumber($allocationResult['target_room_number']); + + $status = 'warning'; + $statusComment = sprintf('割当NG: %s (park_id:%d, ptype_id:%d, psection_id:%d)', + $allocationResult['reason'], $parkId, $ptypeId, $psectionId); + + // バッチログ作成 + $this->createBatchLog($status, $statusComment); + + // JOB3: ゾーンID, 車室番号, 異常情報を返却(割当NG = 空き車室なし) + return [ + 'success' => true, + 'zone_id' => null, + 'pplace_no' => null, + 'error_info' => $allocationResult['reason'] + ]; + } + + // 【処理2】バッチログ作成 + $statusComment = sprintf('室割当処理完了 (park_id:%d, ptype_id:%d, psection_id:%d, zone_id:%d, pplace_no:%d)', + $parkId, $ptypeId, $psectionId, $allocationResult['zone_id'], $allocationResult['pplace_no']); + + $this->createBatchLog($status, $statusComment); + + Log::info('SHJ-4C 室割当処理完了', [ + 'zone_id' => $allocationResult['zone_id'], + 'pplace_no' => $allocationResult['pplace_no'] + ]); + + // 【処理3】処理結果返却 + // JOB3: ゾーンID, 車室番号, 異常情報を返却 + return [ + 'success' => true, + 'message' => 'SHJ-4C 室割当処理が正常に完了しました', + 'zone_id' => $allocationResult['zone_id'], + 'pplace_no' => $allocationResult['pplace_no'], + 'error_info' => null + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-4C 室割当処理でエラーが発生: ' . $e->getMessage(); + $status = 'error'; + $statusComment = sprintf('例外エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)', + $e->getMessage(), $parkId, $ptypeId, $psectionId); + + // バッチログ作成 + $this->createBatchLog($status, $statusComment); + + Log::error('SHJ-4C 室割当処理エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // JOB3: ゾーンID, 車室番号, 異常情報を返却 + return [ + 'success' => false, + 'zone_id' => null, + 'pplace_no' => null, + 'error_info' => $errorMessage + ]; + } + } + + /** + * SHJ-8バッチ処理ログ作成 + * + * @param string $status ステータス + * @param string $statusComment ステータスコメント + * @return void + */ + private function createBatchLog(string $status, string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + $today = now()->format('Y/m/d'); + + Log::info('SHJ-8バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => 'SHJ-4C', + 'job_name' => 'SHJ-4C室割当', + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-4C', + 'SHJ-4C室割当', + $status, + $statusComment, + $today, + $today + ); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチログ作成エラー', [ + 'error' => $e->getMessage() + ]); + } + } + + /** + * 【処理1】ゾーン情報取得 + * + * 駐輪場ID、駐輪分類ID、車種区分IDに紐づくゾーン情報を取得する + * SQLクエリは設計書の仕様に基づく + * + * @param int $parkId 駐輪場ID + * @param int $ptypeId 駐輪分類ID + * @param int $psectionId 車種区分ID + * @return array ゾーン情報 + */ + private function getZoneInformation(int $parkId, int $ptypeId, int $psectionId): array + { + try { + // 設計書に記載されたSQLクエリに基づくゾーン情報取得 + $zoneInfo = DB::table('zone as T1') + ->select([ + 'T1.zone_id', + 'T1.zone_name', + 'T1.zone_number as zone_number', + 'T1.zone_standard as zone_standard', + 'T1.zone_tolerance as zone_tolerance', + 'T1.zone_sort', + 'T2.update_grace_period_start_date', + 'T2.update_grace_period_end_date' + ]) + ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') + ->where('T1.park_id', $parkId) + ->where('T1.ptype_id', $ptypeId) + ->where('T1.psection_id', $psectionId) + ->where('T1.delete_flag', 0) + ->orderBy('T1.zone_sort') + ->get() + ->toArray(); + + Log::info('ゾーン情報取得完了', [ + 'park_id' => $parkId, + 'ptype_id' => $ptypeId, + 'psection_id' => $psectionId, + 'zone_count' => count($zoneInfo) + ]); + + return $zoneInfo; + + } catch (\Exception $e) { + Log::error('ゾーン情報取得エラー', [ + 'park_id' => $parkId, + 'ptype_id' => $ptypeId, + 'psection_id' => $psectionId, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【判断1】割当判定処理 + * + * ゾーンIDごとにわか順(インクリメント、内部変数.対象番号とする)に + * ゾーン内標準台数に達するまで、定期契約マスタから該当する車室番号で + * 契約済みの情報の有無を確認する + * + * @param array $zoneInfo ゾーン情報 + * @param int $parkId 駐輪場ID + * @param int $ptypeId 駐輪分類ID + * @param int $psectionId 車種区分ID + * @return array 割当判定結果 + */ + private function performAllocationJudgment(array $zoneInfo, int $parkId, int $ptypeId, int $psectionId): array + { + try { + foreach ($zoneInfo as $zone) { + // ゾーン内標準台数まで車室番号をインクリメントして検索 + for ($pplaceNo = 1; $pplaceNo <= $zone->zone_standard; $pplaceNo++) { + // 該当する車室番号で契約済みの情報の有無を確認 + $contractInfo = $this->getRegularContractInfo( + $parkId, + $zone->zone_id, + $pplaceNo, + (int) $zone->update_grace_period_start_date, + (int) $zone->update_grace_period_end_date + ); + + // 契約情報が存在しない場合、この車室番号は空き + if ($contractInfo === null) { + Log::info('SHJ-4C 割当OK', [ + 'zone_id' => $zone->zone_id, + 'zone_name' => $zone->zone_name, + 'pplace_no' => $pplaceNo, + ]); + + return [ + 'can_allocate' => true, + 'zone_id' => $zone->zone_id, + 'zone_name' => $zone->zone_name, + 'pplace_no' => $pplaceNo, + 'reason' => "割当OK: ゾーンID {$zone->zone_id}, 車室番号 {$pplaceNo}" + ]; + } + } + + Log::info('SHJ-4C ゾーン満杯', [ + 'zone_id' => $zone->zone_id, + 'zone_name' => $zone->zone_name, + ]); + } + + // 全ゾーンで割当NGの場合 + $targetRoomNumber = $this->generateTargetRoomNumber($parkId, $ptypeId, $psectionId); + + Log::warning('SHJ-4C 割当NG', [ + 'target_room_number' => $targetRoomNumber, + ]); + + return [ + 'can_allocate' => false, + 'target_room_number' => $targetRoomNumber, + 'reason' => "車室割り当てNG: " . $targetRoomNumber + ]; + + } catch (\Exception $e) { + Log::error('割当判定処理エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 定期契約情報取得 + * + * 設計書のSQLクエリに基づく定期契約マスタ検索 + * 特定の車室番号で契約済みの情報の有無を確認する + * + * @param int $parkId 駐輪場ID + * @param int $zoneId ゾーンID + * @param int $pplaceNo 車室番号(対象番号) + * @param int $updateGracePeriodStartDate 更新期間開始日 + * @param int $updateGracePeriodEndDate 更新期間終了日 + * @return array|null 定期契約情報(存在しない場合はnull) + */ + private function getRegularContractInfo( + int $parkId, + int $zoneId, + int $pplaceNo, + int $updateGracePeriodStartDate, + int $updateGracePeriodEndDate + ): ?array { + $currentDate = Carbon::now()->format('y.%m.%d'); + + $query = DB::table('regular_contract as T1') + ->select(['T1.contract_id']) + ->where('T1.park_id', $parkId) + ->where('T1.zone_id', $zoneId) + ->where('T1.pplace_no', $pplaceNo); + + // 【「JOB1.更新期間開始日」<=「JOB1. 更新期間終了日」の場合】 + if ($updateGracePeriodStartDate <= $updateGracePeriodEndDate) { + // パターンA: T1.有効期間E >= date_format(now(), '%y.%m.%d') + $query->whereRaw("date_format(T1.contract_periode, '%y.%m.%d') >= ?", [$currentDate]); + } else { + // 【その他の場合】 + // パターンB: T1.有効期間E + 「JOB1. 更新期間終了日」>= date_format(now(), '%y.%m.%d') + $query->whereRaw("DATE_ADD(T1.contract_periode, INTERVAL ? DAY) >= ?", [$updateGracePeriodEndDate, $currentDate]); + } + + $query->where('T1.contract_flag', 1); + + $result = $query->first(); + + return $result ? (array) $result : null; + } + + /** + * 対象事室番号生成 + * + * @param int $parkId 駐輪場ID + * @param int $ptypeId 駐輪分類ID + * @param int $psectionId 車種区分ID + * @return string 対象事室番号 + */ + private function generateTargetRoomNumber(int $parkId, int $ptypeId, int $psectionId): string + { + return sprintf('%d_%d_%d', $parkId, $ptypeId, $psectionId); + } + + /** + * 対象事室番号設定 + * + * @param string $targetRoomNumber 対象事室番号 + * @return void + */ + private function setTargetRoomNumber(string $targetRoomNumber): void + { + Log::info('対象事室番号設定', [ + 'target_room_number' => $targetRoomNumber + ]); + + // 実際の事室番号設定ロジックをここに実装 + // 具体的な仕様が必要な場合は後で追加実装 + } + +} \ No newline at end of file diff --git a/app/Services/ShjMailSendService.php b/app/Services/ShjMailSendService.php new file mode 100644 index 0000000..8da047f --- /dev/null +++ b/app/Services/ShjMailSendService.php @@ -0,0 +1,666 @@ +mailTemplateModel = $mailTemplateModel; + } + + /** + * SHJ-7 メール送信処理メイン実行 + * + * 処理フロー: + * 【処理1】入力パラメーターをチェックする + * 【判断1】チェック結果 + * 【処理2】メール送信テンプレート情報を取得する + * 【判断2】取得結果判定 + * 【処理3】メールを送信する + * 【処理4】処理結果を返却する + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param int $mailTemplateId メールテンプレートID(使用プログラムID) + * @param array $additionalParams 追加パラメータ(reserve_id, expiryなど) + * @return array 処理結果 ['result' => 0|1, 'error_info' => string] + */ + public function executeMailSend(string $mailAddress, string $backupMailAddress, int $mailTemplateId, array $additionalParams = []): array + { + try { + Log::info('SHJ-7 メール送信処理開始', [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId, + 'additional_params' => $additionalParams + ]); + + // 【処理1】入力パラメーターをチェックする + $paramCheckResult = $this->checkInputParameters($mailAddress, $backupMailAddress, $mailTemplateId); + + // 【判断1】チェック結果 + if (!$paramCheckResult['valid']) { + $errorInfo = $paramCheckResult['error_info']; + + Log::warning('SHJ-7 パラメーターチェックNG', [ + 'error_info' => $errorInfo + ]); + + // 【処理4】処理結果を返却する(異常終了) + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + // 【処理2】メール送信テンプレート情報を取得する + $templateInfo = $this->getMailTemplateInfo($mailTemplateId); + + // 【判断2】取得結果判定 + if (empty($templateInfo)) { + // 0件の場合:取得NGの結果を設定する + $errorInfo = "メール送信NG:メール送信テンプレートが存在しません。/{$mailTemplateId}"; + + Log::warning('SHJ-7 メールテンプレート取得NG', [ + 'mail_template_id' => $mailTemplateId, + 'error_info' => $errorInfo + ]); + + // 【処理4】処理結果を返却する(異常終了) + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + // 【処理3】メールを送信する + $mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo); + + if (!$mailSendResult['success']) { + $errorInfo = $mailSendResult['error_info']; + + Log::error('SHJ-7 メール送信失敗', [ + 'error_info' => $errorInfo + ]); + + // 【処理4】処理結果を返却する(異常終了) + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + Log::info('SHJ-7 メール送信処理完了', [ + 'to_address' => $mailSendResult['to_address'] + ]); + + // 【処理4】処理結果を返却する(正常終了) + return [ + 'result' => 0, + 'error_info' => '' + ]; + + } catch (\Exception $e) { + $errorInfo = 'メール送信NG:' . $e->getMessage(); + + Log::error('SHJ-7 メール送信処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // 【処理4】処理結果を返却する(異常終了) + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + } + + /** + * 【処理1】入力パラメーターをチェックする + * + * SHJ-7仕様書に基づくチェック内容: + * 1. メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須 + * 2. 予備メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須 + * 3. 使用プログラムID: 必須 + * + * エラーメッセージ形式: "パラメーターNG: 項目名/入力値" + * 複数エラーの場合は全角カンマ(、)で連結 + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param int $mailTemplateId メールテンプレートID(使用プログラムID) + * @return array チェック結果 ['valid' => bool, 'error_info' => string] + */ + private function checkInputParameters(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array + { + $errors = []; + + try { + // 1. メールアドレスと予備メールアドレスのいずれか必須チェック + // 両方とも空の場合は、それぞれ別エラーとして追加 + if (empty($mailAddress) && empty($backupMailAddress)) { + $errors[] = "パラメーターNG:メールアドレス/<空>"; + $errors[] = "パラメーターNG:予備メールアドレス/<空>"; + } else { + // いずれかに値がある場合は、形式チェックを実施 + // 2. メールアドレス形式チェック(値がある場合のみ) + if (!empty($mailAddress) && !filter_var($mailAddress, FILTER_VALIDATE_EMAIL)) { + $errors[] = "パラメーターNG:メールアドレス/{$mailAddress}"; + } + + // 3. 予備メールアドレス形式チェック(値がある場合のみ) + if (!empty($backupMailAddress) && !filter_var($backupMailAddress, FILTER_VALIDATE_EMAIL)) { + $errors[] = "パラメーターNG:予備メールアドレス/{$backupMailAddress}"; + } + } + + // 4. 使用プログラムID(メールテンプレートID)必須チェック + if (empty($mailTemplateId) || $mailTemplateId <= 0) { + $errors[] = "パラメーターNG:使用プログラムID/{$mailTemplateId}"; + } + + // エラーがある場合 + if (!empty($errors)) { + // 複数のエラーがある場合は全角カンマ(、)で連結 + $errorInfo = implode('、', $errors); + + Log::warning('SHJ-7 入力パラメーターチェックNG', [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId, + 'error_info' => $errorInfo + ]); + + return [ + 'valid' => false, + 'error_info' => $errorInfo + ]; + } + + // 正常終了 + Log::info('SHJ-7 入力パラメーターチェックOK', [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ]); + + return [ + 'valid' => true, + 'error_info' => '' + ]; + + } catch (\Exception $e) { + $errorInfo = 'パラメーターチェック中にエラーが発生:' . $e->getMessage(); + + Log::error('SHJ-7 入力パラメーターチェック例外エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'valid' => false, + 'error_info' => $errorInfo + ]; + } + } + + /** + * 【処理2】メール送信テンプレート情報を取得する + * + * SHJ-7仕様書に基づくSQLクエリ: + * SELECT エリアマネージャー同報, bccアドレス, 件名, 本文 + * FROM メール送信テンプレート + * WHERE 使用プログラムID = 入力パラメーター.使用プログラムID + * AND 使用フラグ = 1 + * + * @param int $mailTemplateId メールテンプレートID(使用プログラムID) + * @return MailTemplate|null メールテンプレート情報 + */ + private function getMailTemplateInfo(int $mailTemplateId): ?MailTemplate + { + try { + // SHJ-7仕様: 使用プログラムID(pg_id)で検索、使用フラグ=1 + $templateInfo = $this->mailTemplateModel::where('pg_id', $mailTemplateId) + ->where('use_flag', 1) + ->first(); + + if ($templateInfo) { + Log::info('SHJ-7 メールテンプレート情報取得完了', [ + 'mail_template_id' => $mailTemplateId, + 'template_found' => true, + 'subject' => $templateInfo->getSubject(), + 'mgr_cc_flag' => $templateInfo->isManagerCcEnabled(), + 'has_bcc' => !empty($templateInfo->getBccAddress()) + ]); + } else { + Log::warning('SHJ-7 メールテンプレート情報取得結果0件', [ + 'mail_template_id' => $mailTemplateId, + 'template_found' => false + ]); + } + + return $templateInfo; + + } catch (\Exception $e) { + Log::error('SHJ-7 メールテンプレート情報取得エラー', [ + 'mail_template_id' => $mailTemplateId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + /** + * 【処理3】メールを送信する + * + * SHJ-7仕様書に基づくmb_send_mail関数使用: + * - 送信先: 入力パラメーター.メールアドレス(空なら予備メールアドレス) + * - タイトル: 処理2.件名 + * - 本文: 処理2.本文 + * - 追加ヘッダ: 処理2.bccアドレス(※値が設定されている場合のみ) + * + * ※待ち時間: 仕様では「一定の待ち時間を設ける」とあるが、現時点では実装しない(0秒) + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param MailTemplate $templateInfo テンプレート情報 + * @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string] + */ + private function sendMail(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array + { + try { + // 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス) + $toAddress = !empty($mailAddress) ? $mailAddress : $backupMailAddress; + + // 処理2で取得したメール内容を取得 + $subject = $templateInfo->getSubject() ?? ''; + $message = $templateInfo->getText() ?? ''; + + // 追加ヘッダ設定(BCC、From等) + $headers = $this->buildMailHeaders($templateInfo); + + Log::info('SHJ-7 メール送信準備完了', [ + 'to_address' => $toAddress, + 'subject' => $subject, + 'has_bcc' => !empty($templateInfo->getBccAddress()), + 'mgr_cc_flag' => $templateInfo->isManagerCcEnabled() + ]); + + // mb_send_mail関数を使用してメール送信 + $sendResult = mb_send_mail( + $toAddress, // 送信先 + $subject, // 件名(タイトル) + $message, // 本文 + $headers // 追加ヘッダ + ); + + if ($sendResult) { + Log::info('SHJ-7 メール送信成功', [ + 'to_address' => $toAddress, + 'subject' => $subject + ]); + + return [ + 'success' => true, + 'error_info' => '', + 'to_address' => $toAddress + ]; + } else { + // mb_send_mail がfalseを返した場合 + $errorInfo = 'メール送信NG:mb_send_mail関数がfalseを返しました'; + Log::error('SHJ-7 メール送信失敗', [ + 'to_address' => $toAddress, + 'subject' => $subject, + 'error_info' => $errorInfo + ]); + + return [ + 'success' => false, + 'error_info' => $errorInfo + ]; + } + + } catch (\Exception $e) { + $errorInfo = 'メール送信NG:' . $e->getMessage(); + Log::error('SHJ-7 メール送信例外エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error_info' => $errorInfo + ]; + } + } + + /** + * メールヘッダを構築 + * + * SHJ-7仕様書に基づく追加ヘッダ設定: + * - 追加ヘッダ: 処理2.bccアドレス(※値が設定されている場合のみ) + * - From: システム設定から取得 + * - エリアマネージャー同報: フラグが有効な場合はログ出力のみ(実装保留) + * + * @param MailTemplate $templateInfo テンプレート情報 + * @return string メールヘッダ + */ + private function buildMailHeaders(MailTemplate $templateInfo): string + { + $headers = []; + + // BCCアドレス設定(値が設定されている場合のみ追加) + $bccAddress = $templateInfo->getBccAddress(); + if (!empty($bccAddress)) { + $headers[] = "Bcc: {$bccAddress}"; + } + + // エリアマネージャー同報フラグが有効な場合 + // ※SHJ-7では実装しない(仕様詳細不明)。ログ出力のみ。 + if ($templateInfo->isManagerCcEnabled()) { + Log::info('SHJ-7 エリアマネージャー同報フラグが有効', [ + 'mail_template_id' => $templateInfo->mail_template_id + ]); + // 実際のエリアマネージャーアドレス取得・設定処理は将来実装 + } + + // From設定(システム設定から取得) + $fromAddress = config('mail.from.address', 'noreply@so-manager.com'); + $headers[] = "From: {$fromAddress}"; + + // Content-Type設定(日本語対応) + $headers[] = "Content-Type: text/plain; charset=UTF-8"; + + return implode("\r\n", $headers); + } + + + /** + * SHJ-12 未払い者通知メール送信 + * + * SHJ-12から呼び出される専用メール送信メソッド + * 未払い者への通知メールを送信する + * + * @param array $mailParams メール送信パラメータ + * @return array 送信結果 + */ + public function sendUnpaidNotificationMail(array $mailParams): array + { + try { + // パラメータ展開 + $toEmail = $mailParams['to_email'] ?? ''; + $userName = $mailParams['user_name'] ?? ''; + $parkName = $mailParams['park_name'] ?? ''; + $billingAmount = $mailParams['billing_amount'] ?? 0; + $contractId = $mailParams['contract_id'] ?? ''; + + // 未払い者通知専用のメールテンプレートID(仮定: 1) + // 実際の運用では設定ファイルまたはデータベースから取得 + $mailTemplateId = 1; + + // メールテンプレート情報取得 + $templateInfo = $this->getMailTemplateInfo($mailTemplateId); + + if (!$templateInfo) { + return [ + 'success' => false, + 'message' => '未払い者通知用メールテンプレートが見つかりません' + ]; + } + + // メール件名とメッセージのカスタマイズ + $subject = str_replace( + ['{park_name}', '{user_name}'], + [$parkName, $userName], + $templateInfo->getSubject() + ); + + $message = str_replace( + ['{user_name}', '{park_name}', '{billing_amount}', '{contract_id}'], + [$userName, $parkName, number_format($billingAmount), $contractId], + $templateInfo->getText() + ); + + // 追加ヘッダ設定 + $headers = $this->buildMailHeaders($templateInfo); + + Log::info('SHJ-12 未払い者通知メール送信準備完了', [ + 'to_email' => $toEmail, + 'user_name' => $userName, + 'billing_amount' => $billingAmount, + 'contract_id' => $contractId + ]); + + // メール送信実行 + $sendResult = mb_send_mail( + $toEmail, // 送信先 + $subject, // 件名 + $message, // 本文 + $headers // 追加ヘッダ + ); + + if ($sendResult) { + Log::info('SHJ-12 未払い者通知メール送信成功', [ + 'to_email' => $toEmail, + 'contract_id' => $contractId + ]); + + return [ + 'success' => true, + 'message' => '未払い者通知メール送信完了' + ]; + } else { + $errorMessage = '未払い者通知メール送信に失敗しました'; + Log::error('SHJ-12 未払い者通知メール送信失敗', [ + 'to_email' => $toEmail, + 'contract_id' => $contractId, + 'error' => $errorMessage + ]); + + return [ + 'success' => false, + 'message' => $errorMessage + ]; + } + + } catch (\Exception $e) { + $errorMessage = 'SHJ-12 未払い者通知メール送信エラー: ' . $e->getMessage(); + Log::error('SHJ-12 未払い者通知メール送信エラー', [ + 'error' => $e->getMessage(), + 'mail_params' => $mailParams + ]); + + return [ + 'success' => false, + 'message' => $errorMessage + ]; + } + } + + /** + * Laravel Mail を使用したメール送信(開発・テスト用) + * + * mb_send_mail() の代わりに Laravel の Mail ファサードを使用 + * .env の MAIL_MAILER 設定(smtp, log, array 等)に従って送信 + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param int $mailTemplateId メールテンプレートID(使用プログラムID) + * @return array 処理結果 ['result' => 0|1, 'error_info' => string] + */ + public function executeMailSendWithLaravel(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array + { + try { + Log::info('SHJ-7 メール送信処理開始(Laravel Mail使用)', [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ]); + + // 【処理1】入力パラメーターをチェックする + $paramCheckResult = $this->checkInputParameters($mailAddress, $backupMailAddress, $mailTemplateId); + + // 【判断1】チェック結果 + if (!$paramCheckResult['valid']) { + $errorInfo = $paramCheckResult['error_info']; + + Log::warning('SHJ-7 パラメーターチェックNG', [ + 'error_info' => $errorInfo + ]); + + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + // 【処理2】メール送信テンプレート情報を取得する + $templateInfo = $this->getMailTemplateInfo($mailTemplateId); + + // 【判断2】取得結果判定 + if (empty($templateInfo)) { + $errorInfo = "メール送信NG:メール送信テンプレートが存在しません。/{$mailTemplateId}"; + + Log::warning('SHJ-7 メールテンプレート取得NG', [ + 'mail_template_id' => $mailTemplateId, + 'error_info' => $errorInfo + ]); + + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + // 【処理3】Laravel Mail を使用してメールを送信する + $mailSendResult = $this->sendMailWithLaravel($mailAddress, $backupMailAddress, $templateInfo); + + if (!$mailSendResult['success']) { + $errorInfo = $mailSendResult['error_info']; + + Log::error('SHJ-7 メール送信失敗(Laravel Mail)', [ + 'error_info' => $errorInfo + ]); + + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + + Log::info('SHJ-7 メール送信処理完了(Laravel Mail)', [ + 'to_address' => $mailSendResult['to_address'] + ]); + + // 【処理4】処理結果を返却する(正常終了) + return [ + 'result' => 0, + 'error_info' => '' + ]; + + } catch (\Exception $e) { + $errorInfo = 'メール送信NG:' . $e->getMessage(); + + Log::error('SHJ-7 メール送信処理例外エラー(Laravel Mail)', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'result' => 1, + 'error_info' => $errorInfo + ]; + } + } + + /** + * Laravel Mail ファサードを使用してメールを送信 + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param MailTemplate $templateInfo テンプレート情報 + * @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string] + */ + private function sendMailWithLaravel(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array + { + try { + // 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス) + $toAddress = !empty($mailAddress) ? $mailAddress : $backupMailAddress; + + // メール内容取得 + $subject = $templateInfo->getSubject() ?? ''; + $message = $templateInfo->getText() ?? ''; + $bccAddress = $templateInfo->getBccAddress(); + + Log::info('SHJ-7 メール送信準備完了(Laravel Mail)', [ + 'to_address' => $toAddress, + 'subject' => $subject, + 'has_bcc' => !empty($bccAddress), + 'mgr_cc_flag' => $templateInfo->isManagerCcEnabled() + ]); + + // Laravel Mail で送信 + Mail::raw($message, function ($mail) use ($toAddress, $subject, $bccAddress) { + $mail->to($toAddress) + ->subject($subject); + + // BCC設定(値がある場合のみ) + if (!empty($bccAddress)) { + $mail->bcc($bccAddress); + } + }); + + Log::info('SHJ-7 メール送信成功(Laravel Mail)', [ + 'to_address' => $toAddress, + 'subject' => $subject + ]); + + return [ + 'success' => true, + 'error_info' => '', + 'to_address' => $toAddress + ]; + + } catch (\Exception $e) { + $errorInfo = 'メール送信NG(Laravel Mail):' . $e->getMessage(); + Log::error('SHJ-7 メール送信例外エラー(Laravel Mail)', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error_info' => $errorInfo + ]; + } + } +} \ No newline at end of file diff --git a/app/Services/ShjNineService.php b/app/Services/ShjNineService.php new file mode 100644 index 0000000..b8ffec9 --- /dev/null +++ b/app/Services/ShjNineService.php @@ -0,0 +1,814 @@ +shjEightService = $shjEightService; + } + + /** + * SHJ-9 売上集計処理メイン実行(日次のみ) + * + * 処理フロー (todo/SHJ-9/SHJ-9.txt): + * 【処理1】集計対象を設定する + * 【処理2】駐輪場マスタを取得する + * 【判断1】取得件数判定 + * 【処理3】車種区分毎に算出する + * 【判断2】取得判定 + * 【処理4】売上集計結果を削除→登録する + * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する + * + * @param string $type 集計種別(daily 固定) + * @param string $aggregationDate 集計対象日(YYYY-MM-DD) + * @return array 処理結果 + */ + public function executeEarningsAggregation(string $type, string $aggregationDate): array + { + $statusComments = []; // 内部変数.ステータスコメント + $dataIntegrityIssues = []; // 内部変数.情報不備 + + try { + // 【処理1】集計対象を設定する + // パラメーター検証(日付形式チェック) + if (!$this->isValidDateFormat($aggregationDate)) { + // 日付形式エラー時は【処理5】へ(warning扱い) + $statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)"; + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($statusComment, null); + + // SHJ-8 バッチ処理ログ作成 + $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + + return [ + 'success' => true, // 仕様上はwarningで成功扱い + 'message' => $statusComment + ]; + } + + $targetDate = Carbon::parse($aggregationDate)->format('Y-m-d'); + + Log::info('SHJ-9 売上集計処理開始', [ + 'type' => $type, + 'target_date' => $targetDate + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $statusComment = '売上集計(日次):駐輪場マスタが存在していません。'; + $statusComments[] = $statusComment; + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($statusComment, null); + + // SHJ-8 バッチ処理ログ作成 + $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + + return [ + 'success' => true, + 'message' => $statusComment, + 'processed_parks' => 0, + 'summary_records' => 0 + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $result = $this->processEarningsForPark($park, $targetDate); + + $processedParks++; + $summaryRecords += $result['summary_records']; + + // 対象データなしの場合のステータスコメント収集 + if (!empty($result['no_data_message'])) { + $statusComments[] = $result['no_data_message']; + } + + // 情報不備を収集("なし"でない場合) + if ($result['data_integrity_issue'] !== '情報不備:なし') { + $dataIntegrityIssues[] = $result['data_integrity_issue']; + } + } + + // 最終ステータスコメント生成 + $finalStatusComment = "売上集計(日次):対象日={$targetDate}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}"; + if (!empty($dataIntegrityIssues)) { + $finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues); + } + + // 【処理5】オペレータキュー作成 + // ※ 駐輪場単位で既に作成済み(processEarningsForPark内で情報不備検出時に実施) + if (!empty($dataIntegrityIssues)) { + Log::warning('SHJ-9 情報不備検出', [ + 'issues' => $dataIntegrityIssues + ]); + } + + // SHJ-8 バッチ処理ログ作成 + $this->callShjEight('SHJ-9売上集計(日次)', 'success', $finalStatusComment); + + Log::info('SHJ-9 売上集計処理完了', [ + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), + 'no_data_parks' => count($statusComments) + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-9 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues) + ]; + + } catch (\Exception $e) { + $errorMessage = '売上集計(日次):エラー発生 - ' . $e->getMessage(); + + // SHJ-8 バッチ処理ログ作成(エラー時も作成) + try { + $this->callShjEight('SHJ-9売上集計(日次)', 'error', $errorMessage); + } catch (\Exception $shjException) { + Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); + } + + Log::error('SHJ-9 売上集計処理エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage() + ]; + } + } + + /** + * 日付形式の検証 + * + * @param string $date 日付文字列 + * @return bool 有効な日付形式かどうか + */ + private function isValidDateFormat(string $date): bool + { + try { + $parsed = Carbon::parse($date); + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * @return array 駐輪場情報 + */ + private function getParkInformation(): array + { + try { + $parkInfo = DB::table('park') + ->select(['park_id', 'park_name']) + ->where('park_close_flag', '<>', 1) + ->orderBy('park_ruby') + ->get() + ->toArray(); + + Log::info('駐輪場マスタ取得完了', [ + 'park_count' => count($parkInfo) + ]); + + return $parkInfo; + + } catch (\Exception $e) { + Log::error('駐輪場マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 駐輪場毎の売上集計処理 + * + * @param object $park 駐輪場情報 + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] + */ + private function processEarningsForPark($park, string $targetDate): array + { + try { + // 0. 情報不備チェック + $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate); + + // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263) + if ($dataIntegrityIssue !== '情報不備:なし') { + $this->createOperatorQueue($dataIntegrityIssue, $park->park_id); + } + + // ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + $regularData = $this->calculateRegularEarnings($park->park_id, $targetDate); + + // ② 一時金データ取得(車種毎) + $lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $targetDate); + + // ③ 解約返戻金データ取得(車種区分毎) + $refundData = $this->calculateRefundEarnings($park->park_id, $targetDate); + + // ④ 再発行データ取得(車種区分毎) + $reissueData = $this->calculateReissueCount($park->park_id, $targetDate); + + // 【判断2】データがいずれかあれば【処理4】へ + if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) { + // 対象データなし - 仕様 todo/SHJ-9/SHJ-9.txt:172-175 + $noDataMessage = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}:売上データが存在しません。"; + + return [ + 'summary_records' => 0, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => $noDataMessage + ]; + } + + // 【処理4】既存の売上集計結果を削除 + $this->deleteExistingSummary($park->park_id, $targetDate); + + // 【処理4】売上集計結果を登録 + $summaryRecords = 0; + + // ① 定期契約データがある場合:同じ組合せ(psection×usertype×months)を統合 + $mergedRegularData = $this->mergeRegularDataByGroup($regularData); + foreach ($mergedRegularData as $key => $mergedRow) { + $this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular'); + $summaryRecords++; + } + + // ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約) + $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); + foreach ($otherDataByPsection as $psectionId => $data) { + $this->createEarningsSummary($park, $data, $targetDate, 'other'); + $summaryRecords++; + } + + Log::info('駐輪場売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue + ]); + + return [ + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => null + ]; + + } catch (\Exception $e) { + Log::error('駐輪場売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 0. 情報不備チェック + * + * 仕様 todo/SHJ-9/SHJ-9.txt:44-68 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return string 情報不備メッセージ(仕様フォーマット:"情報不備:xxx" or "情報不備:なし") + */ + private function checkDataIntegrity(int $parkId, string $targetDate): string + { + $incompleteContracts = DB::table('regular_contract') + ->select('contract_id') + ->where('park_id', $parkId) + ->where('contract_flag', 1) + ->whereDate('contract_payment_day', '=', $targetDate) + ->where(function($query) { + $query->whereNull('update_flag') + ->orWhereNull('psection_id') + ->orWhereNull('enable_months'); + }) + ->pluck('contract_id') + ->toArray(); + + if (empty($incompleteContracts)) { + return '情報不備:なし'; + } + + // 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り + return '情報不備:' . implode(',', $incompleteContracts); + } + + /** + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + * + * 仕様 todo/SHJ-9/SHJ-9.txt:70-95 + * SQL定義:減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、 + * 授受金額の合計と件数を算出する + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array + */ + private function calculateRegularEarnings(int $parkId, string $targetDate): array + { + $results = DB::table('regular_contract as T1') + ->join('usertype as T3', 'T1.user_categoryid', '=', 'T3.user_categoryid') + ->select([ + DB::raw('IFNULL(T1.contract_reduction, 0) as contract_reduction'), + 'T1.update_flag', + 'T1.psection_id', + 'T3.usertype_subject1', + 'T1.enable_months', + DB::raw('SUM(T1.contract_money) as total_amount'), // 仕様line 77: 授受金額の合計 + DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様line 78: 授受件数 + ]) + ->where('T1.park_id', $parkId) + ->where('T1.contract_flag', 1) + ->whereDate('T1.contract_payment_day', '=', $targetDate) + ->whereNotNull('T1.update_flag') + ->whereNotNull('T1.psection_id') + ->whereNotNull('T1.enable_months') + ->groupBy([ + DB::raw('IFNULL(T1.contract_reduction, 0)'), + 'T1.update_flag', + 'T1.psection_id', + 'T3.usertype_subject1', + 'T1.enable_months' + ]) + ->get(); + + return $results->toArray(); + } + + /** + * ② 一時金データ取得(車種毎) + * + * 仕様 todo/SHJ-9/SHJ-9.txt:114-125 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array + */ + private function calculateLumpsumEarnings(int $parkId, string $targetDate): array + { + $results = DB::table('lumpsum_transaction') + ->select([ + 'type_class as psection_id', + DB::raw('COUNT(*) as lumpsum_count'), + DB::raw('COALESCE(SUM(deposit_amount), 0) as lumpsum') + ]) + ->where('park_id', $parkId) + ->whereDate('pay_date', '=', $targetDate) + ->groupBy('type_class') + ->get(); + + return $results->toArray(); + } + + /** + * ③ 解約返戻金データ取得(車種区分毎) + * + * 仕様 todo/SHJ-9/SHJ-9.txt:126-137 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array + */ + private function calculateRefundEarnings(int $parkId, string $targetDate): array + { + $results = DB::table('regular_contract') + ->select([ + 'psection_id', + DB::raw('COALESCE(SUM(refunds), 0) as refunds') + ]) + ->where('park_id', $parkId) + ->where('contract_cancel_flag', 1) + ->whereDate('repayment_at', '=', $targetDate) + ->groupBy('psection_id') + ->get(); + + return $results->toArray(); + } + + /** + * ④ 再発行データ取得(車種区分毎) + * + * 仕様 todo/SHJ-9/SHJ-9.txt:138-149 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array + */ + private function calculateReissueCount(int $parkId, string $targetDate): array + { + $results = DB::table('seal') + ->select([ + 'psection_id', + DB::raw('COUNT(contract_id) as reissue_count') + ]) + ->where('park_id', $parkId) + ->where('contract_seal_issue', '>=', 2) + ->whereDate('seal_day', '=', $targetDate) + ->groupBy('psection_id') + ->get(); + + return $results->toArray(); + } + + /** + * 定期契約データを組合せ毎に統合 + * + * SQLは contract_reduction × update_flag で分組しているため、 + * 同じ psection × usertype × months の組合せで複数行が返る場合がある。 + * ここで park_id + psection_id + usertype_subject1 + enable_months をキーに統合し、 + * 新規/更新 × 減免/通常 の各件数・金額を1つのオブジェクトに集約する。 + * + * 仕様 todo/SHJ-9/SHJ-9.txt:96-113 + * + * @param array $regularData calculateRegularEarnings()の結果 + * @return array キー:psection_id|usertype|months、値:統合されたデータオブジェクト + */ + private function mergeRegularDataByGroup(array $regularData): array + { + $merged = []; + + foreach ($regularData as $row) { + // 統合キー:psection_id|usertype_subject1|enable_months + $key = $row->psection_id . '|' . $row->usertype_subject1 . '|' . $row->enable_months; + + // 初回作成 + if (!isset($merged[$key])) { + $merged[$key] = (object)[ + 'psection_id' => $row->psection_id, + 'usertype_subject1' => $row->usertype_subject1, + 'enable_months' => $row->enable_months, + // 新規・通常 + 'regular_new_count' => 0, + 'regular_new_amount' => 0, + // 新規・減免 + 'regular_new_reduction_count' => 0, + 'regular_new_reduction_amount' => 0, + // 更新・通常 + 'regular_update_count' => 0, + 'regular_update_amount' => 0, + // 更新・減免 + 'regular_update_reduction_count' => 0, + 'regular_update_reduction_amount' => 0 + ]; + } + + // 区分判定 + $isNew = in_array($row->update_flag, [2, null]); // 新規 + $isReduction = ($row->contract_reduction == 1); // 減免 + + $count = $row->contract_count ?? 0; + $amount = $row->total_amount ?? 0; + + // 対応するフィールドに累加 + if ($isNew && !$isReduction) { + // 新規・通常 + $merged[$key]->regular_new_count += $count; + $merged[$key]->regular_new_amount += $amount; + } elseif ($isNew && $isReduction) { + // 新規・減免 + $merged[$key]->regular_new_reduction_count += $count; + $merged[$key]->regular_new_reduction_amount += $amount; + } elseif (!$isNew && !$isReduction) { + // 更新・通常 + $merged[$key]->regular_update_count += $count; + $merged[$key]->regular_update_amount += $amount; + } elseif (!$isNew && $isReduction) { + // 更新・減免 + $merged[$key]->regular_update_reduction_count += $count; + $merged[$key]->regular_update_reduction_amount += $amount; + } + } + + return $merged; + } + + /** + * 一時金・解約・再発行データを車種区分毎に統合 + * + * @param array $lumpsumData + * @param array $refundData + * @param array $reissueData + * @return array + */ + private function mergeOtherEarningsData(array $lumpsumData, array $refundData, array $reissueData): array + { + $merged = []; + + // 一時金 + foreach ($lumpsumData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->lumpsum_count = $row->lumpsum_count; + $merged[$psectionId]->lumpsum = $row->lumpsum; + } + + // 解約返戻金 + foreach ($refundData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->refunds = $row->refunds; + } + + // 再発行 + foreach ($reissueData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->reissue_count = $row->reissue_count; + } + + return $merged; + } + + /** + * 【処理4】既存の売上集計結果を削除 + * + * 仕様書のキー:駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数 + * 仕様 todo/SHJ-9/SHJ-9.txt:181-189 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return void + */ + private function deleteExistingSummary(int $parkId, string $targetDate): void + { + // 仕様書どおり、同一キーの組み合わせで削除 + // 日次の場合、集計開始日・終了日はNULL、売上日付で判定 + DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_type', self::SUMMARY_TYPE_DAILY) + ->whereNull('summary_start_date') + ->whereNull('summary_end_date') + ->where('earnings_date', $targetDate) + // psection_id, usertype_subject1, enable_months は + // レコードごとに異なるため、ここでは指定しない + ->delete(); + + Log::debug('既存の売上集計結果削除', [ + 'park_id' => $parkId, + 'target_date' => $targetDate + ]); + } + + /** + * 売上集計結果を登録 + * + * 仕様 todo/SHJ-9/SHJ-9.txt:181-247 + * + * @param object $park 駐輪場情報 + * @param object $data 売上データ + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @param string $dataType データ種別(regular or other) + * @return void + */ + private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void + { + $insertData = [ + 'park_id' => $park->park_id, + 'summary_type' => self::SUMMARY_TYPE_DAILY, // 3 = 日次 + 'summary_start_date' => null, // 日次は NULL (仕様line 215) + 'summary_end_date' => null, // 日次は NULL (仕様line 216) + 'earnings_date' => $targetDate, + 'psection_id' => $data->psection_id, + 'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名 + 'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数 + 'summary_note' => "SHJ-9:{$targetDate}", // 仕様line 236 + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 239) + ]; + + if ($dataType === 'regular') { + // 定期契約データの場合:mergeRegularDataByGroup()で既に統合済み + // 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 98-113) + $insertData = array_merge($insertData, [ + 'regular_new_count' => $data->regular_new_count ?? 0, + 'regular_new_amount' => $data->regular_new_amount ?? 0, + 'regular_new_reduction_count' => $data->regular_new_reduction_count ?? 0, + 'regular_new_reduction_amount' => $data->regular_new_reduction_amount ?? 0, + 'regular_update_count' => $data->regular_update_count ?? 0, + 'regular_update_amount' => $data->regular_update_amount ?? 0, + 'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0, + 'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0, + 'lumpsum_count' => 0, // 仕様line 106 + 'lumpsum' => 0, // 仕様line 107 + 'refunds' => 0, // 仕様line 108 + 'other_income' => 0, // 仕様line 109 + 'other_spending' => 0, // 仕様line 110 + 'reissue_count' => 0, // 仕様line 111 + 'reissue_amount' => 0 // 仕様line 112 + ]); + } else { + // 一時金・解約・再発行データの場合:定期フィールドは0固定 (仕様line 152-167) + $insertData = array_merge($insertData, [ + 'regular_new_count' => 0, // 仕様line 152 + 'regular_new_amount' => 0, // 仕様line 153 + 'regular_new_reduction_count' => 0, // 仕様line 154 + 'regular_new_reduction_amount' => 0, // 仕様line 155 + 'regular_update_count' => 0, // 仕様line 156 + 'regular_update_amount' => 0, // 仕様line 157 + 'regular_update_reduction_count' => 0, // 仕様line 158 + 'regular_update_reduction_amount' => 0, // 仕様line 159 + 'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 160 + 'lumpsum' => $data->lumpsum ?? 0, // 仕様line 161 + 'refunds' => $data->refunds ?? 0, // 仕様line 162 + 'other_income' => 0, // 仕様line 163 + 'other_spending' => 0, // 仕様line 164 + 'reissue_count' => $data->reissue_count ?? 0, // 仕様line 165 + 'reissue_amount' => 0 // 仕様line 166: 0固定 + ]); + } + + DB::table('earnings_summary')->insert($insertData); + + Log::debug('売上集計結果登録', [ + 'park_id' => $park->park_id, + 'psection_id' => $data->psection_id, + 'data_type' => $dataType, + 'target_date' => $targetDate + ]); + } + + /** + * 【処理5】オペレータキュー作成(駐輪場単位・情報不備がある場合のみ) + * + * 仕様 todo/SHJ-9/SHJ-9.txt:253-280 + * - que_class: 14(集計対象エラー) + * - que_comment: 空文字("") + * - que_status: 1(キュー発生) + * - que_status_comment: 空文字("") + * - work_instructions: 情報不備メッセージ + * - park_id: 駐輪場ID(仕様 "処理1.駐輪場ID"、パラメータエラー時はnull) + * - operator_id: 9999999(バッチ処理固定値) + * + * @param string $message 情報不備メッセージ + * @param int|null $parkId 駐輪場ID(パラメータエラー時はnull) + * @return void + */ + private function createOperatorQueue(string $message, ?int $parkId = null): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 14, // 集計対象エラー + 'user_id' => null, + 'contract_id' => null, + 'park_id' => $parkId, // 仕様:処理1.駐輪場ID + 'que_comment' => '', // 仕様line 260: "" + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', // 仕様line 262: "" + 'work_instructions' => $message, // 仕様line 263: 情報不備 + 'operator_id' => self::BATCH_OPERATOR_ID, // 9999999 + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'park_id' => $parkId, + 'que_class' => 14, + 'que_status' => 1, + 'operator_id' => self::BATCH_OPERATOR_ID, + 'work_instructions' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * SHJ-8 バッチ処理ログ作成 + * + * 仕様 todo/SHJ-9/SHJ-9.txt:289-300 + * 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す + * + * @param string $jobName ジョブ名 + * @param string $status ステータス (success/error) + * @param string $statusComment 業務固有のステータスコメント + * @return void + */ + private function callShjEight(string $jobName, string $status, string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + $today = now()->format('Y/m/d'); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-9', + $jobName, + $status, + $statusComment, + $today, + $today + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了', [ + 'job_name' => $jobName, + 'status' => $status + ]); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'job_name' => $jobName, + 'status_comment' => $statusComment + ]); + throw $e; + } + } +} diff --git a/app/Services/ShjOneService.php b/app/Services/ShjOneService.php new file mode 100644 index 0000000..7932e59 --- /dev/null +++ b/app/Services/ShjOneService.php @@ -0,0 +1,1102 @@ +googleVisionService = $googleVisionService; + $this->googleMapsService = $googleMapsService; + $this->mailSendService = $mailSendService; + $this->shjEightService = $shjEightService; + } + + /** + * SHJ-1メイン処理実行 + * + * @param int $userId 利用者連番 + * @param int $parkId 駐輪場ID + * @return array + */ + public function execute(int $userId, int $parkId): array + { + try { + Log::info('SHJ-1 【処理1】開始: 本人確認自動処理レコード群取得', [ + 'user_id' => $userId, + 'park_id' => $parkId + ]); + + // 【処理1】本人確認自動処理レコード群取得 - 設計書通りの条件でフィルタリング + $user = $this->getUserRecord($userId); + if (!$user) { + Log::error('SHJ-1 【処理1】失敗: 利用者が見つかりません', [ + 'user_id' => $userId, + 'park_id' => $parkId + ]); + $result = [ + 'system_success' => false, + 'message' => '利用者が見つかりません', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + $this->createBatchLog($userId, null, null, $result); + return $result; + } + + Log::info('SHJ-1 【処理1】成功: 利用者データ取得', [ + 'user_seq' => $user->user_seq, + 'user_name' => $user->user_name, + 'user_idcard' => $user->user_idcard, + 'photo_filename1' => $user->photo_filename1, + 'photo_filename2' => $user->photo_filename2 + ]); + + // 本人確認書類が免許証以外、または写真ファイルがない場合は対象外 + if (!$this->isTargetUser($user)) { + Log::warning('SHJ-1 処理対象外判定', [ + 'user_seq' => $user->user_seq, + 'user_idcard' => $user->user_idcard, + 'photo_filename1' => $user->photo_filename1, + 'user_idcard_chk_flag' => $user->user_idcard_chk_flag, + 'reason' => '免許証以外または写真なしまたは既に処理済み' + ]); + $result = [ + 'system_success' => false, + 'message' => '処理対象外の利用者です(免許証以外または写真なし)', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + $this->createBatchLog($userId, $user->user_name, null, $result); + return $result; + } + + $park = $this->getParkRecord($parkId); + if (!$park) { + Log::error('SHJ-1 駐輪場データ取得失敗', [ + 'park_id' => $parkId + ]); + $result = [ + 'system_success' => false, + 'message' => '駐輪場が見つかりません', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + $this->createBatchLog($userId, $user->user_name, null, $result); + return $result; + } + + Log::info('SHJ-1 駐輪場データ取得成功', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name + ]); + + // トランザクション開始 + DB::beginTransaction(); + Log::info('SHJ-1 データベーストランザクション開始'); + + // 【判断1】対象外判定 + Log::info('SHJ-1 【判断1】開始: 利用者分類チェック'); + $categoryCheck = $this->checkUserCategory($user); + + if (!$categoryCheck['is_target']) { + Log::warning('SHJ-1 【判断1】対象外: 手動処理対象', [ + 'user_seq' => $user->user_seq, + 'category_name' => $categoryCheck['category_name'], + 'reason' => '高齢者、障がい者、生活保護、中国、母子家庭等' + ]); + $result = $this->processNonTargetUser($user, $categoryCheck['category_name']); + DB::commit(); // 対象外処理後はコミット + $this->createBatchLog($userId, $user->user_name, null, $result); + return $result; + } + + Log::info('SHJ-1 【判断1】成功: 自動処理対象', [ + 'user_seq' => $user->user_seq, + 'category_name' => $categoryCheck['category_name'] + ]); + + // 【処理2】本人確認自動処理+800Mチェック処理 + Log::info('SHJ-1 【処理2】開始: 本人確認自動処理'); + $identityResult = $this->processIdentityVerification($user, $park); + + DB::commit(); + + // バッチ処理ログ作成 + $this->createBatchLog($userId, $user->user_name, $identityResult['similarity_rate'] ?? null, $identityResult); + + return $identityResult; + + } catch (Exception $e) { + DB::rollBack(); + Log::error('SHJ-1処理エラー', [ + 'user_id' => $userId, + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + + $result = [ + 'system_success' => false, + 'message' => 'システムエラーが発生しました: ' . $e->getMessage(), + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + + // エラー時もバッチログ作成 + $this->createBatchLog($userId, null, null, $result); + + return $result; + } + } + + /** + * 利用者レコード取得(設計書の条件に従ってフィルタリング) + */ + private function getUserRecord(int $userId): ?User + { + return User::where('user_seq', $userId) + ->whereNotNull('photo_filename1') // 本人確認写真必須 + ->first(); + } + + /** + * 処理対象ユーザーかチェック(設計書の条件) + */ + private function isTargetUser(User $user): bool + { + Log::info('SHJ-1 isTargetUser チェック開始', [ + 'user_seq' => $user->user_seq, + 'user_idcard' => $user->user_idcard, + 'photo_filename1' => $user->photo_filename1, + 'user_idcard_chk_flag' => $user->user_idcard_chk_flag, + 'user_idcard_chk_flag_type' => gettype($user->user_idcard_chk_flag), + 'auto_ok_config' => config('shj1.identity_check_status.auto_ok') + ]); + + // 本人確認書類が免許証であること + if ($user->user_idcard !== '免許証') { + Log::info('SHJ-1 isTargetUser: 免許証以外', ['user_idcard' => $user->user_idcard]); + return false; + } + + // 写真ファイルが存在すること + if (empty($user->photo_filename1)) { + Log::info('SHJ-1 isTargetUser: 写真ファイルなし'); + return false; + } + + // 既に本人確認済みの場合はスキップ(一度OKなら再チェック省略) + // ※テスト用に一時的にコメントアウト - 重複処理を許可 + /* + $autoOk = config('shj1.identity_check_status.auto_ok'); + if ($user->user_idcard_chk_flag === $autoOk) { // 厳密比較に変更 + Log::info('SHJ-1 isTargetUser: 既に処理済み', [ + 'current_flag' => $user->user_idcard_chk_flag, + 'current_flag_type' => gettype($user->user_idcard_chk_flag), + 'auto_ok' => $autoOk, + 'auto_ok_type' => gettype($autoOk), + 'loose_comparison' => $user->user_idcard_chk_flag == $autoOk ? 'equal' : 'not_equal', + 'strict_comparison' => $user->user_idcard_chk_flag === $autoOk ? 'equal' : 'not_equal' + ]); + return false; + } + */ + + Log::info('SHJ-1 isTargetUser: 処理対象OK'); + return true; + } + + /** + * 駐輪場レコード取得 + */ + private function getParkRecord(int $parkId): ?Park + { + return Park::where('park_id', $parkId)->first(); + } + + /** + * 利用者分類チェック + */ + private function checkUserCategory(User $user): array + { + $usertype = Usertype::where('user_categoryid', $user->user_categoryid)->first(); + + if (!$usertype) { + return ['is_target' => false, 'category_name' => 'unknown']; + } + + $categoryName = $usertype->usertype_subject3; + $excludedCategories = config('shj1.user_categories.excluded_categories'); + + // 対象外カテゴリーかチェック + $isExcluded = in_array($categoryName, $excludedCategories); + + return [ + 'is_target' => !$isExcluded, + 'category_name' => $categoryName + ]; + } + + /** + * 対象外ユーザーの処理 + */ + private function processNonTargetUser(User $user, string $categoryName): array + { + // 本人確認済ステータスをNGで更新(設計書通り) + $autoNgValue = config('shj1.identity_check_status.auto_ng'); + + Log::info('SHJ-1 対象外ユーザー: ユーザー情報更新開始', [ + 'user_seq' => $user->user_seq, + 'category_name' => $categoryName, + 'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag, + 'will_set_to' => $autoNgValue + ]); + + $updateResult = $user->update([ + 'user_idcard_chk_flag' => $autoNgValue, + 'user_chk_day' => now(), + 'user_chk_opeid' => 'SHJ-1' + ]); + + Log::info('SHJ-1 対象外ユーザー: ユーザー情報更新完了', [ + 'user_seq' => $user->user_seq, + 'update_result' => $updateResult, + 'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag + ]); + + // オペレータキューも作成(設計書の要求) + $this->createOperatorQueue($user, null, [], true); + + return [ + 'system_success' => true, + 'identity_result' => 'NG', + 'message' => '対象外ユーザーのためNG処理完了', + 'details' => [ + 'user_id' => $user->user_seq, + 'category' => $categoryName, + 'action' => 'set_ng_status_with_queue' + ], + 'stats' => ['processed_count' => 1, 'ng_count' => 1, 'non_target_count' => 1] + ]; + } + + /** + * 本人確認自動処理+800Mチェック処理(SHJ-1文書仕様) + * + * 文書仕様: + * 1. まずOCR照合処理を実行 + * 2. OCR照合成功の場合のみ距離チェック処理を実行 + * 3. 両方成功で最終成功 + */ + private function processIdentityVerification(User $user, Park $park): array + { + try { + // 1. OCR処理による本人確認 + Log::info('SHJ-1 【処理2】開始: 本人確認自動処理'); + $ocrResult = $this->performComprehensiveOcrVerification($user); + + // OCR処理失敗の場合、距離チェックを実行せずに失敗処理 + if (!$ocrResult['success']) { + Log::info('SHJ-1 OCR照合失敗、距離チェックスキップ', [ + 'user_seq' => $user->user_seq, + 'reason' => $ocrResult['reason'] + ]); + + // 距離チェック結果をダミーで設定(実行しない) + $distanceResult = [ + 'within_limit' => true, // OCR失敗なので距離は関係ない + 'skipped' => true, + 'reason' => 'ocr_failed' + ]; + + return $this->processFailureCase($user, $park, $ocrResult, $distanceResult); + } + + // 2. OCR成功 → 距離チェック処理を実行 + Log::info('SHJ-1 OCR照合成功、距離チェック開始', [ + 'user_seq' => $user->user_seq, + 'matched_address_type' => $ocrResult['matched_address_type'] + ]); + + $distanceResult = $this->performDistanceCheck($user, $park); + + // 3. 最終判定:OCR成功 AND 距離内 + if ($distanceResult['within_limit']) { + return $this->processSuccessCase($user, $park, $ocrResult, $distanceResult); + } else { + return $this->processFailureCase($user, $park, $ocrResult, $distanceResult); + } + + } catch (Exception $e) { + Log::error('SHJ-1 【処理2】エラー', [ + 'user_seq' => $user->user_seq, + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + // システムエラーとして失敗処理 + return $this->processFailureCase($user, $park, + ['success' => false, 'reason' => 'system_error'], + ['within_limit' => true, 'skipped' => true, 'reason' => 'system_error'] + ); + } + } + + /** + * 包括的OCR処理(SHJ-1文書仕様に従った実装) + * + * 文書仕様: + * - 表面画像で一度OKが出た場合、裏面のチェックはスキップする + * - [OCR值]と[利用者住所]を照合する + */ + private function performComprehensiveOcrVerification(User $user): array + { + try { + Log::info('SHJ-1 OCR処理開始', [ + 'user_seq' => $user->user_seq, + 'photo_filename1' => $user->photo_filename1, + 'photo_filename2' => $user->photo_filename2 + ]); + + // 表面画像の処理 + if ($user->photo_filename1) { + $photoPath = $this->buildPhotoPath($user->photo_filename1); + Log::info('SHJ-1 表面画像処理開始', [ + 'user_seq' => $user->user_seq, + 'photo_filename1' => $user->photo_filename1, + 'photo_path' => $photoPath + ]); + + $frontResult = $this->processImageForOcr($user, $photoPath, 'front'); + + Log::info('SHJ-1 表面画像処理完了', [ + 'user_seq' => $user->user_seq, + 'front_result' => $frontResult + ]); + + // SHJ-1文書通り:一度OKが出た場合、裏面チェックはスキップ + if ($frontResult['success']) { + Log::info('SHJ-1 表面画像でOK、裏面チェックスキップ', [ + 'user_seq' => $user->user_seq + ]); + + return [ + 'success' => true, + 'processed_side' => 'front', + 'extracted_name' => $frontResult['extracted_name'], + 'extracted_address' => $frontResult['extracted_address'], + 'ocr_value' => $frontResult['ocr_value'], + 'matched_address_type' => $frontResult['matched_address_type'], + 'matched_address' => $frontResult['matched_address'], + 'similarity' => $frontResult['similarity'], + 'reason' => $frontResult['reason'] + ]; + } + } + + // 表面画像がNGまたは存在しない場合、裏面画像を処理 + if ($user->photo_filename2) { + $photoPath = $this->buildPhotoPath($user->photo_filename2); + Log::info('SHJ-1 裏面画像処理開始', [ + 'user_seq' => $user->user_seq, + 'photo_filename2' => $user->photo_filename2, + 'photo_path' => $photoPath + ]); + + $backResult = $this->processImageForOcr($user, $photoPath, 'back'); + + Log::info('SHJ-1 裏面画像処理完了', [ + 'user_seq' => $user->user_seq, + 'back_result' => $backResult + ]); + + if ($backResult['success']) { + return [ + 'success' => true, + 'processed_side' => 'back', + 'extracted_name' => $backResult['extracted_name'], + 'extracted_address' => $backResult['extracted_address'], + 'ocr_value' => $backResult['ocr_value'], + 'matched_address_type' => $backResult['matched_address_type'], + 'matched_address' => $backResult['matched_address'], + 'similarity' => $backResult['similarity'], + 'reason' => $backResult['reason'] + ]; + } + + // 裏面もNG + return [ + 'success' => false, + 'processed_side' => 'both', + 'reason' => 'both_sides_failed', + 'front_result' => $frontResult ?? null, + 'back_result' => $backResult + ]; + } + + // 表面のみでNG + return [ + 'success' => false, + 'processed_side' => 'front_only', + 'reason' => 'front_failed_no_back', + 'front_result' => $frontResult ?? null + ]; + + } catch (Exception $e) { + Log::error('SHJ-1 OCR処理エラー', [ + 'user_seq' => $user->user_seq, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'reason' => 'system_error', + 'error' => $e->getMessage() + ]; + } + } + + /** + * 照片文件路径构建(Laravel Storage方式) + */ + private function buildPhotoPath(string $filename): string + { + // Storage::disk('public')用,需要 "photo/filename" 格式 + return 'photo/' . $filename; + } + + /** + * 画像OCR処理(SHJ-1文書仕様に従った実装) + */ + private function processImageForOcr(User $user, string $filename, string $side): array + { + // Google Vision APIでOCR実行 + $ocrText = $this->googleVisionService->extractTextFromImage($filename); + + // 完全なOCR認識結果をログ出力 + Log::info('SHJ-1 完全OCR認識結果', [ + 'user_seq' => $user->user_seq, + 'filename' => $filename, + 'side' => $side, + 'ocr_text_length' => strlen($ocrText), + 'ocr_full_text' => base64_encode($ocrText) + ]); + + if (empty($ocrText)) { + Log::error('SHJ-1 OCR結果が空', ['user_seq' => $user->user_seq, 'filename' => $filename]); + return [ + 'success' => false, + 'reason' => 'ocr_empty', + 'extracted_name' => '', + 'extracted_address' => '', + 'ocr_value' => '' + ]; + } + + // SHJ-1文書通り:OCR文本からの氏名・住所抽出 + $extractResult = $this->extractNameAndAddressFromOcr($ocrText); + + if (!$extractResult['success']) { + Log::error('SHJ-1 OCR抽出失敗', [ + 'user_seq' => $user->user_seq, + 'filename' => $filename, + 'reason' => $extractResult['reason'] + ]); + return $extractResult; + } + + Log::info('SHJ-1 OCR抽出成功', [ + 'user_seq' => $user->user_seq, + 'filename' => $filename, + 'side' => $side, + 'extracted_name' => $extractResult['extracted_name'], + 'extracted_address' => $extractResult['extracted_address'], + 'ocr_value' => $extractResult['ocr_value'] + ]); + + // SHJ-1文書通り:[OCR値]と[利用者住所]の照合 + $matchResult = $this->performOcrMatching($user, $extractResult['ocr_value']); + + return array_merge($extractResult, $matchResult); + } + + /** + * SHJ-1文書通り:OCRテキストから氏名と住所を抽出 + * + * 文書仕様: + * 1. "氏名"があるかチェック + * 2. "氏名"の次から年号和暦(昭和|平成)まで → [氏名] + * 3. "住所"があるかチェック + * 4. "住所"の次から和暦(昭和|平成|令和)または"交付"まで → [住所] + * 5. [OCR値] = [氏名] + [住所] + */ + private function extractNameAndAddressFromOcr(string $ocrText): array + { + // ノイズ除去:改行とスペースを統一 + $cleanedOcr = preg_replace('/\s+/', '', $ocrText); + + // 1. "氏名"キーワードの存在チェック + if (strpos($cleanedOcr, '氏名') === false) { + return [ + 'success' => false, + 'reason' => 'name_keyword_not_found', + 'extracted_name' => '', + 'extracted_address' => '', + 'ocr_value' => '' + ]; + } + + // 2. 氏名抽出:「氏名」の次から「住所」まで + $namePattern = '/氏名([^住]*?)住所/u'; + if (!preg_match($namePattern, $cleanedOcr, $nameMatches)) { + return [ + 'success' => false, + 'reason' => 'name_extraction_failed', + 'extracted_name' => '', + 'extracted_address' => '', + 'ocr_value' => '' + ]; + } + + $extractedName = $this->removeSymbols($nameMatches[1]); + + // 3. "住所"キーワードの存在チェック + if (strpos($cleanedOcr, '住所') === false) { + return [ + 'success' => false, + 'reason' => 'address_keyword_not_found', + 'extracted_name' => $extractedName, + 'extracted_address' => '', + 'ocr_value' => '' + ]; + } + + // 4. 住所抽出:「住所」の次から改行文字または「交付」まで + $addressPattern = '/住所([^\r\n|]*?)(?:\r|\n|\|交付|交付)/u'; + if (!preg_match($addressPattern, $cleanedOcr, $addressMatches)) { + return [ + 'success' => false, + 'reason' => 'address_extraction_failed', + 'extracted_name' => $extractedName, + 'extracted_address' => '', + 'ocr_value' => '' + ]; + } + + $extractedAddress = $this->removeSymbols($addressMatches[1]); + + // 5. SHJ-1文書通り:[OCR値] = [氏名] + [住所] + $ocrValue = $extractedName . $extractedAddress; + + return [ + 'success' => true, + 'extracted_name' => $extractedName, + 'extracted_address' => $extractedAddress, + 'ocr_value' => $ocrValue + ]; + } + + /** + * 記号を除去(SHJ-1文書仕様) + */ + private function removeSymbols(string $text): string + { + // 基本的な記号・区切り文字を除去 + $cleaned = preg_replace('/[|\-\|\/\\\\()()\[\]【】「」『』<>《》≪≫]/u', '', $text); + $cleaned = preg_replace('/\s+/u', '', $cleaned); // スペース除去 + return trim($cleaned); + } + + /** + * SHJ-1文書通り:[OCR值]と[利用者住所]の照合処理 + * + * 文書仕様: + * 1. [利用者居住所]と[OCR値]を照合 → 閾値以下なら次へ + * 2. [利用者関連住所]と[OCR値]を照合 → 閾値以下ならOCR照合失敗 + */ + private function performOcrMatching(User $user, string $ocrValue): array + { + $threshold = config('shj1.ocr.similarity_threshold'); + + // [利用者居住所]を構築 + $residentAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add; + + // 1. [利用者居住所]と[OCR値]を照合 + similar_text($residentAddress, $ocrValue, $residentSimilarity); + + Log::info('SHJ-1 居住住所照合', [ + 'user_seq' => $user->user_seq, + 'resident_address' => $residentAddress, + 'ocr_value' => $ocrValue, + 'similarity' => $residentSimilarity, + 'threshold' => $threshold + ]); + + if ($residentSimilarity >= $threshold) { + // 居住住所で照合成功 + Log::info('SHJ-1 OCR照合成功(居住住所)', [ + 'user_seq' => $user->user_seq, + 'matched_address_type' => '居住住所', + 'similarity' => $residentSimilarity + ]); + + return [ + 'success' => true, + 'matched_address_type' => '居住住所', + 'matched_address' => $residentAddress, + 'similarity' => $residentSimilarity, + 'reason' => 'resident_address_matched' + ]; + } + + // 2. [利用者関連住所]と[OCR値]を照合 + $relatedAddress = $user->user_relate_pre . $user->user_relate_city . $user->user_relate_add; + + if (!empty(trim($relatedAddress))) { + similar_text($relatedAddress, $ocrValue, $relatedSimilarity); + + Log::info('SHJ-1 関連住所照合', [ + 'user_seq' => $user->user_seq, + 'related_address' => $relatedAddress, + 'ocr_value' => $ocrValue, + 'similarity' => $relatedSimilarity, + 'threshold' => $threshold + ]); + + if ($relatedSimilarity >= $threshold) { + // 関連住所で照合成功 + Log::info('SHJ-1 OCR照合成功(関連住所)', [ + 'user_seq' => $user->user_seq, + 'matched_address_type' => '関連住所', + 'similarity' => $relatedSimilarity + ]); + + return [ + 'success' => true, + 'matched_address_type' => '関連住所', + 'matched_address' => $relatedAddress, + 'similarity' => $relatedSimilarity, + 'reason' => 'related_address_matched' + ]; + } + } + + // 両方とも閾值以下:OCR照合失敗 + Log::info('SHJ-1 OCR照合失敗', [ + 'user_seq' => $user->user_seq, + 'resident_similarity' => $residentSimilarity, + 'related_similarity' => $relatedAddress ? ($relatedSimilarity ?? 0) : 'N/A', + 'threshold' => $threshold + ]); + + return [ + 'success' => false, + 'matched_address_type' => null, + 'matched_address' => null, + 'similarity' => max($residentSimilarity, $relatedSimilarity ?? 0), + 'reason' => 'threshold_not_met' + ]; + } + + /** + * 距離チェック処理(設計書通りpark.distance_between_two_pointsを使用) + */ + private function performDistanceCheck(User $user, Park $park): array + { + try { + // ユーザー住所を構築 + $userAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add; + + // 駐輪場住所 + $parkAddress = $park->park_adrs; + + Log::info('SHJ-1 距離計算開始', [ + 'user_seq' => $user->user_seq, + 'park_id' => $park->park_id, + 'user_address' => $userAddress, + 'park_address' => $parkAddress + ]); + + // Google Maps APIで距離計算 + $distanceResult = $this->googleMapsService->calculateDistance($userAddress, $parkAddress); + $distanceMeters = $distanceResult['distance_meters']; + + // 駐輪場の二点間距離制限を取得(設計書の要求) + $limitMeters = $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'); + + $withinLimit = $distanceMeters <= $limitMeters; + $distanceDetail = $this->googleMapsService->generateDistanceDetailString( + $park->park_id, + $distanceMeters, + 'google_maps' + ); + + Log::info('SHJ-1 距離計算完了', [ + 'user_seq' => $user->user_seq, + 'park_id' => $park->park_id, + 'calculated_distance_meters' => $distanceMeters, + 'distance_text' => $distanceResult['distance_text'] ?? null, + 'limit_meters' => $limitMeters, + 'within_limit' => $withinLimit, + 'distance_detail' => $distanceDetail + ]); + + return [ + 'within_limit' => $withinLimit, + 'distance_meters' => $distanceMeters, + 'limit_meters' => $limitMeters, + 'distance_detail' => $distanceDetail, + 'user_address' => $userAddress, + 'park_address' => $parkAddress + ]; + + } catch (Exception $e) { + Log::error('Distance check error', [ + 'user_id' => $user->user_seq, + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + // API失敗時は距離NGとして処理(設計書の要求) + return [ + 'within_limit' => false, + 'distance_meters' => 999999, + 'limit_meters' => $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'), + 'error' => $e->getMessage(), + 'distance_detail' => $park->park_id . "/" . $park->park_id . "/API Error: " . $e->getMessage() + ]; + } + } + + /** + * 成功ケースの処理 + */ + private function processSuccessCase(User $user, Park $park, array $ocrResult, array $distanceResult): array + { + // ユーザー情報更新 + $autoOkValue = config('shj1.identity_check_status.auto_ok'); + + Log::info('SHJ-1 成功ケース: ユーザー情報更新開始', [ + 'user_seq' => $user->user_seq, + 'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag, + 'will_set_to' => $autoOkValue + ]); + + $updateResult = $user->update([ + 'user_idcard_chk_flag' => $autoOkValue, + 'user_chk_day' => now(), + 'user_chk_opeid' => 'SHJ-1' + ]); + + Log::info('SHJ-1 成功ケース: ユーザー情報更新完了', [ + 'user_seq' => $user->user_seq, + 'update_result' => $updateResult, + 'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag + ]); + + // 成功メール送信 + $this->sendSuccessEmail($user, $park); + + return [ + 'system_success' => true, + 'identity_result' => 'OK', + 'message' => '本人確認自動処理が成功しました', + 'similarity_rate' => $ocrResult['similarity'] ?? 0, + 'details' => [ + 'user_id' => $user->user_seq, + 'park_id' => $park->park_id, + 'ocr_result' => $ocrResult['reason'], + 'distance_check' => 'within_limit' + ], + 'stats' => ['processed_count' => 1, 'success_count' => 1] + ]; + } + + /** + * 失敗ケースの処理 + */ + private function processFailureCase(User $user, Park $park, array $ocrResult, array $distanceResult): array + { + // ユーザー情報更新(設計書通り、NG時もuser_chk_dayを更新) + $autoNgValue = config('shj1.identity_check_status.auto_ng'); + + Log::info('SHJ-1 失敗ケース: ユーザー情報更新開始', [ + 'user_seq' => $user->user_seq, + 'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag, + 'will_set_to' => $autoNgValue, + 'ocr_result' => $ocrResult['reason'] ?? 'unknown', + 'distance_within_limit' => $distanceResult['within_limit'] ?? 'unknown' + ]); + + $updateResult = $user->update([ + 'user_idcard_chk_flag' => $autoNgValue, + 'user_chk_day' => now(), + 'user_chk_opeid' => 'SHJ-1' + ]); + + Log::info('SHJ-1 失敗ケース: ユーザー情報更新完了', [ + 'user_seq' => $user->user_seq, + 'update_result' => $updateResult, + 'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag + ]); + + // オペレータキュー作成 + $this->createOperatorQueue($user, $park, $distanceResult); + + // 800M違反フラグ更新(距離NGの場合) + if (!$distanceResult['within_limit']) { + $this->update800mFlag($user, $park); + } + + // 失敗メール送信 + $this->sendFailureEmail($user, $park); + + return [ + 'system_success' => true, + 'identity_result' => 'NG', + 'message' => '本人確認自動処理NGのため手動処理キューを作成しました', + 'similarity_rate' => $ocrResult['similarity'] ?? 0, + 'details' => [ + 'user_id' => $user->user_seq, + 'park_id' => $park->park_id, + 'ocr_result' => $ocrResult['reason'], + 'distance_check' => $distanceResult['within_limit'] ? 'within_limit' : 'over_limit' + ], + 'stats' => ['processed_count' => 1, 'ng_count' => 1] + ]; + } + + /** + * オペレータキュー作成 + */ + private function createOperatorQueue(User $user, ?Park $park, array $distanceResult, bool $isNonTarget = false): void + { + $queueType = $user->user_school ? + config('shj1.operator_queue.queue_types.student') : + config('shj1.operator_queue.queue_types.general'); + + $comment = config('shj1.operator_queue.default_comment'); + if (isset($distanceResult['distance_detail'])) { + $comment .= " / " . $distanceResult['distance_detail']; + } + if ($isNonTarget) { + $comment .= " / 対象外ユーザー"; + } + + OperatorQue::create([ + 'que_class' => $queueType, + 'user_id' => $user->user_seq, + 'park_id' => $park ? $park->park_id : null, + 'que_status' => config('shj1.operator_queue.queue_status.created'), + 'que_status_comment' => $comment, + 'operator_id' => config('shj1.operator_queue.batch_operator_id'), + 'created_at' => now(), + 'updated_at' => now() + ]); + } + + /** + * 800M違反フラグ更新(設計書通り本人確認オペレータIDも更新) + */ + private function update800mFlag(User $user, Park $park): void + { + RegularContract::where('user_id', $user->user_seq) + ->where('park_id', $park->park_id) + ->update([ + '800m_flag' => config('shj1.contract_800m.violation_flag'), + 'updated_at' => now() + ]); + } + + /** + * 成功メール送信 + */ + private function sendSuccessEmail(User $user, Park $park): void + { + try { + // SHJ-7 メール送信処理 + $mailResult = $this->mailSendService->executeMailSend( + $user->user_primemail, + $user->user_submail, + config('shj1.mail.program_id_success') + ); + + // SHJ-7仕様準拠: result === 0 が正常、それ以外は異常 + if (($mailResult['result'] ?? 1) === 0) { + Log::info('SHJ-1 成功メール送信成功', [ + 'user_id' => $user->user_seq, + 'email' => $user->user_primemail + ]); + } else { + // 仕様準拠: error_info をログに記録 + Log::error('SHJ-1 成功メール送信失敗', [ + 'user_id' => $user->user_seq, + 'email' => $user->user_primemail, + 'error_info' => $mailResult['error_info'] ?? 'メール送信失敗' + ]); + } + } catch (Exception $e) { + Log::error('SHJ-1 成功メール送信例外エラー', [ + 'user_id' => $user->user_seq, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 失敗メール送信 + */ + private function sendFailureEmail(User $user, Park $park): void + { + try { + // SHJ-7 メール送信処理 + $mailResult = $this->mailSendService->executeMailSend( + $user->user_primemail, + $user->user_submail, + config('shj1.mail.program_id_failure') + ); + + // SHJ-7仕様準拠: result === 0 が正常、それ以外は異常 + if (($mailResult['result'] ?? 1) === 0) { + Log::info('SHJ-1 失敗メール送信成功', [ + 'user_id' => $user->user_seq, + 'email' => $user->user_primemail + ]); + } else { + // 仕様準拠: error_info をログに記録 + Log::error('SHJ-1 失敗メール送信失敗', [ + 'user_id' => $user->user_seq, + 'email' => $user->user_primemail, + 'error_info' => $mailResult['error_info'] ?? 'メール送信失敗' + ]); + } + } catch (Exception $e) { + Log::error('SHJ-1 失敗メール送信例外エラー', [ + 'user_id' => $user->user_seq, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 分析用テキスト正規化 + */ + private function normalizeForAnalysis(string $text): string + { + // 空白・改行除去 + $text = preg_replace('/\s+/', '', $text); + + // 全角→半角統一 + $text = mb_convert_kana($text, 'rnask', 'UTF-8'); + + // 住所同義語統一 + $text = str_replace(['東京市', '東京府'], '東京都', $text); + $text = str_replace(['の'], '', $text); + + return $text; + } + + /** + * 文字レベル一致分析 + */ + private function analyzeCharacterMatches(string $expected, string $ocrText): array + { + $expectedChars = mb_str_split($expected, 1, 'UTF-8'); + $normalizedOcr = $this->normalizeForAnalysis($ocrText); + + $analysis = [ + 'total_chars' => count($expectedChars), + 'matched_chars' => 0, + 'missing_chars' => [], + 'match_rate' => 0 + ]; + + foreach ($expectedChars as $char) { + if (mb_strpos($normalizedOcr, $char, 0, 'UTF-8') !== false) { + $analysis['matched_chars']++; + } else { + $analysis['missing_chars'][] = $char; + } + } + + $analysis['match_rate'] = round(($analysis['matched_chars'] / $analysis['total_chars']) * 100, 2); + + return $analysis; + } + + /** + * バッチ処理ログ作成 + * + * SHJ-8サービスを呼び出してbat_job_logテーブルに登録 + * + * @param int $userId 利用者連番 + * @param string|null $userName 利用者名 + * @param float|null $similarityRate 類似度 + * @param array $result 処理結果 + * @return void + */ + private function createBatchLog(int $userId, ?string $userName, ?float $similarityRate, array $result): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + // ステータス判定 + $status = $result['system_success'] ? 'success' : 'error'; + + // ステータスコメント生成: {user_id}/{user_name}/{similarity_rate}% + $displayUserId = $userId; + $displayUserName = $userName ?? '不明'; + $displaySimilarity = $similarityRate !== null ? number_format($similarityRate, 1) : '0.0'; + + $statusComment = "{$displayUserId}/{$displayUserName}/{$displaySimilarity}%"; + + // SHJ-8サービス呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-1', + 'SHJ-1本人確認自動処理', + $status, + $statusComment, + $today, + $today + ); + + Log::info('SHJ-1 バッチ処理ログ作成完了', [ + 'user_id' => $userId, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + } catch (Exception $e) { + Log::error('SHJ-1 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'user_id' => $userId + ]); + } + } + +} \ No newline at end of file diff --git a/app/Services/ShjSixService.php b/app/Services/ShjSixService.php new file mode 100644 index 0000000..eee380e --- /dev/null +++ b/app/Services/ShjSixService.php @@ -0,0 +1,1275 @@ +mailSendService = $mailSendService; + $this->shjEightService = $shjEightService; + } + + /** + * SHJ-6 サーバ死活監視処理メイン実行 + * + * 処理フロー: + * 【処理1】サーバ死活監視(DBアクセス) + * 【処理2】デバイス管理マスタを取得する + * 【処理3】デバイス毎のハードウェア状態を取得する + * 【処理4】プリンタ制御プログラムログを取得する + * 【処理5】バッチ処理ログを作成する + * ※ 異常時は共通A処理を実行 + * + * @return array 処理結果 + */ + public function executeServerMonitoring(): array + { + $batchLogId = null; + $warnings = []; + $errorDetails = []; + $alertCount = 0; + $totalMailSuccessCount = 0; + $totalMailErrorCount = 0; + $totalQueueSuccessCount = 0; // キュー登録正常終了件数(仕様書準拠) + $totalQueueErrorCount = 0; // キュー登録異常終了件数(仕様書準拠) + $accumulatedBatchComment = ''; // 累積バッチコメント + + try { + // バッチ処理開始ログ(内部ログのみ、batch_log廃止) + Log::info('SHJ-6 サーバ死活監視処理開始'); + + // 【処理1】サーバ死活監視(DBアクセス) + $dbAccessResult = $this->checkDatabaseAccessWithSettings(); + + if (!$dbAccessResult['success']) { + // DB接続NGの場合は共通A処理実行(キュー種別:101、DB登録可否:0) + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + 'DB接続エラー: ' . $dbAccessResult['message'], + self::QUE_CLASS_SERVER_ERROR, + null, // park_id + null, // user_id + null, // contract_id + 0, // DB登録可否=0(登録不可) + 'サーバ死活監視:DB接続異常検出' + ); + + // メール送信統計を集計 + $totalMailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $commonAResult['mail_error_count'] ?? 0; + $accumulatedBatchComment = $commonAResult['updated_batch_comment'] ?? ''; + + return [ + 'success' => false, + 'message' => 'データベース接続エラーが発生しました', + 'error_details' => [$dbAccessResult['message']], + 'batch_log_id' => $batchLogId, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount + ]; + } + + // DB登録可否フラグを取得 + $dbRegisterFlag = $dbAccessResult['db_register_flag']; + + // 【処理2】デバイス管理マスタを取得する + $devices = $this->getServerDevices(); + Log::info('サーバーデバイス取得完了', [ + 'device_count' => count($devices) + ]); + + // 【処理3】デバイス毎のハードウェア状態を取得する + foreach ($devices as $device) { + $hardwareResult = $this->checkDeviceHardwareStatus($device, $batchLogId, $dbRegisterFlag); + if ($hardwareResult['has_alert']) { + $alertCount++; + $warnings[] = "デバイスID {$device->device_id}: ハードウェア異常検出"; + + // 仕様書準拠:メール送信統計とキュー登録統計を集計 + $totalMailSuccessCount += $hardwareResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $hardwareResult['mail_error_count'] ?? 0; + $totalQueueSuccessCount += $hardwareResult['queue_success_count'] ?? 0; + $totalQueueErrorCount += $hardwareResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($hardwareResult['updated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $hardwareResult['updated_batch_comment']; + } + } + } + + // 【処理4】プリンタ制御プログラムログを取得する + $printerResult = $this->checkPrinterErrorLogs($batchLogId, $dbRegisterFlag); + if ($printerResult['error_count'] > 0) { + $alertCount += $printerResult['error_count']; + $warnings[] = "プリンタエラー {$printerResult['error_count']}件検出"; + + // 仕様書準拠:メール送信統計とキュー登録統計を集計 + $totalMailSuccessCount += $printerResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $printerResult['mail_error_count'] ?? 0; + $totalQueueSuccessCount += $printerResult['queue_success_count'] ?? 0; + $totalQueueErrorCount += $printerResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($printerResult['accumulated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $printerResult['accumulated_batch_comment']; + } + } + + // 【処理5】バッチ処理ログを作成する - SHJ-8呼び出し + // 仕様書準拠:キュー登録件数も含める + $statusComment = sprintf( + 'デバイス数: %d, アラート件数: %d, メール正常: %d件, メール異常: %d件, キュー登録正常: %d件, キュー登録異常: %d件', + count($devices), + $alertCount, + $totalMailSuccessCount, + $totalMailErrorCount, + $totalQueueSuccessCount, + $totalQueueErrorCount + ); + + // バッチコメントが累積されている場合は追加 + if (!empty($accumulatedBatchComment)) { + $statusComment .= ' | ' . $accumulatedBatchComment; + } + + $status = 'success'; // 仕様書:常に"success"で記録 + $message = empty($warnings) ? + 'SHJ-6 サーバ死活監視処理正常完了' : + 'SHJ-6 サーバ死活監視処理完了(警告あり)'; + + // 仕様書準拠:処理2で取得したデバイスIDを連結 + $deviceIds = $devices->pluck('device_id')->toArray(); + $concatenatedDeviceIds = !empty($deviceIds) ? implode(',', $deviceIds) : ''; + + // SHJ-8 バッチ処理ログ作成(仕様書準拠) + // device_id = 処理2で取得したデバイスID(複数なら連結) + // process_name = 処理4のプロセス名(プリンタログから取得、なければ'SHJ-6') + // job_name = "SHJ-6サーバ死活監視" 固定 + // status = 常に "success" + $shj8Result = $this->createShjBatchLog([ + 'device_id' => $concatenatedDeviceIds, // 処理2のデバイスID(連結) + 'process_name' => $printerResult['process_name'] ?? 'SHJ-6', // 処理4のプロセス名 + 'job_name' => 'SHJ-6サーバ死活監視', + 'status' => 'success', // 仕様書:常にsuccess + 'status_comment' => $statusComment, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount, + 'queue_success_count' => $totalQueueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $totalQueueErrorCount, // 仕様書準拠 + 'device_count' => count($devices), + 'alert_count' => $alertCount + ]); + + // 仕様書準拠:SHJ-8の戻り値確認(処理結果 = 0以外なら異常) + if (!$shj8Result['success']) { + $shj8Error = $shj8Result['error'] ?? 'Unknown error'; + Log::warning('SHJ-8 バッチ処理ログ作成で異常が発生しました', [ + 'error' => $shj8Error + ]); + + // 仕様書準拠:異常時はバッチコメントへ反映 + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + sprintf('SHJ-8異常: %s', $shj8Error); + + // statusCommentも更新 + $statusComment .= sprintf(' | SHJ-8異常: %s', $shj8Error); + } + + Log::info('SHJ-6 サーバ死活監視処理完了', [ + 'status_comment' => $statusComment, + 'warnings' => $warnings, + 'alert_count' => $alertCount, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount + ]); + + // 監視サマリーを生成 + $monitoringSummary = sprintf( + '監視デバイス数: %d, アラート: %d件, メール成功: %d件', + count($devices), + $alertCount, + $totalMailSuccessCount + ); + + return [ + 'success' => true, + 'message' => 'SHJ-6 サーバ死活監視処理が完了しました', + 'monitoring_summary' => $monitoringSummary, + 'status_comment' => $statusComment, + 'warnings' => $warnings, + 'alert_count' => $alertCount, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-6 サーバ死活監視処理でエラーが発生: ' . $e->getMessage(); + + // 例外発生時も共通A処理実行(キュー種別:101、DB登録可否:0) + $commonAResult = $this->executeCommonProcessA( + null, // batch_log廃止のためnull + $errorMessage, + self::QUE_CLASS_SERVER_ERROR, + null, + null, + null, + 0, + 'サーバ死活監視:システムエラー発生' + ); + + $totalMailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $commonAResult['mail_error_count'] ?? 0; + + Log::error('SHJ-6 サーバ死活監視処理エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'monitoring_summary' => 'エラーにより監視中断', + 'error_details' => [$e->getMessage()], + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount + ]; + } + } + + /** + * 【処理1】サーバ死活監視(DBアクセス)- 設定マスタを使用 + * + * @return array アクセス結果 + */ + private function checkDatabaseAccessWithSettings(): array + { + try { + // 設定マスタを取得してDB接続確認 + $setting = Setting::getSettings(); + + if (!$setting) { + return [ + 'success' => false, + 'message' => '設定マスタが取得できませんでした', + 'db_register_flag' => 0 + ]; + } + + Log::info('データベース接続確認成功(設定マスタ取得)', [ + 'setting_id' => $setting->setting_id + ]); + + return [ + 'success' => true, + 'message' => 'データベース接続正常', + 'db_register_flag' => 1 // DB登録可能 + ]; + + } catch (\Exception $e) { + Log::error('データベース接続エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => $e->getMessage(), + 'db_register_flag' => 0 // DB登録不可 + ]; + } + } + + /** + * 【処理2】サーバーデバイスを取得する + * + * 条件: + * - device_type = 'サーバー' + * - device_work = '1' (稼働中) + * - device_workstart <= 現在日付 + * - device_replace IS NULL + * + * @return \Illuminate\Support\Collection デバイス情報 + */ + private function getServerDevices() + { + try { + $devices = DB::table('device') + ->select([ + 'device_id', + 'park_id', + 'device_type', + 'device_subject', + 'device_identifier', + 'device_work', + 'device_workstart', + 'device_remarks' + ]) + ->where('device_type', self::DEVICE_TYPE_SERVER) + ->where('device_work', self::DEVICE_WORK_ACTIVE) + ->where('device_workstart', '<=', now()) + ->whereNull('device_replace') + ->get(); + + Log::info('サーバーデバイス取得完了', [ + 'device_count' => $devices->count() + ]); + + return $devices; + + } catch (\Exception $e) { + Log::error('サーバーデバイス取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【処理3】デバイスのハードウェア状態をチェック + * + * 直近15分のハードウェアチェックログを確認し、異常時は共通A処理を実行 + * + * @param object $device デバイス情報 + * @param int|null $batchLogId バッチログID + * @param int $dbRegisterFlag DB登録可否 + * @return array チェック結果(メール送信統計含む) + */ + private function checkDeviceHardwareStatus($device, ?int $batchLogId, int $dbRegisterFlag): array + { + try { + // 直近15分のログを取得(最新1件) + $fifteenMinutesAgo = Carbon::now()->subMinutes(self::MONITOR_PERIOD_MINUTES); + + $latestLog = DB::table('hardware_check_log') + ->where('device_id', $device->device_id) + ->where('created_at', '>=', $fifteenMinutesAgo) + ->where('created_at', '<=', now()) + ->orderBy('created_at', 'desc') + ->first(); + + // ログが取得できない、または異常状態の場合 + if (!$latestLog) { + // ログが存在しない = 異常 + $alertMessage = sprintf( + 'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 理由=直近15分のログが存在しません', + $device->device_id, + $device->device_subject ?? 'N/A' + ); + + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + self::QUE_CLASS_SERVER_ERROR, + $device->park_id, + null, // user_id + null, // contract_id + $dbRegisterFlag, + 'ハードウェア監視:ログ未検出' + ); + + return [ + 'has_alert' => true, + 'reason' => 'ログなし', + 'mail_success_count' => $commonAResult['mail_success_count'] ?? 0, + 'mail_error_count' => $commonAResult['mail_error_count'] ?? 0, + 'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠 + 'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠 + 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' + ]; + } + + // status != 1(正常)の場合は異常 + if ($latestLog->status != HardwareCheckLog::STATUS_NORMAL) { + $statusName = HardwareCheckLog::getStatusName($latestLog->status); + $alertMessage = sprintf( + 'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 状態=%s, コメント=%s', + $device->device_id, + $device->device_subject ?? 'N/A', + $statusName, + $latestLog->status_comment ?? 'N/A' + ); + + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + self::QUE_CLASS_SERVER_ERROR, + $device->park_id, + null, + null, + $dbRegisterFlag, + 'ハードウェア監視:異常状態検出' + ); + + return [ + 'has_alert' => true, + 'reason' => '異常状態', + 'mail_success_count' => $commonAResult['mail_success_count'] ?? 0, + 'mail_error_count' => $commonAResult['mail_error_count'] ?? 0, + 'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠 + 'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠 + 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' + ]; + } + + // 正常 + return [ + 'has_alert' => false, + 'reason' => '正常', + 'mail_success_count' => 0, + 'mail_error_count' => 0 + ]; + + } catch (\Exception $e) { + Log::error('ハードウェア状態チェックエラー', [ + 'device_id' => $device->device_id, + 'error' => $e->getMessage() + ]); + + return [ + 'has_alert' => false, + 'reason' => 'エラー', + 'mail_success_count' => 0, + 'mail_error_count' => 0 + ]; + } + } + + /** + * 【処理4】プリンタエラーログをチェック + * + * 直近15分のプリンタエラーログ(status >= 100)を確認し、 + * エラーログごとに共通A処理を実行 + * + * 仕様書準拠:ステータス >= 100 を条件とする + * + * @param int|null $batchLogId バッチログID + * @param int $dbRegisterFlag DB登録可否 + * @return array チェック結果 + */ + private function checkPrinterErrorLogs(?int $batchLogId, int $dbRegisterFlag): array + { + try { + // 直近15分のエラーログを取得 + $fifteenMinutesAgo = Carbon::now()->subMinutes(self::MONITOR_PERIOD_MINUTES); + + // 仕様書: ステータス >= 100 + // status は varchar型のため CAST で数値変換して比較 + $errorLogs = DB::table('print_job_log') + ->where('created_at', '>=', $fifteenMinutesAgo) + ->where('created_at', '<=', now()) + ->whereRaw('CAST(status AS SIGNED) >= 100') + ->orderBy('created_at', 'desc') + ->get(); + + Log::info('プリンタエラーログ取得完了', [ + 'error_count' => $errorLogs->count(), + 'period_minutes' => self::MONITOR_PERIOD_MINUTES + ]); + + // メール送信統計とキュー登録統計(仕様書準拠) + $mailSuccessCount = 0; + $mailErrorCount = 0; + $queueSuccessCount = 0; + $queueErrorCount = 0; + $accumulatedBatchComment = ''; + + // エラーログごとに共通A処理を実行 + foreach ($errorLogs as $log) { + // 仕様書準拠:statusの値に応じてキュー種別を判定 + // - status 200番台 → 102(プリンタエラー) + // - status 300番台 → 103(スキャナエラー) + // - status 400番台 → 104(プリンタ用紙残少警報) + $statusValue = (int)$log->status; + if ($statusValue >= 400 && $statusValue < 500) { + $queClass = self::QUE_CLASS_PAPER_WARNING; // 104 正しい定数名 + $errorType = 'プリンタ用紙残少警報'; + } elseif ($statusValue >= 300 && $statusValue < 400) { + $queClass = self::QUE_CLASS_SCANNER_ERROR; // 103 + $errorType = 'スキャナエラー'; + } else { + $queClass = self::QUE_CLASS_PRINTER_ERROR; // 102(デフォルト) + $errorType = 'プリンタエラー'; + } + + $alertMessage = sprintf( + '%s: ステータス=%s, プロセス=%s, ジョブ=%s, エラーコード=%d, コメント=%s', + $errorType, + $log->status ?? 'N/A', + $log->process_name ?? 'N/A', + $log->job_name ?? 'N/A', + $log->error_code ?? 0, + $log->status_comment ?? 'N/A' + ); + + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + $queClass, // ステータス値に応じて102/103/104 + $log->park_id, + $log->user_id, + $log->contract_id, + $dbRegisterFlag, + sprintf('プリンタ制御:%s検出', $errorType) + ); + + // 仕様書準拠:メール送信結果とキュー登録結果を集計 + $mailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $mailErrorCount += $commonAResult['mail_error_count'] ?? 0; + $queueSuccessCount += $commonAResult['queue_success_count'] ?? 0; + $queueErrorCount += $commonAResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($commonAResult['updated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $commonAResult['updated_batch_comment']; + } + } + + // 仕様書準拠:処理4のプロセス名を返却(複数件を連結) + $processNames = []; + foreach ($errorLogs as $log) { + if (!empty($log->process_name)) { + $processNames[] = $log->process_name; + } + } + // 重複を除去して連結 + $processNames = array_unique($processNames); + $concatenatedProcessNames = !empty($processNames) ? implode(',', $processNames) : null; + + return [ + 'success' => true, + 'error_count' => $errorLogs->count(), + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $queueErrorCount, // 仕様書準拠 + 'accumulated_batch_comment' => $accumulatedBatchComment, + 'process_name' => $concatenatedProcessNames // 処理4のプロセス名(複数件連結) + ]; + + } catch (\Exception $e) { + Log::error('プリンタエラーログチェックエラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error_count' => 0 + ]; + } + } + + /** + * 共通A処理:監視結果を反映 + * + * 仕様書準拠: + * - SHJ-7 メール送信結果を集計(正常終了件数/異常終了件数) + * - 異常時はバッチコメントに SHJ-7 異常情報を追記 + * - 更新されたバッチコメントでオペレータキュー登録 + * + * @param int|null $batchLogId バッチログID + * @param string $alertMessage アラートメッセージ + * @param int $queClass 対象キュー種別ID(101〜104) + * @param int|null $parkId 駐輪場ID + * @param int|null $userId 利用者ID + * @param int|null $contractId 定期契約ID + * @param int $dbRegisterFlag DB登録可否(0=不可、1=可) + * @param string|null $batchComment バッチコメント + * @return array 処理結果(メール送信統計含む) + */ + private function executeCommonProcessA( + ?int $batchLogId, + string $alertMessage, + int $queClass, + ?int $parkId, + ?int $userId, + ?int $contractId, + int $dbRegisterFlag, + ?string $batchComment + ): array { + // メール送信結果統計 + $mailSuccessCount = 0; + $mailErrorCount = 0; + $mailErrorDetails = []; + + // キュー登録結果統計(仕様書準拠) + $queueSuccessCount = 0; + $queueErrorCount = 0; + $registeredQueId = null; // 登録したキューID(後で更新するため) + + try { + Log::info('共通A処理開始', [ + 'batch_log_id' => $batchLogId, + 'alert_message' => $alertMessage, + 'que_class' => $queClass, + 'park_id' => $parkId, + 'db_register_flag' => $dbRegisterFlag + ]); + + // 【共通判断1】DB反映可否判定 + if ($dbRegisterFlag === 1) { + // DB登録可能な場合 + + // 仕様書準拠:バッチコメント = 処理1.定期契約ID + 元のバッチコメント + $updatedBatchComment = sprintf( + '定期契約ID: %s / %s', + $contractId ?? 'なし', + $batchComment ?? '' + ); + + // 【共通処理1】オペレータキューを登録する(仕様書順序準拠) + // 注:SHJ-7異常情報はメール送信後に追記 + $queueResult = $this->registerOperatorQueue( + $alertMessage, + $batchLogId, + $queClass, + $parkId, + $userId, + $contractId, + $updatedBatchComment + ); + + // 仕様書準拠:キュー登録正常終了件数/異常終了件数を更新 + if ($queueResult['success']) { + $queueSuccessCount++; + $registeredQueId = $queueResult['que_id']; // キューIDを保存 + } else { + $queueErrorCount++; + $updatedBatchComment .= sprintf( + ' | キュー登録異常: %s', + $queueResult['error'] ?? 'Unknown error' + ); + } + + // 【共通処理2】メール送信対象オペレータを取得する + $operators = $this->getMailTargetOperators($queClass, $parkId); + + // 【共通判断2】送信対象有無 + if (!empty($operators)) { + foreach ($operators as $operator) { + $result = $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); + + // SHJ-7 メール送信結果を集計 + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + 'オペレータ[%s]へのメール送信失敗: %s', + $operator['email'], + $result['error'] ?? 'Unknown error' + ); + } + } + } + + // 【共通処理3】駐輪場管理者を取得する + $parkManagers = $this->getParkManagers($parkId); + + // 【共通判断3】送信対象有無 + if (!empty($parkManagers)) { + foreach ($parkManagers as $manager) { + $result = $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); + + // SHJ-7 メール送信結果を集計 + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + '駐輪場管理者[%s]へのメール送信失敗: %s', + $manager['email'], + $result['error'] ?? 'Unknown error' + ); + } + } + } + + // 仕様書準拠:メール異常時にSHJ-7異常情報を追記 + if ($mailErrorCount > 0) { + $updatedBatchComment .= sprintf( + ' | SHJ-7メール異常: %d件 (%s)', + $mailErrorCount, + implode('; ', $mailErrorDetails) + ); + } + + // 仕様書準拠:キュー登録後、SHJ-7異常情報を含めてキューコメントを更新 + // キューコメント = 内部変数.バッチコメント(定期契約ID + SHJ-7異常情報) + if ($registeredQueId && $mailErrorCount > 0) { + try { + OperatorQue::where('que_id', $registeredQueId)->update([ // 主キーはque_id + 'que_comment' => $updatedBatchComment, // 仕様書:キューコメントに追記 + 'updated_at' => now() + ]); + + Log::info('オペレータキューコメント更新完了(SHJ-7異常情報追記)', [ + 'que_id' => $registeredQueId, + 'updated_comment' => $updatedBatchComment + ]); + } catch (\Exception $e) { + Log::error('オペレータキューコメント更新エラー', [ + 'que_id' => $registeredQueId, + 'error' => $e->getMessage() + ]); + } + } + + } else { + // DB反映NGの場合は固定メールアドレスに緊急メール送信 + // 仕様書準拠:テンプレート不使用、件名固定、本文なし + $result = $this->sendEmergencyMail( + self::FIXED_EMAIL_ADDRESS, + $alertMessage + ); + + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + '固定アドレス[%s]への緊急メール送信失敗: %s', + self::FIXED_EMAIL_ADDRESS, + $result['error'] ?? 'Unknown error' + ); + } + + $updatedBatchComment = $batchComment ?? ''; + if ($mailErrorCount > 0) { + $updatedBatchComment .= sprintf( + ' | 緊急メール異常: %d件 (%s)', + $mailErrorCount, + implode('; ', $mailErrorDetails) + ); + } + } + + Log::info('共通A処理完了', [ + 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'updated_batch_comment' => $updatedBatchComment ?? $batchComment + ]); + + return [ + 'success' => true, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'mail_error_details' => $mailErrorDetails, + 'queue_success_count' => $queueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $queueErrorCount, // 仕様書準拠 + 'updated_batch_comment' => $updatedBatchComment ?? $batchComment + ]; + + } catch (\Exception $e) { + Log::error('共通A処理エラー', [ + 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, + 'error' => $e->getMessage() + ]); + + $updatedBatchComment = ($batchComment ?? '') . ' | 共通A処理エラー: ' . $e->getMessage(); + + return [ + 'success' => false, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'mail_error_details' => $mailErrorDetails, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'updated_batch_comment' => $updatedBatchComment, + 'error' => $e->getMessage() + ]; + } + } + + /** + * オペレータキューを登録 + * + * 仕様書準拠: + * - 定期契約IDをバッチコメントに含める + * - 登録の成否とque_idを返却(後で更新するため) + * + * @param string $alertMessage アラートメッセージ + * @param int|null $batchLogId バッチログID + * @param int $queClass 対象キュー種別ID(101〜104) + * @param int|null $parkId 駐輪場ID + * @param int|null $userId 利用者ID + * @param int|null $contractId 定期契約ID + * @param string|null $batchComment バッチコメント(SHJ-7異常情報は後で追記) + * @return array 登録結果 ['success' => bool, 'error' => string|null, 'que_id' => int|null] + */ + private function registerOperatorQueue( + string $alertMessage, + ?int $batchLogId, + int $queClass, + ?int $parkId, + ?int $userId, + ?int $contractId, + ?string $batchComment + ): array { + try { + // 仕様書準拠(todo/SHJ-6/SHJ-6.txt:170-182): + // - キューコメント = 内部変数.バッチコメント + // - キューステータスコメント = "" (空) + // - 業務指示コメント = "" (空) + + $operatorQue = OperatorQue::create([ + 'que_class' => $queClass, + 'user_id' => $userId, + 'contract_id' => $contractId, + 'park_id' => $parkId, + 'que_comment' => $batchComment ?? '', // 仕様書:内部変数.バッチコメント(SHJ-7異常情報は後で追記) + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', // 仕様書:空文字列 + 'work_instructions' => '', // 仕様書:空文字列 + 'operator_id' => 9999999, // 仕様書準拠:固定値 + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー登録完了', [ + 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, + 'que_id' => $operatorQue->que_id, // 主キーはque_id + 'park_id' => $parkId, + 'user_id' => $userId, + 'contract_id' => $contractId + ]); + + return [ + 'success' => true, + 'error' => null, + 'que_id' => $operatorQue->que_id // 仕様書準拠:主キーque_idを返却 + ]; + + } catch (\Exception $e) { + Log::error('オペレータキュー登録エラー', [ + 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'que_id' => null + ]; + } + } + + /** + * メール送信対象オペレータを取得 + * + * 管轄駐輪場マスタをJOINして、指定駐輪場を管轄しているオペレータのみ取得 + * + * キュー種別IDに応じた送信フラグをチェック: + * - 101(サーバーエラー) → ope_sendalart_que10 + * - 102(プリンタエラー) → ope_sendalart_que11 + * - 103(スキャナエラー) → ope_sendalart_que12 + * - 104(プリンタ用紙残少) → ope_sendalart_que13 + * + * @param int $queClass 対象キュー種別ID + * @param int|null $parkId 駐輪場ID(nullの場合は全オペレータ) + * @return array オペレータ一覧 + */ + private function getMailTargetOperators(int $queClass, ?int $parkId): array + { + try { + // キュー種別IDに対応する送信フラグカラム名を決定 + $alertFlagColumn = $this->getOperatorAlertFlagColumn($queClass); + + if (empty($alertFlagColumn)) { + Log::warning('不正なキュー種別ID', ['que_class' => $queClass]); + return []; + } + + // 管轄駐輪場マスタをJOINしてオペレータを取得 + $query = DB::table('ope as T1') + ->select(['T1.ope_id', 'T1.ope_name', 'T1.ope_mail']) + ->where('T1.' . $alertFlagColumn, 1) // 該当アラート送信フラグが有効 + ->where('T1.ope_quit_flag', 0) // 退職していない + ->whereNotNull('T1.ope_mail') + ->where('T1.ope_mail', '!=', ''); + + // 駐輪場IDが指定されている場合は管轄駐輪場マスタでフィルタ + if ($parkId !== null) { + $query->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id') + ->where('T2.park_id', $parkId); + } + + $operators = $query->get() + ->map(function ($ope) { + return [ + 'ope_id' => $ope->ope_id, + 'name' => $ope->ope_name, + 'email' => $ope->ope_mail + ]; + }) + ->toArray(); + + Log::info('メール送信対象オペレータ取得完了', [ + 'que_class' => $queClass, + 'park_id' => $parkId, + 'alert_flag_column' => $alertFlagColumn, + 'operator_count' => count($operators) + ]); + + return $operators; + + } catch (\Exception $e) { + Log::error('メール送信対象オペレータ取得エラー', [ + 'que_class' => $queClass, + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * キュー種別IDに対応するオペレータ送信フラグカラム名を取得 + * + * @param int $queClass キュー種別ID + * @return string|null カラム名 + */ + private function getOperatorAlertFlagColumn(int $queClass): ?string + { + $mapping = [ + 101 => 'ope_sendalart_que10', // サーバーエラー + 102 => 'ope_sendalart_que11', // プリンタエラー + 103 => 'ope_sendalart_que12', // スキャナエラー + 104 => 'ope_sendalart_que13', // プリンタ用紙残少警報 + ]; + + return $mapping[$queClass] ?? null; + } + + /** + * 駐輪場管理者を取得 + * + * 駐輪場管理者マスタから指定駐輪場の管理者を取得: + * - 退職フラグ = 0(在職中) + * - メールアドレスが設定されている + * - 所属駐輪場ID = 指定駐輪場ID + * + * @param int|null $parkId 駐輪場ID(nullの場合は全管理者) + * @return array 駐輪場管理者一覧 + */ + private function getParkManagers(?int $parkId): array + { + try { + $query = Manager::active()->hasEmail(); + + // 駐輪場IDが指定されている場合はフィルタ + if ($parkId !== null) { + $query->where('manager_parkid', $parkId); + } + + $managers = $query->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) + ->get() + ->map(function ($manager) { + return [ + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid + ]; + }) + ->toArray(); + + Log::info('駐輪場管理者取得完了', [ + 'park_id' => $parkId, + 'manager_count' => count($managers) + ]); + + return $managers; + + } catch (\Exception $e) { + Log::error('駐輪場管理者取得エラー', [ + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * アラートメールを送信(SHJ-7) + * + * 仕様書準拠: + * - SHJ-7 メール送信サービスを呼び出し + * - 戻り値(成功/失敗、異常情報)を返却 + * + * @param string $email メールアドレス + * @param string $alertMessage アラートメッセージ + * @param string $recipientType 受信者タイプ + * @return array 送信結果 ['success' => bool, 'error' => string|null] + */ + private function sendAlertMail(string $email, string $alertMessage, string $recipientType): array + { + try { + // SHJ-7 メール送信機能を使用(メールテンプレートID=202を使用) + $result = $this->mailSendService->executeMailSend( + $email, + '', // 予備メールアドレスは空 + self::SYSTEM_ALERT_MAIL_TEMPLATE_ID + ); + + // SHJ-7仕様準拠: result === 0 が正常、それ以外は異常 + if (($result['result'] ?? 1) === 0) { + Log::info('アラートメール送信成功', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'alert_message' => $alertMessage + ]); + + return [ + 'success' => true, + 'error' => null + ]; + } else { + // 仕様準拠: error_info を使用 + $errorInfo = $result['error_info'] ?? 'メール送信失敗'; + + Log::error('アラートメール送信失敗', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error_info' => $errorInfo + ]); + + return [ + 'success' => false, + 'error' => $errorInfo + ]; + } + + } catch (\Exception $e) { + Log::error('アラートメール送信エラー', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + /** + * DB反映不可時の緊急メール送信 + * + * 仕様書準拠: + * - テンプレート不使用 + * - 件名: 「So-Manager:死活監視DBアタッチエラー(SHJ-6)」固定 + * - 本文: なし(空) + * - 固定アドレスに直接送信 + * + * @param string $email 送信先メールアドレス + * @param string $alertMessage アラートメッセージ(ログ用) + * @return array 送信結果 ['success' => bool, 'error' => string|null] + */ + private function sendEmergencyMail(string $email, string $alertMessage): array + { + try { + // 仕様書準拠:件名固定、本文なし + $subject = 'So-Manager:死活監視DBアタッチエラー(SHJ-6)'; + $body = ''; // 本文なし + + // Laravelの Mail facade を使用して直接送信 + \Illuminate\Support\Facades\Mail::raw($body, function ($message) use ($email, $subject) { + $message->to($email) + ->subject($subject); + }); + + Log::info('緊急メール送信成功', [ + 'email' => $email, + 'subject' => $subject, + 'alert_message' => $alertMessage + ]); + + return [ + 'success' => true, + 'error' => null + ]; + + } catch (\Exception $e) { + Log::error('緊急メール送信エラー', [ + 'email' => $email, + 'alert_message' => $alertMessage, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + /** + * 【処理5】SHJ-8 バッチ処理ログ作成 + * + * 仕様書に基づくSHJ-8共通処理呼び出し + * + * @param array $statistics 処理統計情報 + * @return array 処理結果 ['success' => bool, 'error' => string|null] + */ + private function createShjBatchLog(array $statistics): array + { + try { + // 仕様書準拠のSHJ-8パラメータ設定 + // device_id: 処理2で取得したデバイスID(複数ある場合は最初のIDを使用) + // process_name: 処理4のプロセス名 + $deviceIdString = $statistics['device_id']; // 連結されたデバイスID文字列 "1,2,3" + + // 複数デバイスIDがある場合は最初のIDを使用(SHJ-8はint型を期待) + $deviceIdArray = !empty($deviceIdString) ? explode(',', $deviceIdString) : []; + $deviceId = !empty($deviceIdArray) ? (int)$deviceIdArray[0] : 1; + + $processName = $statistics['process_name'] ?? 'SHJ-6'; + $jobName = $statistics['job_name']; // "SHJ-6サーバ死活監視" 固定 + $status = $statistics['status']; // 常に "success" + $statusComment = $statistics['status_comment'] ?? ''; + + $createdDate = now()->format('Y/m/d'); + $updatedDate = now()->format('Y/m/d'); + + Log::info('SHJ-8 バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'device_id_original' => $deviceIdString, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + $processName, + $jobName, + $status, + $statusComment, + $createdDate, + $updatedDate + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了', [ + 'device_id' => $deviceId, + 'process_name' => $processName + ]); + + return [ + 'success' => true, + 'error' => null + ]; + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'statistics' => $statistics + ]); + + // 仕様書準拠:SHJ-8でエラーが発生してもメイン処理は継続 + // エラー情報を返却 + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } +} diff --git a/app/Services/ShjTenService.php b/app/Services/ShjTenService.php new file mode 100644 index 0000000..6220395 --- /dev/null +++ b/app/Services/ShjTenService.php @@ -0,0 +1,885 @@ +shjEightService = $shjEightService; + } + + /** + * SHJ-10 財政年度売上集計処理メイン実行 + * + * 処理フロー (todo/SHJ-10/SHJ-10.txt): + * 【処理1】集計対象を設定する + * 【処理2】駐輪場マスタを取得する + * 【判断1】取得件数判定 + * 【処理3】車種区分毎に算出する + * 【判断2】取得判定 + * 【処理4】売上集計結果を削除→登録する + * 【処理5】バッチ処理ログを作成し、情報不備がある場合のみオペレータキューを作成する + * + * @param string $type 集計種別(yearly/monthly) + * @param string $target 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @return array 処理結果 + */ + public function executeFiscalEarningsAggregation(string $type, string $target, array $fiscalPeriod): array + { + $statusComments = []; // 内部変数.ステータスコメント + $dataIntegrityIssues = []; // 内部変数.情報不備 + + try { + // 【処理1】集計対象を設定する(財政年度ベース) + $aggregationTarget = $this->setFiscalAggregationTarget($fiscalPeriod); + + Log::info('SHJ-10 売上集計処理開始', [ + 'type' => $type, + 'target' => $target, + 'fiscal_period' => $fiscalPeriod, + 'aggregation_target' => $aggregationTarget + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $typeLabel = $this->getTypeLabel($type); + $statusComment = "売上集計{$typeLabel}:駐輪場マスタが存在していません。"; + $statusComments[] = $statusComment; + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($statusComment, null); + + // SHJ-8 バッチ処理ログ作成 + $this->callShjEight('SHJ-10売上集計(年次・月次)', 'success', $statusComment); + + return [ + 'success' => true, + 'message' => $statusComment, + 'processed_parks' => 0, + 'summary_records' => 0 + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $result = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod); + + $processedParks++; + $summaryRecords += $result['summary_records']; + + // 対象データなしの場合のステータスコメント収集 + if (!empty($result['no_data_message'])) { + $statusComments[] = $result['no_data_message']; + } + + // 情報不備を収集("なし"でない場合) + if ($result['data_integrity_issue'] !== '情報不備:なし') { + $dataIntegrityIssues[] = $result['data_integrity_issue']; + } + } + + // 最終ステータスコメント生成 + $typeLabel = $this->getTypeLabel($type); + $finalStatusComment = "売上集計{$typeLabel}:対象={$fiscalPeriod['target_label']}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}"; + if (!empty($dataIntegrityIssues)) { + $finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues); + } + + // 【処理5】オペレータキュー作成 + // ※ 駐輪場単位で既に作成済み(processFiscalEarningsForPark内で情報不備検出時に実施) + if (!empty($dataIntegrityIssues)) { + Log::warning('SHJ-10 情報不備検出', [ + 'issues' => $dataIntegrityIssues + ]); + } + + // SHJ-8 バッチ処理ログ作成 + $this->callShjEight('SHJ-10売上集計(年次・月次)', 'success', $finalStatusComment); + + Log::info('SHJ-10 売上集計処理完了', [ + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), + 'no_data_parks' => count($statusComments) + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-10 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues) + ]; + + } catch (\Exception $e) { + $typeLabel = $this->getTypeLabel($type ?? 'unknown'); + $errorMessage = "売上集計{$typeLabel}:エラー発生 - " . $e->getMessage(); + + // SHJ-8 バッチ処理ログ作成(エラー時も作成) + try { + $this->callShjEight('SHJ-10売上集計(年次・月次)', 'error', $errorMessage); + } catch (\Exception $shjException) { + Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); + } + + Log::error('SHJ-10 売上集計処理エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage() + ]; + } + } + + /** + * 【処理1】財政年度集計対象を設定する + * + * @param array $fiscalPeriod 財政期間情報 + * @return array 集計対象情報 + */ + private function setFiscalAggregationTarget(array $fiscalPeriod): array + { + return [ + 'type' => $fiscalPeriod['type'], + 'start_date' => $fiscalPeriod['start_date'], + 'end_date' => $fiscalPeriod['end_date'], + 'earnings_date' => $fiscalPeriod['earnings_date'], + 'summary_type' => $fiscalPeriod['summary_type'], + 'fiscal_year' => $fiscalPeriod['fiscal_year'], + 'target_label' => $fiscalPeriod['target_label'] + ]; + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * @return array 駐輪場情報 + */ + private function getParkInformation(): array + { + try { + $parkInfo = DB::table('park') + ->select(['park_id', 'park_name']) + ->where('park_close_flag', '<>', 1) + ->orderBy('park_ruby') + ->get() + ->toArray(); + + Log::info('駐輪場マスタ取得完了', [ + 'park_count' => count($parkInfo) + ]); + + return $parkInfo; + + } catch (\Exception $e) { + Log::error('駐輪場マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 駐輪場毎の財政年度売上集計処理 + * + * @param object $park 駐輪場情報 + * @param array $aggregationTarget 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] + */ + private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): array + { + try { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // 0. 情報不備チェック + $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $startDate, $endDate); + + // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-10/SHJ-10.txt:289-299) + if ($dataIntegrityIssue !== '情報不備:なし') { + $this->createOperatorQueue($dataIntegrityIssue, $park->park_id); + } + + // ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + $regularData = $this->calculateRegularEarnings($park->park_id, $startDate, $endDate); + + // ② 一時金データ取得(車種毎) + $lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $startDate, $endDate); + + // ③ 解約返戻金データ取得(車種区分毎) + $refundData = $this->calculateRefundEarnings($park->park_id, $startDate, $endDate); + + // ④ 再発行データ取得(車種区分毎) + $reissueData = $this->calculateReissueCount($park->park_id, $startDate, $endDate); + + // 【判断2】データがいずれかあれば【処理4】へ + if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) { + // 対象データなし - 仕様 todo/SHJ-10/SHJ-10.txt:209-211 + $typeLabel = $this->getTypeLabel($aggregationTarget['type']); + $noDataMessage = "売上集計{$typeLabel}:{$startDate}~{$endDate}/駐輪場:{$park->park_name}:売上データが存在しません。"; + + return [ + 'summary_records' => 0, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => $noDataMessage + ]; + } + + // 【処理4】売上集計結果を登録(削除は createFiscalEarningsSummary 内で実施) + $summaryRecords = 0; + + // ① 定期契約データがある場合:同じ組合せ(psection×usertype×months)を統合 + $mergedRegularData = $this->mergeRegularDataByGroup($regularData); + foreach ($mergedRegularData as $key => $mergedRow) { + $this->createFiscalEarningsSummary($park, $mergedRow, $aggregationTarget, $fiscalPeriod, 'regular'); + $summaryRecords++; + } + + // ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約) + $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); + foreach ($otherDataByPsection as $psectionId => $data) { + $this->createFiscalEarningsSummary($park, $data, $aggregationTarget, $fiscalPeriod, 'other'); + $summaryRecords++; + } + + Log::info('駐輪場売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue + ]); + + return [ + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => null + ]; + + } catch (\Exception $e) { + Log::error('駐輪場売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 0. 情報不備チェック(期間集計版) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:77-101 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return string 情報不備メッセージ(仕様フォーマット:"情報不備:xxx" or "情報不備:なし") + */ + private function checkDataIntegrity(int $parkId, string $startDate, string $endDate): string + { + $incompleteContracts = DB::table('regular_contract') + ->select('contract_id') + ->where('park_id', $parkId) + ->where('contract_flag', 1) + ->whereBetween(DB::raw('DATE(contract_payment_day)'), [$startDate, $endDate]) + ->where(function($query) { + $query->whereNull('update_flag') + ->orWhereNull('psection_id') + ->orWhereNull('enable_months'); + }) + ->pluck('contract_id') + ->toArray(); + + if (empty($incompleteContracts)) { + return '情報不備:なし'; + } + + // 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り + return '情報不備:' . implode(',', $incompleteContracts); + } + + /** + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + * ※ 金額・件数は全て0固定だが、分類や月数の組み合わせごとにレコードが必要 + * + * 仕様 todo/SHJ-10/SHJ-10.txt:104-128 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + /** + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:104-129 + * SQL定義:減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、 + * 授受金額の合計と件数を算出する + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateRegularEarnings(int $parkId, string $startDate, string $endDate): array + { + $results = DB::table('regular_contract as T1') + ->join('usertype as T3', 'T1.user_categoryid', '=', 'T3.user_categoryid') + ->select([ + DB::raw('IFNULL(T1.contract_reduction, 0) as contract_reduction'), + 'T1.update_flag', + 'T1.psection_id', + 'T3.usertype_subject1', + 'T1.enable_months', + DB::raw('SUM(T1.contract_money) as total_amount'), // 仕様:授受金額の合計 + DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様:授受件数 + ]) + ->where('T1.park_id', $parkId) + ->where('T1.contract_flag', 1) + ->whereBetween(DB::raw('DATE(T1.contract_payment_day)'), [$startDate, $endDate]) + ->whereNotNull('T1.update_flag') + ->whereNotNull('T1.psection_id') + ->whereNotNull('T1.enable_months') + ->groupBy([ + DB::raw('IFNULL(T1.contract_reduction, 0)'), + 'T1.update_flag', + 'T1.psection_id', + 'T3.usertype_subject1', + 'T1.enable_months' + ]) + ->get(); + + return $results->toArray(); + } + + /** + * ② 一時金データ取得(車種毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:148-159 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateLumpsumEarnings(int $parkId, string $startDate, string $endDate): array + { + $results = DB::table('lumpsum_transaction') + ->select([ + 'type_class as psection_id', + DB::raw('COUNT(*) as lumpsum_count'), + DB::raw('COALESCE(SUM(deposit_amount), 0) as lumpsum') + ]) + ->where('park_id', $parkId) + ->whereBetween(DB::raw('DATE(pay_date)'), [$startDate, $endDate]) + ->groupBy('type_class') + ->get(); + + return $results->toArray(); + } + + /** + * ③ 解約返戻金データ取得(車種区分毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:160-171 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateRefundEarnings(int $parkId, string $startDate, string $endDate): array + { + $results = DB::table('regular_contract') + ->select([ + 'psection_id', + DB::raw('COALESCE(SUM(refunds), 0) as refunds') + ]) + ->where('park_id', $parkId) + ->where('contract_cancel_flag', 1) + ->whereBetween(DB::raw('DATE(repayment_at)'), [$startDate, $endDate]) + ->groupBy('psection_id') + ->get(); + + return $results->toArray(); + } + + /** + * ④ 再発行データ取得(車種区分毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:172-183 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateReissueCount(int $parkId, string $startDate, string $endDate): array + { + $results = DB::table('seal') + ->select([ + 'psection_id', + DB::raw('COUNT(contract_id) as reissue_count') + ]) + ->where('park_id', $parkId) + ->where('contract_seal_issue', '>=', 2) + ->whereBetween(DB::raw('DATE(seal_day)'), [$startDate, $endDate]) + ->groupBy('psection_id') + ->get(); + + return $results->toArray(); + } + + /** + * 定期契約データを組合せ毎に統合 + * + * SQLは contract_reduction × update_flag で分組しているため、 + * 同じ psection × usertype × months の組合せで複数行が返る場合がある。 + * ここで park_id + psection_id + usertype_subject1 + enable_months をキーに統合し、 + * 新規/更新 × 減免/通常 の各件数・金額を1つのオブジェクトに集約する。 + * + * 仕様 todo/SHJ-10/SHJ-10.txt:130-147 + * + * @param array $regularData calculateRegularEarnings()の結果 + * @return array キー:psection_id|usertype|months、値:統合されたデータオブジェクト + */ + private function mergeRegularDataByGroup(array $regularData): array + { + $merged = []; + + foreach ($regularData as $row) { + // 統合キー:psection_id|usertype_subject1|enable_months + $key = $row->psection_id . '|' . $row->usertype_subject1 . '|' . $row->enable_months; + + // 初回作成 + if (!isset($merged[$key])) { + $merged[$key] = (object)[ + 'psection_id' => $row->psection_id, + 'usertype_subject1' => $row->usertype_subject1, + 'enable_months' => $row->enable_months, + // 新規・通常 + 'regular_new_count' => 0, + 'regular_new_amount' => 0, + // 新規・減免 + 'regular_new_reduction_count' => 0, + 'regular_new_reduction_amount' => 0, + // 更新・通常 + 'regular_update_count' => 0, + 'regular_update_amount' => 0, + // 更新・減免 + 'regular_update_reduction_count' => 0, + 'regular_update_reduction_amount' => 0 + ]; + } + + // 区分判定 + $isNew = in_array($row->update_flag, [2, null]); // 新規 + $isReduction = ($row->contract_reduction == 1); // 減免 + + $count = $row->contract_count ?? 0; + $amount = $row->total_amount ?? 0; + + // 対応するフィールドに累加 + if ($isNew && !$isReduction) { + // 新規・通常 + $merged[$key]->regular_new_count += $count; + $merged[$key]->regular_new_amount += $amount; + } elseif ($isNew && $isReduction) { + // 新規・減免 + $merged[$key]->regular_new_reduction_count += $count; + $merged[$key]->regular_new_reduction_amount += $amount; + } elseif (!$isNew && !$isReduction) { + // 更新・通常 + $merged[$key]->regular_update_count += $count; + $merged[$key]->regular_update_amount += $amount; + } elseif (!$isNew && $isReduction) { + // 更新・減免 + $merged[$key]->regular_update_reduction_count += $count; + $merged[$key]->regular_update_reduction_amount += $amount; + } + } + + return $merged; + } + + /** + * 一時金・解約・再発行データを車種区分毎に統合 + * + * @param array $lumpsumData + * @param array $refundData + * @param array $reissueData + * @return array + */ + private function mergeOtherEarningsData(array $lumpsumData, array $refundData, array $reissueData): array + { + $merged = []; + + // 一時金 + foreach ($lumpsumData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->lumpsum_count = $row->lumpsum_count; + $merged[$psectionId]->lumpsum = $row->lumpsum; + } + + // 解約返戻金 + foreach ($refundData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->refunds = $row->refunds; + } + + // 再発行 + foreach ($reissueData as $row) { + $psectionId = $row->psection_id; + if (!isset($merged[$psectionId])) { + $merged[$psectionId] = (object)[ + 'psection_id' => $psectionId, + 'usertype_subject1' => null, + 'enable_months' => 0, + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'reissue_count' => 0 + ]; + } + $merged[$psectionId]->reissue_count = $row->reissue_count; + } + + return $merged; + } + + /** + * 【処理4】既存の売上集計結果を削除 + * + * 仕様書のキー:駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数 + * 仕様書どおり summary_start_date と summary_end_date は NULL で保存されているため、NULL で検索する + * + * @param int $parkId 駐輪場ID + * @param array $aggregationTarget 集計対象 + * @param int $psectionId 車種区分ID + * @param string|null $usertypeSubject1 分類名1 + * @param int $enableMonths 定期有効月数 + * @return void + */ + private function deleteExistingFiscalSummary( + int $parkId, + array $aggregationTarget, + int $psectionId, + ?string $usertypeSubject1, + int $enableMonths + ): void { + // 仕様書どおり、同一キーの組み合わせで削除 + $query = DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_type', $aggregationTarget['summary_type']) + ->whereNull('summary_start_date') + ->whereNull('summary_end_date') + ->where('earnings_date', $aggregationTarget['earnings_date']) + ->where('psection_id', $psectionId) + ->where('enable_months', $enableMonths); + + // 分類名1がnullの場合はIS NULLで検索 + if ($usertypeSubject1 === null) { + $query->whereNull('usertype_subject1'); + } else { + $query->where('usertype_subject1', $usertypeSubject1); + } + + $deletedCount = $query->delete(); + + if ($deletedCount > 0) { + Log::debug('既存の売上集計結果削除', [ + 'park_id' => $parkId, + 'summary_type' => $aggregationTarget['summary_type'], + 'earnings_date' => $aggregationTarget['earnings_date'], + 'psection_id' => $psectionId, + 'usertype_subject1' => $usertypeSubject1, + 'enable_months' => $enableMonths, + 'deleted_count' => $deletedCount + ]); + } + } + + /** + * 売上集計結果を登録(財政年度ベース) + * + * 仕様書: 同一キーのレコードが存在する場合は先に削除してから登録 + * + * @param object $park 駐輪場情報 + * @param object $data 売上データ + * @param array $aggregationTarget 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @param string $dataType データ種別(regular or other) + * @return void + */ + private function createFiscalEarningsSummary($park, $data, array $aggregationTarget, array $fiscalPeriod, string $dataType): void + { + // 仕様書: 同一キーのレコードを先に削除 + $this->deleteExistingFiscalSummary( + $park->park_id, + $aggregationTarget, + $data->psection_id, + $data->usertype_subject1 ?? null, + $data->enable_months ?? 0 + ); + + $insertData = [ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], // 1=年次, 2=月次 + 'summary_start_date' => null, // 仕様書: null + 'summary_end_date' => null, // 仕様書: null + 'earnings_date' => $aggregationTarget['earnings_date'], // 仕様書: 年次=年度(yyyy-01-01), 月次=年月(yyyy-mm-01) + 'psection_id' => $data->psection_id, + 'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名 + 'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数 + 'summary_note' => "SHJ-10:{$fiscalPeriod['target_label']}/{$aggregationTarget['start_date']}~{$aggregationTarget['end_date']}", // 仕様: 集計備考 + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様: バッチオペレータID) + ]; + + if ($dataType === 'regular') { + // 定期契約データの場合:mergeRegularDataByGroup()で既に統合済み + // 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 130-147) + $insertData = array_merge($insertData, [ + 'regular_new_count' => $data->regular_new_count ?? 0, + 'regular_new_amount' => $data->regular_new_amount ?? 0, + 'regular_new_reduction_count' => $data->regular_new_reduction_count ?? 0, + 'regular_new_reduction_amount' => $data->regular_new_reduction_amount ?? 0, + 'regular_update_count' => $data->regular_update_count ?? 0, + 'regular_update_amount' => $data->regular_update_amount ?? 0, + 'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0, + 'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0, + 'lumpsum_count' => 0, // 仕様line 140 + 'lumpsum' => 0, // 仕様line 141 + 'refunds' => 0, // 仕様line 142 + 'other_income' => 0, // 仕様line 143 + 'other_spending' => 0, // 仕様line 144 + 'reissue_count' => 0, // 仕様line 145 + 'reissue_amount' => 0 // 仕様line 146 + ]); + } else { + // 一時金・解約・再発行データの場合:定期フィールドは0固定 (仕様line 186-202) + $insertData = array_merge($insertData, [ + 'regular_new_count' => 0, // 仕様line 186 + 'regular_new_amount' => 0, // 仕様line 187 + 'regular_new_reduction_count' => 0, // 仕様line 188 + 'regular_new_reduction_amount' => 0, // 仕様line 189 + 'regular_update_count' => 0, // 仕様line 190 + 'regular_update_amount' => 0, // 仕様line 191 + 'regular_update_reduction_count' => 0, // 仕様line 192 + 'regular_update_reduction_amount' => 0, // 仕様line 193 + 'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 194 + 'lumpsum' => $data->lumpsum ?? 0, // 仕様line 195 + 'refunds' => $data->refunds ?? 0, // 仕様line 196 + 'other_income' => 0, // 仕様line 197 + 'other_spending' => 0, // 仕様line 198 + 'reissue_count' => $data->reissue_count ?? 0, // 仕様line 199 + 'reissue_amount' => 0 // 仕様line 200: 0固定 + ]); + } + + DB::table('earnings_summary')->insert($insertData); + + Log::debug('売上集計結果登録', [ + 'park_id' => $park->park_id, + 'psection_id' => $data->psection_id, + 'data_type' => $dataType, + 'summary_type' => $aggregationTarget['summary_type'] + ]); + } + + /** + * 【処理5】オペレータキュー作成(駐輪場単位・情報不備がある場合のみ) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:289-317 + * - que_class: 14(集計対象エラー) + * - que_comment: 空(仕様書line 297) + * - que_status: 1(キュー発生) + * - que_status_comment: 空(仕様書line 299) + * - work_instructions: 情報不備メッセージ(仕様書line 300) + * - park_id: 駐輪場ID(仕様 "処理1.駐輪場ID"、パラメータエラー時はnull) + * - operator_id: 9999999(バッチ処理固定値) + * + * @param string $message 情報不備メッセージ + * @param int|null $parkId 駐輪場ID(パラメータエラー時はnull) + * @return void + */ + private function createOperatorQueue(string $message, ?int $parkId = null): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 14, // 集計対象エラー + 'user_id' => null, + 'contract_id' => null, + 'park_id' => $parkId, // 仕様:処理1.駐輪場ID + 'que_comment' => '', // 仕様: 空 + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', // 仕様: 空 + 'work_instructions' => $message, // 仕様: 情報不備 + 'operator_id' => self::BATCH_OPERATOR_ID, // 9999999 + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'park_id' => $parkId, + 'que_class' => 14, + 'que_status' => 1, + 'operator_id' => self::BATCH_OPERATOR_ID, + 'work_instructions' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * SHJ-8 バッチ処理ログ作成 + * + * 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す + * + * @param string $jobName ジョブ名 + * @param string $status ステータス (success/error) + * @param string $statusComment 業務固有のステータスコメント + * @return void + */ + private function callShjEight(string $jobName, string $status, string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + $today = now()->format('Y/m/d'); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-10', + $jobName, + $status, + $statusComment, + $today, + $today + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了', [ + 'job_name' => $jobName, + 'status' => $status + ]); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'job_name' => $jobName, + 'status_comment' => $statusComment + ]); + throw $e; + } + } + + /** + * 集計種別のラベル取得 + * + * @param string $type 集計種別 + * @return string ラベル + */ + private function getTypeLabel(string $type): string + { + switch ($type) { + case 'yearly': + return '(年次)'; + case 'monthly': + return '(月次)'; + default: + return ''; + } + } +} diff --git a/app/Services/ShjThirteenService.php b/app/Services/ShjThirteenService.php new file mode 100644 index 0000000..1461de7 --- /dev/null +++ b/app/Services/ShjThirteenService.php @@ -0,0 +1,380 @@ +shjEightService = $shjEightService; + } + /** + * SHJ-13 契約台数追加処理実行 + * + * 【処理1】契約台数を反映する - park_number・zone テーブルの契約台数を+1更新 + * 【処理2】バッチ処理ログを作成する - SHJ-8共通仕様でログ登録 + * + * @param array $contractData 契約データ + * @return array 処理結果 + */ + public function execute(array $contractData): array + { + $startTime = now(); + + Log::info('SHJ-13 契約台数追加処理開始', [ + 'contract_id' => $contractData['contract_id'] ?? null, + 'park_id' => $contractData['park_id'] ?? null, + 'psection_id' => $contractData['psection_id'] ?? null, + 'ptype_id' => $contractData['ptype_id'] ?? null, + 'zone_id' => $contractData['zone_id'] ?? null, + ]); + + try { + // パラメータ検証 + $validationResult = $this->validateParameters($contractData); + if (!$validationResult['valid']) { + return $this->createErrorResult( + 1001, + 'パラメータエラー: ' . $validationResult['message'] + ); + } + + // 【処理1・2】契約台数反映とバッチログ作成を一体で実行 + $processResult = $this->executeProcessWithLogging($contractData); + if (!$processResult['success']) { + return $this->createErrorResult( + $processResult['error_code'], + $processResult['error_message'], + $processResult['stack_trace'] ?? '' + ); + } + + $statusComment = $processResult['status_comment']; + + $endTime = now(); + Log::info('SHJ-13 契約台数追加処理完了', [ + 'contract_id' => $contractData['contract_id'], + 'execution_time' => $startTime->diffInSeconds($endTime), + 'updated_count' => $processResult['updated_count'], + 'status_comment' => $statusComment, + ]); + + return ['result' => 0]; + + } catch (\Throwable $e) { + Log::error('SHJ-13 契約台数追加処理例外エラー', [ + 'contract_data' => $contractData, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return $this->createErrorResult( + $e->getCode() ?: 1999, + $e->getMessage(), + $e->getTraceAsString() + ); + } + } + + /** + * パラメータ検証 + * + * @param array $contractData + * @return array + */ + private function validateParameters(array $contractData): array + { + $errors = []; + + if (empty($contractData['park_id'])) { + $errors[] = '駐輪場IDが設定されていません'; + } + + if (empty($contractData['psection_id'])) { + $errors[] = '車種区分IDが設定されていません'; + } + + if (empty($contractData['ptype_id'])) { + $errors[] = '駐輪分類IDが設定されていません'; + } + + if (empty($contractData['zone_id'])) { + $errors[] = 'ゾーンIDが設定されていません'; + } + + if (!empty($errors)) { + return [ + 'valid' => false, + 'message' => implode(', ', $errors), + 'details' => $errors, + ]; + } + + return ['valid' => true]; + } + + /** + * 契約台数反映とバッチログ作成の一体処理 + * + * park_number・zone テーブルの契約台数を+1更新し、バッチログを作成 + * 全てを1つのトランザクション内で実行 + * + * @param array $contractData + * @return array + */ + private function executeProcessWithLogging(array $contractData): array + { + try { + return DB::transaction(function() use ($contractData) { + // 各テーブルの名称取得 + $names = $this->getTableNames($contractData); + if (!$names['success']) { + throw new \Exception('名称取得エラー: ' . $names['message'], 1002); + } + + // 【JOB1】zone テーブル更新(仕様書SQL-1) + // WHERE: 駐輪場ID, 車種区分ID, 駐輪分類ID, ゾーンID + // SET: 現在契約台数+1, 更新日時, 更新オペレータID + $zoneUpdated = DB::table('zone') + ->where('park_id', $contractData['park_id']) + ->where('psection_id', $contractData['psection_id']) + ->where('ptype_id', $contractData['ptype_id']) + ->where('zone_id', $contractData['zone_id']) + ->increment('zone_number', 1, [ + 'updated_at' => now(), + 'ope_id' => 9999999, // 仕様書: 更新オペレータID + ]); + + if ($zoneUpdated === 0) { + throw new \Exception('zoneテーブルの対象レコードが存在しません', 1012); + } + + // 更新後の契約台数取得 + $updatedZone = DB::table('zone') + ->where('park_id', $contractData['park_id']) + ->where('psection_id', $contractData['psection_id']) + ->where('ptype_id', $contractData['ptype_id']) + ->where('zone_id', $contractData['zone_id']) + ->first(['zone_number']); + + // ステータスコメント構築 + $statusComment = $this->buildStatusComment($names['names'], $updatedZone->zone_number); + + // バッチ処理ログ作成(同一トランザクション内) + // SHJ-8サービスを呼び出し + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-13', + 'SHJ-13 契約台数追加', + 'success', + $statusComment, + $today, + $today + ); + + Log::info('SHJ-13 契約台数更新・ログ作成完了', [ + 'contract_id' => $contractData['contract_id'], + 'zone_updated' => $zoneUpdated, + 'updated_count' => $updatedZone->zone_number, + 'status_comment' => $statusComment, + ]); + + return [ + 'success' => true, + 'updated_count' => $updatedZone->zone_number, + 'status_comment' => $statusComment, + ]; + }); + + } catch (\Throwable $e) { + Log::error('SHJ-13 契約台数更新・ログ作成エラー', [ + 'contract_data' => $contractData, + 'error_code' => $e->getCode(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return [ + 'success' => false, + 'error_code' => $e->getCode() ?: 1010, + 'error_message' => $e->getMessage(), + 'stack_trace' => $e->getTraceAsString(), + ]; + } + } + + /** + * テーブル名称取得 + * + * @param array $contractData + * @return array + */ + private function getTableNames(array $contractData): array + { + try { + // 駐輪場名取得 + $park = DB::table('park') + ->where('park_id', $contractData['park_id']) + ->first(['park_name']); + + if (!$park) { + return [ + 'success' => false, + 'message' => "駐輪場が見つかりません: park_id={$contractData['park_id']}", + ]; + } + + // 駐輪分類名取得 + $ptype = DB::table('ptype') + ->where('ptype_id', $contractData['ptype_id']) + ->first(['ptype_subject']); + + if (!$ptype) { + return [ + 'success' => false, + 'message' => "駐輪分類が見つかりません: ptype_id={$contractData['ptype_id']}", + ]; + } + + // 車種区分名取得 + $psection = DB::table('psection') + ->where('psection_id', $contractData['psection_id']) + ->first(['psection_subject']); + + if (!$psection) { + return [ + 'success' => false, + 'message' => "車種区分が見つかりません: psection_id={$contractData['psection_id']}", + ]; + } + + // ゾーン名取得 + $zone = DB::table('zone') + ->where('zone_id', $contractData['zone_id']) + ->first(['zone_name']); + + if (!$zone) { + return [ + 'success' => false, + 'message' => "ゾーンが見つかりません: zone_id={$contractData['zone_id']}", + ]; + } + + return [ + 'success' => true, + 'names' => [ + 'park_name' => $park->park_name, + 'ptype_subject' => $ptype->ptype_subject, + 'psection_subject' => $psection->psection_subject, + 'zone_name' => $zone->zone_name, + ], + ]; + + } catch (\Throwable $e) { + return [ + 'success' => false, + 'message' => 'テーブル名称取得エラー: ' . $e->getMessage(), + 'details' => $e->getTraceAsString(), + ]; + } + } + + /** + * ステータスコメント構築 + * + * 形式:駐輪場名/駐輪分類名/車種区分名/ゾーン名/現在契約台数(更新後):{数値}/ + * + * @param array $names + * @param int $updatedCount + * @return string + */ + private function buildStatusComment(array $names, int $updatedCount): string + { + return sprintf( + '%s/%s/%s/%s/現在契約台数(更新後):%d/', + $names['park_name'], + $names['ptype_subject'], + $names['psection_subject'], + $names['zone_name'], + $updatedCount + ); + } + + + /** + * エラー結果作成(SHJ-8ログも作成) + * + * @param int $errorCode + * @param string $errorMessage + * @param string $stackTrace + * @return array + */ + private function createErrorResult(int $errorCode, string $errorMessage, string $stackTrace = ''): array + { + // エラー時もSHJ-8でバッチログを作成 + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + $statusComment = sprintf( + 'エラーコード:%d/エラーメッセージ:%s', + $errorCode, + $errorMessage + ); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-13', + 'SHJ-13 契約台数追加', + 'success', // 仕様書:エラー時も"success"で記録 + $statusComment, + $today, + $today + ); + + Log::info('SHJ-13 エラー時バッチログ作成完了', [ + 'error_code' => $errorCode, + 'status_comment' => $statusComment + ]); + } catch (\Exception $e) { + Log::error('SHJ-13 エラー時バッチログ作成失敗', [ + 'error' => $e->getMessage() + ]); + } + + return [ + 'result' => 1, + 'error_code' => $errorCode, + 'error_message' => $errorMessage, + 'stack_trace' => $stackTrace, + ]; + } +} diff --git a/app/Services/ShjThreeService.php b/app/Services/ShjThreeService.php new file mode 100644 index 0000000..e8c4763 --- /dev/null +++ b/app/Services/ShjThreeService.php @@ -0,0 +1,903 @@ +parkModel = $parkModel; + $this->userModel = $userModel; + $this->contractModel = $contractModel; + $this->operatorQueModel = $operatorQueModel; + $this->mailSendService = $mailSendService; + $this->shjEightService = $shjEightService; + } + + /** + * SHJ-3 定期更新リマインダー処理メイン実行 + * + * 処理フロー(仕様書準拠): + * 【処理0】駐輪場マスタの情報を取得する + * 【判断0】当該駐輪場実行タイミングチェック + * 【処理2】定期更新対象者を取得する + * 【判断2】利用者有無をチェック + * 【処理3】対象者向けにメール送信、またはオペレーターキュー追加処理 + * 【処理4】バッチ処理ログを作成する(各駐輪場ごとに実行) + * + * @return array 処理結果 + */ + public function executeReminderProcess(): array + { + $overallProcessedParksCount = 0; + $overallTotalTargetUsers = 0; + $overallMailSuccessCount = 0; + $overallMailErrorCount = 0; + $overallQueueSuccessCount = 0; + $overallQueueErrorCount = 0; + + try { + Log::info('SHJ-3 定期更新リマインダー処理開始'); + + // 【処理0】駐輪場マスタの情報を取得する + $parkList = $this->getParkMasterInfo(); + + if (empty($parkList)) { + $message = '対象の駐輪場マスタが見つかりません'; + Log::warning($message); + + // 駐輪場が見つからない場合でも実行ログを記録 + $this->createOverallBatchLog(0, 0, 0, 0, 0, 0); + + return [ + 'success' => false, + 'message' => $message, + 'processed_parks_count' => 0, + 'total_target_users' => 0, + 'mail_success_count' => 0, + 'mail_error_count' => 0, + 'operator_queue_count' => 0 + ]; + } + + // 取得レコード数分【判断0】を繰り返す + foreach ($parkList as $park) { + // 各駐輪場ごとの内部変数(仕様書:場景A) + $mailSuccessCount = 0; + $mailErrorCount = 0; + $queueSuccessCount = 0; + $queueErrorCount = 0; + $batchComment = ''; + + Log::info('駐輪場処理開始', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name + ]); + + // 【判断0】当該駐輪場実行タイミングチェック + $timingCheckResult = $this->checkExecutionTiming($park); + + if (!$timingCheckResult['should_execute']) { + Log::info('実行タイミング対象外', [ + 'park_id' => $park->park_id, + 'reason' => $timingCheckResult['reason'] + ]); + // 次の駐輪場マスタへ + continue; + } + + $overallProcessedParksCount++; + + // 【処理2】定期更新対象者を取得する + $targetUsers = $this->getRegularUpdateTargetUsers( + $park, + $timingCheckResult['update_pattern'] + ); + + // 【判断2】利用者有無をチェック + if (empty($targetUsers)) { + // 仕様書:利用者なしの結果を設定する + $batchComment = "定期更新リマインダー:今月の定期更新対象者は無しです / {$park->park_name}"; + + Log::info('利用者なし', [ + 'park_id' => $park->park_id, + 'batch_comment' => $batchComment + ]); + + // 【処理4】バッチ処理ログを作成する + $this->createShjBatchLog( + $park, + $batchComment, + $mailSuccessCount, + $mailErrorCount, + $queueSuccessCount, + $queueErrorCount + ); + + // 次の駐輪場マスタへ + continue; + } + + $overallTotalTargetUsers += count($targetUsers); + + // 【処理3】処理2の対象レコード数分繰り返す + foreach ($targetUsers as $targetUser) { + $processResult = $this->processTargetUser($targetUser, $park); + + if ($processResult['type'] === 'mail_success') { + $mailSuccessCount++; + } elseif ($processResult['type'] === 'mail_error') { + $mailErrorCount++; + // バッチコメントに異常情報を追加 + if (!empty($processResult['error_info'])) { + $batchComment .= ($batchComment ? ' / ' : '') . $processResult['error_info']; + } + } elseif ($processResult['type'] === 'queue_success') { + $queueSuccessCount++; + } elseif ($processResult['type'] === 'queue_error') { + $queueErrorCount++; + } + } + + // 全体集計用に加算 + $overallMailSuccessCount += $mailSuccessCount; + $overallMailErrorCount += $mailErrorCount; + $overallQueueSuccessCount += $queueSuccessCount; + $overallQueueErrorCount += $queueErrorCount; + + Log::info('駐輪場処理完了', [ + 'park_id' => $park->park_id, + 'target_users_count' => count($targetUsers), + 'mail_success' => $mailSuccessCount, + 'mail_error' => $mailErrorCount, + 'queue_success' => $queueSuccessCount, + 'queue_error' => $queueErrorCount + ]); + + // 【処理4】バッチ処理ログを作成する(各駐輪場ごと) + $this->createShjBatchLog( + $park, + $batchComment, + $mailSuccessCount, + $mailErrorCount, + $queueSuccessCount, + $queueErrorCount + ); + } + + Log::info('SHJ-3 定期更新リマインダー処理完了', [ + 'processed_parks_count' => $overallProcessedParksCount, + 'total_target_users' => $overallTotalTargetUsers, + 'mail_success_count' => $overallMailSuccessCount, + 'mail_error_count' => $overallMailErrorCount, + 'queue_success_count' => $overallQueueSuccessCount, + 'queue_error_count' => $overallQueueErrorCount + ]); + + // 駐輪場が0件でも全体の実行ログを記録する + $this->createOverallBatchLog( + $overallProcessedParksCount, + $overallTotalTargetUsers, + $overallMailSuccessCount, + $overallMailErrorCount, + $overallQueueSuccessCount, + $overallQueueErrorCount + ); + + return [ + 'success' => true, + 'message' => 'SHJ-3 定期更新リマインダー処理が正常に完了しました', + 'processed_parks_count' => $overallProcessedParksCount, + 'total_target_users' => $overallTotalTargetUsers, + 'mail_success_count' => $overallMailSuccessCount, + 'mail_error_count' => $overallMailErrorCount, + 'operator_queue_count' => $overallQueueSuccessCount + $overallQueueErrorCount + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-3 定期更新リマインダー処理でエラーが発生: ' . $e->getMessage(); + + Log::error('SHJ-3 定期更新リマインダー処理エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // エラー時も実行ログを記録 + $this->createOverallBatchLog( + $overallProcessedParksCount, + $overallTotalTargetUsers, + $overallMailSuccessCount, + $overallMailErrorCount, + $overallQueueSuccessCount, + $overallQueueErrorCount + ); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'processed_parks_count' => $overallProcessedParksCount, + 'total_target_users' => $overallTotalTargetUsers, + 'mail_success_count' => $overallMailSuccessCount, + 'mail_error_count' => $overallMailErrorCount, + 'operator_queue_count' => $overallQueueSuccessCount + $overallQueueErrorCount + ]; + } + } + + /** + * 【処理0】駐輪場マスタの情報を取得する + * + * 仕様書に基づくSQLクエリ: + * SELECT 駐輪場ID, 駐輪場名, 更新期間開始日, 更新期間開始時, + * 更新期間終了日, 更新期間終了時, リマインダー種別, リマインダー時間 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ = 0 + * ORDER BY 駐輪場ふりがな asc + * + * @return array 駐輪場マスタ情報 + */ + private function getParkMasterInfo(): array + { + try { + $parkInfo = DB::table('park') + ->select([ + 'park_id', // 駐輪場ID + 'park_name', // 駐輪場名 + 'update_grace_period_start_date', // 更新期間開始日(例:"20") + 'update_grace_period_start_time', // 更新期間開始時(例:"09:00") + 'update_grace_period_end_date', // 更新期間終了日(例:"6") + 'update_grace_period_end_time', // 更新期間終了時(例:"23:59") + 'reminder_type', // リマインダー種別(0=毎日、1=1日おき、2=2日おき) + 'reminder_time' // リマインダー時間(例:"09:00") + ]) + ->where('park_close_flag', 0) // 閉設フラグ = 0 + ->orderBy('park_ruby', 'asc') // 駐輪場ふりがな 昇順 + ->get() + ->toArray(); + + Log::info('駐輪場マスタ情報取得完了', [ + 'park_count' => count($parkInfo) + ]); + + return $parkInfo; + + } catch (\Exception $e) { + Log::error('駐輪場マスタ情報取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【判断0】当該駐輪場実行タイミングチェック + * + * 仕様書に基づく実行タイミング判定: + * 1. リマインダー時間 = 現在の時間 のチェック + * 2. 内部変数.更新パターン の設定(A or B) + * 3. 内部変数.更新期間開始日からの経過日数 の算出 + * 4. 内部変数.実行フラグ の判定 + * + * @param object $park 駐輪場情報 + * @return array 実行タイミング判定結果 + */ + private function checkExecutionTiming($park): array + { + try { + $now = Carbon::now(); + $currentTime = $now->format('H:i'); + $todayDay = (int)$now->format('d'); // 本日の日(1-31) + + Log::info('実行タイミングチェック開始', [ + 'park_id' => $park->park_id, + 'current_time' => $currentTime, + 'today_day' => $todayDay, + 'reminder_time' => $park->reminder_time, + 'reminder_type' => $park->reminder_type + ]); + + // 仕様書:駐輪場マスタ.リマインダー時間 = [現在の時間] の場合 + if ($park->reminder_time !== $currentTime) { + return [ + 'should_execute' => false, + 'reason' => "リマインダー時間不一致(設定:{$park->reminder_time} vs 現在:{$currentTime})" + ]; + } + + // 内部変数.更新パターン を設定 + // DBから返る値は文字列なので、型変換して使用 + $startDay = (int)$park->update_grace_period_start_date; // 例:"20" → 20 + $endDay = (int)$park->update_grace_period_end_date; // 例:"6" → 6 + + $updatePattern = ''; + if ($startDay <= $endDay) { + // パターンA: 月を跨らない場合 + $updatePattern = 'A'; + } else { + // パターンB: 月を跨る場合 + $updatePattern = 'B'; + } + + // 内部変数.更新期間開始日からの経過日数 を設定 + $elapsedDays = 99; // デフォルト: 対象外 + + if ($updatePattern === 'A') { + // パターンA の場合 + if ($endDay < $todayDay) { + // 駐輪場マスタ.更新期間終了日 > [本日の日付]の日 の場合 + $elapsedDays = 99; // 対象外 + } elseif ($startDay <= $todayDay) { + // 駐輪場マスタ.更新期間開始日 <= [本日の日付]の日 の場合 + $elapsedDays = $todayDay - $startDay; + } else { + // その他の場合 + $elapsedDays = 99; // 対象外 + } + } else { + // パターンB の場合 + if ($startDay <= $todayDay) { + // 駐輪場マスタ.更新期間開始日 <= [本日の日付]の日 の場合 + $elapsedDays = $todayDay - $startDay; + } elseif ($endDay >= $todayDay) { + // 駐輪場マスタ.更新期間終了日 >= [本日の日付]の日 の場合 + // 仕様書の計算式: ([先月の月末日]の日 − 駐輪場マスタ.更新期間開始日) + [本日の日付]の日 + $lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->day; + $elapsedDays = ($lastMonthEnd - $startDay) + $todayDay; + } else { + // その他の場合 + $elapsedDays = 99; // 対象外 + } + } + + Log::info('経過日数算出完了', [ + 'park_id' => $park->park_id, + 'update_pattern' => $updatePattern, + 'start_day' => $startDay, + 'end_day' => $endDay, + 'today_day' => $todayDay, + 'elapsed_days' => $elapsedDays + ]); + + // 内部変数.実行フラグ を設定 + $executionFlag = 0; + + if ($elapsedDays !== 99) { + // DBから返る値は文字列なので、型変換して比較 + $reminderType = (int)($park->reminder_type ?? 0); + + if ($reminderType === 0) { + // 仕様書:毎日 + $executionFlag = 1; + } elseif ($reminderType === 1) { + // 仕様書:1日おき(経過日数が偶数の場合) + $executionFlag = ($elapsedDays % 2 === 0) ? 1 : 0; + } elseif ($reminderType === 2) { + // 仕様書:2日おき(経過日数を3で割った余りが0の場合) + $executionFlag = ($elapsedDays % 3 === 0) ? 1 : 0; + } else { + // あり得ない + $executionFlag = 0; + } + } + + $shouldExecute = ($executionFlag === 1); + + Log::info('実行タイミングチェック完了', [ + 'park_id' => $park->park_id, + 'update_pattern' => $updatePattern, + 'elapsed_days' => $elapsedDays, + 'reminder_type' => $park->reminder_type, + 'execution_flag' => $executionFlag, + 'should_execute' => $shouldExecute + ]); + + return [ + 'should_execute' => $shouldExecute, + 'reason' => $shouldExecute ? '実行対象' : "実行フラグ=0(経過日数:{$elapsedDays}, リマインダー種別:{$park->reminder_type})", + 'update_pattern' => $updatePattern, + 'elapsed_days' => $elapsedDays, + 'execution_flag' => $executionFlag + ]; + + } catch (\Exception $e) { + Log::error('実行タイミングチェックエラー', [ + 'park_id' => $park->park_id ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + + /** + * 【処理2】定期更新対象者を取得する + * + * 仕様書に基づくSQLクエリ: + * 定期契約マスタ T1 と 利用者マスタ T2 を結合して更新対象者の情報を取得する + * + * WHERE条件: + * - T1.駐輪場ID = 処理0.駐輪場ID + * - T1.更新可能日 <= 本日の日付 + * - T1.解約フラグ = 0 + * - T2.退会フラグ = 0 + * - T1.授受フラグ = 1 + * - T1.更新済フラグ is null + * - 更新パターンによる有効期間Eの判定 + * + * @param object $park 駐輪場情報 + * @param string $updatePattern 更新パターン("A" or "B") + * @return array 定期更新対象者情報 + */ + private function getRegularUpdateTargetUsers($park, string $updatePattern): array + { + try { + $now = Carbon::now(); + $currentDate = $now->format('Y-m-d'); + $todayDay = (int)$now->format('d'); // 本日の日(1-31) + // DBから返る値は文字列なので、型変換して使用 + $startDay = (int)$park->update_grace_period_start_date; // 例:"20" → 20 + $endDay = (int)$park->update_grace_period_end_date; // 例:"6" → 6 + + // 有効期間E(契約終了日)の判定 + $thisMonthEnd = $now->copy()->endOfMonth()->format('Y-m-d'); + $lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->format('Y-m-d'); + + $query = DB::table('regular_contract as T1') + ->select([ + 'T1.contract_id as 定期契約ID', + 'T1.park_id as 駐輪場ID', + 'T2.user_seq as 利用者ID', // user_seqが主キー + 'T2.user_manual_regist_flag as 手動登録フラグ', + 'T2.user_primemail as メールアドレス', + 'T2.user_submail as 予備メールアドレス', + 'T2.user_name as 氏名', + 'T1.contract_periode as 有効期間E' + ]) + ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') // user_seqに結合 + ->where('T1.park_id', $park->park_id) // 駐輪場ID + ->where('T1.contract_updated_at', '<=', $currentDate) // 更新可能日 + ->where('T1.contract_cancel_flag', 0) // 解約フラグ = 0 + ->where('T2.user_quit_flag', 0) // 退会フラグ = 0 + ->where('T1.contract_flag', 1) // 授受フラグ = 1 + ->whereNull('T1.contract_renewal'); // 更新済フラグ is null + + // 仕様書:更新パターンによる有効期間Eの判定 + if ($updatePattern === 'A') { + // パターンA の場合: 有効期間E = 今月末 + $query->where('T1.contract_periode', '=', $thisMonthEnd); + } else { + // パターンB の場合 + if ($startDay <= $todayDay) { + // 処理0.更新期間開始日 <= [本日の日付]の日 の場合 + $query->where('T1.contract_periode', '=', $thisMonthEnd); + } elseif ($endDay >= $todayDay) { + // 処理0.更新期間終了日 >= [本日の日付]の日 の場合 + $query->where('T1.contract_periode', '=', $lastMonthEnd); + } + } + + $targetUsers = $query->get()->toArray(); + + Log::info('定期更新対象者取得完了', [ + 'park_id' => $park->park_id, + 'update_pattern' => $updatePattern, + 'this_month_end' => $thisMonthEnd, + 'last_month_end' => $lastMonthEnd, + 'target_users_count' => count($targetUsers) + ]); + + return $targetUsers; + + } catch (\Exception $e) { + Log::error('定期更新対象者取得エラー', [ + 'park_id' => $park->park_id ?? 'unknown', + 'update_pattern' => $updatePattern ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + /** + * 【処理3】対象者の処理実行 + * + * 手動登録フラグによって処理を分岐: + * - = 0 (ウェブ申込み): SHJ-7メール送信を呼び出し + * - その他: オペレーターキュー追加処理 + * + * @param object $targetUser 対象者情報 + * @param object $park 駐輪場情報 + * @return array 処理結果 + */ + private function processTargetUser($targetUser, $park): array + { + try { + $manualRegistFlag = $targetUser->手動登録フラグ ?? 1; + + if ($manualRegistFlag == 0) { + // 仕様書:手動登録フラグ = 0(ウェブ申込み)の場合 + // SHJ-7メール送信処理を呼び出し + return $this->sendReminderMail($targetUser); + } else { + // 仕様書:手動登録フラグ <> 0 の場合 + // オペレーターキュー追加処理 + // ※文書には詳細仕様なし。他のService実装を参考に実装 + return $this->addToOperatorQueue($targetUser, $park); + } + + } catch (\Exception $e) { + Log::error('対象者処理エラー', [ + 'user_id' => $targetUser->利用者ID ?? 'unknown', + 'contract_id' => $targetUser->定期契約ID ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'type' => 'error', + 'success' => false, + 'message' => $e->getMessage() + ]; + } + } + + /** + * リマインダーメール送信処理 + * + * 仕様書:SHJ-7メール送信を呼び出し、使用プログラムID=200を使用 + * + * @param object $targetUser 対象者情報 + * @return array 送信結果 + */ + private function sendReminderMail($targetUser): array + { + try { + $mailAddress = $targetUser->メールアドレス ?? ''; + $backupMailAddress = $targetUser->予備メールアドレス ?? ''; + $mailTemplateId = 200; // 仕様書:使用プログラムID = 200 + + Log::info('SHJ-7メール送信処理呼び出し', [ + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約ID, + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ]); + + // 仕様書:共通処理「SHJ-7 メール送信」を呼び出し + $mailResult = $this->mailSendService->executeMailSend( + $mailAddress, + $backupMailAddress, + $mailTemplateId + ); + + // SHJ-7仕様準拠: result === 0 が正常、それ以外は異常 + if (($mailResult['result'] ?? 1) === 0) { + // 仕様書:処理結果 = 0(正常)の場合 + Log::info('メール送信成功', [ + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約ID + ]); + + return [ + 'type' => 'mail_success', + 'success' => true, + 'message' => 'メール送信成功', + 'error_info' => null + ]; + } else { + // 仕様書:その他の場合(異常) + // バッチコメントに「処理2.定期契約ID」+「SHJ-7 メール送信.異常情報」を設定する(後ろに足す) + // 仕様準拠: error_info を使用 + $shjSevenErrorInfo = $mailResult['error_info'] ?? 'メール送信失敗'; + $errorInfo = "定期契約ID:{$targetUser->定期契約ID} / SHJ-7 メール送信.異常情報:{$shjSevenErrorInfo}"; + + Log::warning('メール送信失敗', [ + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約ID, + 'error_info' => $shjSevenErrorInfo + ]); + + return [ + 'type' => 'mail_error', + 'success' => false, + 'message' => 'メール送信失敗', + 'error_info' => $errorInfo + ]; + } + + } catch (\Exception $e) { + Log::error('リマインダーメール送信エラー', [ + 'user_id' => $targetUser->利用者ID ?? 'unknown', + 'contract_id' => $targetUser->定期契約ID ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // 仕様書準拠のエラー情報フォーマット + $errorInfo = "定期契約ID:{$targetUser->定期契約ID} / SHJ-7 メール送信.異常情報:例外エラー - {$e->getMessage()}"; + + return [ + 'type' => 'mail_error', + 'success' => false, + 'message' => 'メール送信例外エラー', + 'error_info' => $errorInfo + ]; + } + } + + /** + * オペレーターキュー追加処理 + * + * 仕様書には詳細記載なし。 + * 他のService(ShjOneService、ShjSixService)の実装を参考に実装。 + * + * @param object $targetUser 対象者情報 + * @param object $park 駐輪場情報 + * @return array 追加結果 + */ + private function addToOperatorQueue($targetUser, $park): array + { + try { + // operator_queテーブルに登録 + $operatorQue = OperatorQue::create([ + 'que_class' => 5, // 定期更新通知(OperatorQueモデルの定数参照) + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約ID, + 'park_id' => $targetUser->駐輪場ID, + 'que_comment' => sprintf( + '定期更新通知 / 契約ID:%s / 利用者:%s / 駐輪場:%s', + $targetUser->定期契約ID, + $targetUser->氏名 ?? '', + $park->park_name ?? '' + ), + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', + 'work_instructions' => '', + 'operator_id' => 9999999, // 仕様書準拠:固定値(他Serviceと同様) + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレーターキュー追加成功', [ + 'que_id' => $operatorQue->que_id, + 'que_class' => 5, + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約ID, + 'park_id' => $targetUser->駐輪場ID + ]); + + return [ + 'type' => 'queue_success', + 'success' => true, + 'message' => 'オペレーターキュー追加成功', + 'que_id' => $operatorQue->que_id + ]; + + } catch (\Exception $e) { + Log::error('オペレーターキュー追加エラー', [ + 'user_id' => $targetUser->利用者ID ?? 'unknown', + 'contract_id' => $targetUser->定期契約ID ?? 'unknown', + 'park_id' => $targetUser->駐輪場ID ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'type' => 'queue_error', + 'success' => false, + 'message' => 'オペレーターキュー追加エラー: ' . $e->getMessage() + ]; + } + } + + /** + * 【処理4】SHJ-8バッチ処理ログ作成 + * + * 仕様書に基づくSHJ-8共通処理呼び出し + * ※各駐輪場ごとに1回実行される + * + * @param object $park 駐輪場情報 + * @param string $batchComment バッチコメント + * @param int $mailSuccessCount メール正常終了件数 + * @param int $mailErrorCount メール異常終了件数 + * @param int $queueSuccessCount キュー登録正常終了件数 + * @param int $queueErrorCount キュー登録異常終了件数 + * @return void + */ + private function createShjBatchLog( + $park, + string $batchComment, + int $mailSuccessCount, + int $mailErrorCount, + int $queueSuccessCount, + int $queueErrorCount + ): void { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + // 仕様書:ステータスコメント生成 + // 「内部変数.バッチコメント」+ "/" + 「処理1.駐輪場名」 + // + ":メール正常終了件数" + 「内部変数.メール正常終了件数」 + // + "、メール異常終了件数" + 「内部変数.メール異常終了件数」 + // + "、キュー登録正常終了件数" + 「内部変数.キュー登録正常終了件数」 + // + "、キュー登録異常終了件数" + 「内部変数.キュー登録異常終了件数」 + $statusComment = ($batchComment ? $batchComment . ' / ' : '') . + "{$park->park_name}:" . + "メール正常終了件数={$mailSuccessCount}、" . + "メール異常終了件数={$mailErrorCount}、" . + "キュー登録正常終了件数={$queueSuccessCount}、" . + "キュー登録異常終了件数={$queueErrorCount}"; + + $today = now()->format('Y/m/d'); + + Log::info('SHJ-8バッチ処理ログ作成', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-3', + 'SHJ-3定期更新リマインダー', + 'success', + $statusComment, + $today, + $today + ); + + } catch (\Exception $e) { + Log::error('SHJ-8バッチ処理ログ作成エラー', [ + 'park_id' => $park->park_id ?? 'unknown', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // 仕様書:SHJ-8でエラーが発生してもメイン処理は継続 + // エラーログのみ出力 + } + } + + /** + * 全体の実行ログを記録する + * + * 駐輪場が0件の場合でも実行記録を残すための全体サマリーログ + * + * @param int $processedParksCount 処理した駐輪場数 + * @param int $totalTargetUsers 対象者総数 + * @param int $mailSuccessCount メール成功件数 + * @param int $mailErrorCount メール失敗件数 + * @param int $queueSuccessCount キュー成功件数 + * @param int $queueErrorCount キュー失敗件数 + * @return void + */ + private function createOverallBatchLog( + int $processedParksCount, + int $totalTargetUsers, + int $mailSuccessCount, + int $mailErrorCount, + int $queueSuccessCount, + int $queueErrorCount + ): void { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + // 全体サマリーのステータスコメント + $statusComment = sprintf( + '処理駐輪場数:%d、対象者総数:%d、メール成功:%d、メール失敗:%d、キュー追加:%d', + $processedParksCount, + $totalTargetUsers, + $mailSuccessCount, + $mailErrorCount, + $queueSuccessCount + $queueErrorCount + ); + + $today = now()->format('Y/m/d'); + + Log::info('SHJ-3 全体実行ログ作成', [ + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-3', + 'SHJ-3定期更新リマインダー', + 'success', + $statusComment, + $today, + $today + ); + + } catch (\Exception $e) { + Log::error('SHJ-3 全体実行ログ作成エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + } +} diff --git a/app/Services/ShjTwelveService.php b/app/Services/ShjTwelveService.php new file mode 100644 index 0000000..0009070 --- /dev/null +++ b/app/Services/ShjTwelveService.php @@ -0,0 +1,523 @@ +shjMailSendService = $shjMailSendService; + $this->shjEightService = $shjEightService; + } + + /** + * 【処理1】定期契約マスタより未払い者を取得する + * + * SQL条件: + * - 解約フラグ = 0 (未解約) + * - 授受フラグ = 0 (授受フラグOFF) + * - 請求金額 > 0 (請求金額あり) + * + * @return array 未払い者リスト + */ + public function getUnpaidUsers(): array + { + try { + $query = DB::table('regular_contract as T1') + ->select([ + 'T1.contract_id', // 定期契約ID + 'T2.user_seq', // 利用者ID + 'T2.user_name', // 利用者名 + 'T2.user_manual_regist_flag', // 手動登録フラグ + 'T2.user_primemail', // メールアドレス + 'T2.user_submail', // 予備メールアドレス + 'T1.park_id', // 駐輪場ID (regular_contractテーブルから) + 'T3.park_name', // 駐輪場名 + 'T1.billing_amount' // 請求金額 + ]) + ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') + ->join('park as T3', 'T1.park_id', '=', 'T3.park_id') + ->where([ + ['T1.contract_cancel_flag', '=', 0], // 解約フラグ = 0 + ['T1.contract_flag', '=', 0], // 授受フラグ = 0 + ]) + ->where('T1.billing_amount', '>', 0) // 請求金額 > 0 + ->get(); + + Log::info('SHJ-12 未払い者取得完了', [ + 'count' => $query->count(), + 'sql_conditions' => [ + 'contract_cancel_flag' => 0, + 'contract_flag' => 0, + 'billing_amount' => '> 0' + ] + ]); + + return $query->toArray(); + + } catch (\Exception $e) { + Log::error('SHJ-12 未払い者取得エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } + + /** + * 【処理2】未払い者への通知、またはオペレーターキュー追加処理 + * + * 仕様: + * - 手動登録フラグ = 0 (Web申込) → SHJ-7メール送信のみ + * - 手動登録フラグ ≠ 0 (その他) → オペレーターキュー追加のみ + * + * @param array $unpaidUsers 未払い者リスト + * @return array 処理結果 + */ + public function processUnpaidUserNotifications(array $unpaidUsers): array + { + // 内部変数(カウンタ)初期化 + $mailSuccessCount = 0; + $mailErrorCount = 0; + $queueSuccessCount = 0; + $queueErrorCount = 0; + $batchComments = []; // バッチコメント(エラー情報)累積用 + $processParameters = []; + + try { + DB::beginTransaction(); + + // 処理1の取得レコード数分繰り返し + foreach ($unpaidUsers as $user) { + try { + // 【判定】手動登録フラグによる分岐 + if ($user->user_manual_regist_flag == 0) { + // Web申込 → SHJ-7メール送信 + $mailResult = $this->sendNotificationMailViaShj7($user); + + // 処理結果判定: result === 0 → 正常 + if (($mailResult['result'] ?? 1) === 0) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + // バッチコメントにSHJ-7異常情報を設定(後ろに足す) + $batchComments[] = sprintf( + 'SHJ-7メール送信: 契約ID:%s メール送信失敗: %s', + $user->contract_id, + $mailResult['error_info'] ?? 'Unknown error' + ); + } + + // 処理パラメータ記録 + $processParameters[] = [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'billing_amount' => $user->billing_amount, + 'process_type' => 'mail', + 'result' => ($mailResult['result'] ?? 1) === 0 ? 'success' : 'error' + ]; + + } else { + // その他(手動登録)→ オペレーターキュー追加 + $queueResult = $this->addToOperatorQueue($user); + + if ($queueResult['success']) { + $queueSuccessCount++; + } else { + $queueErrorCount++; + // バッチコメントに異常情報を設定(後ろに足す) + $batchComments[] = sprintf( + 'キュー登録: 契約ID:%s キュー登録失敗: %s', + $user->contract_id, + $queueResult['message'] ?? 'Unknown error' + ); + } + + // 処理パラメータ記録 + $processParameters[] = [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'billing_amount' => $user->billing_amount, + 'process_type' => 'queue', + 'result' => $queueResult['success'] ? 'success' : 'error' + ]; + } + + } catch (\Exception $e) { + // 個別エラーを記録して次の繰り返し処理へ + $batchComments[] = sprintf( + '契約ID:%s 処理エラー: %s', + $user->contract_id, + $e->getMessage() + ); + + Log::warning('SHJ-12 個別処理エラー', [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'error' => $e->getMessage() + ]); + } + } + + DB::commit(); + + // ステータスコメント構築 + $statusComment = $this->buildStatusComment( + $mailSuccessCount, + $mailErrorCount, + $queueSuccessCount, + $queueErrorCount + ); + + // バッチコメント整形(最大100件、超過分は省略) + $formattedBatchComments = $this->formatBatchComments($batchComments); + + return [ + 'success' => true, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'status_comment' => $statusComment, + 'batch_comments' => $formattedBatchComments, + 'parameters' => $processParameters, + 'message' => '未払い者通知処理完了' + ]; + + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('SHJ-12 通知処理全体エラー', [ + 'error' => $e->getMessage(), + 'processed_count' => count($processParameters) + ]); + + return [ + 'success' => false, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'status_comment' => 'システムエラー', + 'batch_comments' => $batchComments, + 'parameters' => $processParameters, + 'message' => '通知処理エラー: ' . $e->getMessage(), + 'details' => $e->getTraceAsString() + ]; + } + } + + /** + * SHJ-7 メール送信サービス経由でメール通知送信 + * + * 仕様書パラメータ: + * - メールアドレス: 処理1.メールアドレス + * - 予備メールアドレス: 処理1.予備メールアドレス + * - 使用プログラムID: 204 + * + * @param object $user 未払い者情報 + * @return array SHJ-7の処理結果 (result_code: 0=正常, 1=異常) + */ + private function sendNotificationMailViaShj7($user): array + { + try { + // SHJ-7 executeMailSend 呼び出し + $result = $this->shjMailSendService->executeMailSend( + (string)($user->user_primemail ?? ''), // メールアドレス + (string)($user->user_submail ?? ''), // 予備メールアドレス + 204 // 使用プログラムID + ); + + Log::info('SHJ-12 SHJ-7メール送信結果', [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'result' => $result['result'] ?? 1, + 'error_info' => $result['error_info'] ?? '' + ]); + + return $result; + + } catch (\Exception $e) { + Log::error('SHJ-12 SHJ-7メール送信エラー', [ + 'contract_id' => $user->contract_id, + 'error' => $e->getMessage() + ]); + + return [ + 'result' => 1, // 異常終了 + 'error_info' => 'SHJ-7メール送信エラー: ' . $e->getMessage() + ]; + } + } + + /** + * オペレーターキューへの追加 + * + * 仕様書SQL定義に基づき operator_que テーブルに登録 + * - キュー種別ID: 8 (支払い催促) + * - キューステータスID: 1 (キュー発生) + * - que_id: MAX+1方式で生成(db_now.sqlは非AUTO_INCREMENT) + * - 主キー衝突時に1回リトライ + * + * @param object $user 未払い者情報 + * @return array 追加結果 + */ + private function addToOperatorQueue($user): array + { + $maxRetries = 2; // 最大2回試行(初回 + リトライ1回) + $lastException = null; + + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + // オペレーターキューデータ構築 + // 注: que_idはAUTO_INCREMENTのため、DBに委任(手動採番しない) + $queueData = [ + // 'que_id' は削除(AUTO_INCREMENTに委任) + 'que_class' => 8, // キュー種別ID: 8=支払い催促 + 'user_id' => $user->user_seq, // 利用者ID + 'contract_id' => $user->contract_id, // 定期契約ID + 'park_id' => $user->park_id, // 駐輪場ID + 'que_comment' => '', // キューコメント (空文字) + 'que_status' => 1, // キューステータスID: 1=キュー発生 + 'que_status_comment' => '', // キューステータスコメント (空文字) + 'work_instructions' => '', // 業務指示コメント (空文字) + 'created_at' => now(), // 登録日時 + 'updated_at' => now(), // 更新日時 + 'operator_id' => 9999999 // 更新オペレータID: バッチ処理用固定値 + ]; + + // operator_queテーブルに挿入(que_idはDBが自動採番) + $newQueId = DB::table('operator_que')->insertGetId($queueData); + + Log::info('SHJ-12 オペレーターキュー追加完了', [ + 'que_id' => $newQueId, + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'park_id' => $user->park_id, + 'que_class' => 8, + 'que_status' => 1, + 'attempt' => $attempt + ]); + + return [ + 'success' => true, + 'message' => 'オペレーターキュー追加成功', + 'que_id' => $newQueId + ]; + + } catch (\Illuminate\Database\QueryException $e) { + $lastException = $e; + + // 主キー重複エラー(Duplicate entry)の場合のみリトライ + // SQLSTATEコード 23000 は整合性制約違反を示す + if ($e->getCode() == 23000 && $attempt < $maxRetries) { + Log::warning('SHJ-12 que_id重複検出、リトライします', [ + 'attempt' => $attempt, + 'max_retries' => $maxRetries, + 'que_id' => $newQueId ?? null, + 'contract_id' => $user->contract_id, + 'error_code' => $e->getCode() + ]); + + // 短時間待機後に再試行(100ms) + usleep(100000); + continue; + } + + // その他のエラー、または最終試行での失敗時は例外をスロー + throw $e; + + } catch (\Exception $e) { + // QueryException以外の例外は即座にスロー + $lastException = $e; + throw $e; + } + } + + // ループを抜けた場合(通常は到達しない) + Log::error('SHJ-12 オペレーターキュー追加エラー(最大リトライ回数超過)', [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'max_retries' => $maxRetries, + 'error' => $lastException ? $lastException->getMessage() : 'Unknown error', + 'trace' => $lastException ? $lastException->getTraceAsString() : '' + ]); + + return [ + 'success' => false, + 'message' => 'オペレーターキュー追加エラー(リトライ失敗): ' . + ($lastException ? $lastException->getMessage() : 'Unknown error') + ]; + } + + /** + * ステータスコメント構築 + * + * 仕様書に基づくステータスコメント形式: + * - エラーなし: "メール送信成功:X件 / キュー登録成功:Y件" + * - エラーあり: "メール送信成功:X件 / メール送信失敗:A件 / キュー登録成功:Y件 / キュー登録失敗:B件" + * + * @param int $mailSuccessCount メール正常終了件数 + * @param int $mailErrorCount メール異常終了件数 + * @param int $queueSuccessCount キュー登録正常終了件数 + * @param int $queueErrorCount キュー登録異常終了件数 + * @return string ステータスコメント + */ + private function buildStatusComment( + int $mailSuccessCount, + int $mailErrorCount, + int $queueSuccessCount, + int $queueErrorCount + ): string { + $parts = []; + + // メール送信結果 + if ($mailSuccessCount > 0 || $mailErrorCount > 0) { + if ($mailErrorCount > 0) { + $parts[] = "メール送信成功:{$mailSuccessCount}件"; + $parts[] = "メール送信失敗:{$mailErrorCount}件"; + } else { + $parts[] = "メール送信成功:{$mailSuccessCount}件"; + } + } + + // キュー登録結果 + if ($queueSuccessCount > 0 || $queueErrorCount > 0) { + if ($queueErrorCount > 0) { + $parts[] = "キュー登録成功:{$queueSuccessCount}件"; + $parts[] = "キュー登録失敗:{$queueErrorCount}件"; + } else { + $parts[] = "キュー登録成功:{$queueSuccessCount}件"; + } + } + + return implode(' / ', $parts); + } + + /** + * バッチコメント整形 + * + * エラー情報を最大100件に制限し、超過分は省略表記 + * + * @param array $batchComments エラー情報配列 + * @return string 整形済みバッチコメント + */ + private function formatBatchComments(array $batchComments): string + { + if (empty($batchComments)) { + return ''; + } + + // 最大100件に制限 + $limitedComments = array_slice($batchComments, 0, 100); + + // 超過分の件数を追加 + if (count($batchComments) > 100) { + $limitedComments[] = sprintf( + '...他%d件省略', + count($batchComments) - 100 + ); + } + + return implode("\n", $limitedComments); + } + + /** + * 【処理3】バッチ処理ログを作成する + * + * SHJ-8サービスを呼び出してbat_job_logテーブルに登録(業務固有のstatus_comment記録) + * + * @param string $status ステータス + * @param array $parameters パラメータ + * @param string $message メッセージ + * @param int $executionCount 実行回数 + * @param int $successCount 成功回数 + * @param int $errorCount エラー回数 + * @return void + */ + public function createBatchLog( + string $status, + array $parameters, + string $message, + int $executionCount = 0, + int $successCount = 0, + int $errorCount = 0 + ): void { + try { + // デバイスIDを取得 + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + + // status_commentを構築(業務情報を含む) + $statusComment = sprintf( + '%s (実行:%d/成功:%d/エラー:%d)', + $message, + $executionCount, + $successCount, + $errorCount + ); + + $today = now()->format('Y/m/d'); + + Log::info('SHJ-8 バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // SHJ-8サービスを呼び出し + $this->shjEightService->execute( + $deviceId, + 'SHJ-12', + 'SHJ-12未払い者アラート', + $status, + $statusComment, + $today, + $today + ); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチログ作成エラー', [ + 'error' => $e->getMessage(), + 'status' => $status, + 'message' => $message + ]); + } + } +} diff --git a/app/Services/UserInformationHistoryService.php b/app/Services/UserInformationHistoryService.php new file mode 100644 index 0000000..7766efb --- /dev/null +++ b/app/Services/UserInformationHistoryService.php @@ -0,0 +1,184 @@ +exists()) { + throw new ApiException('E03', 'user_id の値が不正です(存在しないユーザー)。', 400); + } + $query->where('user_id', $params['user_id']); + } + + // 日付範囲フィルター + if (!empty($params['entry_date_from'])) { + $query->where('entry_date', '>=', $params['entry_date_from']); + } + if (!empty($params['entry_date_to'])) { + $query->where('entry_date', '<=', $params['entry_date_to']); + } + + // ソート(entry_date降順、ID降順) + $query->orderBy('entry_date', 'desc') + ->orderBy('user_information_history_id', 'desc'); + + // ページネーション + $perPage = $params['per_page'] ?? config('api.pagination.default_per_page', 20); + $perPage = min($perPage, config('api.pagination.max_per_page', 100)); + $page = $params['page'] ?? 1; + + $paginator = $query->paginate($perPage, ['*'], 'page', $page); + + return [ + 'data' => collect($paginator->items())->map(fn($item) => $item->toApiArray())->toArray(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + 'total_pages' => $paginator->lastPage(), + ] + ]; + } catch (ApiException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('UserInformationHistoryService::getList error', [ + 'error' => $e->getMessage(), + 'params' => $params + ]); + throw new ApiException('E99', 'システム内部エラーが発生しました。', 500); + } + } + + /** + * 単一履歴取得 + * + * @param int $id 履歴ID + * @return array + * @throws ApiException + */ + public function getById(int $id): array + { + try { + $history = UserInformationHistory::find($id); + + if (!$history) { + throw new ApiException('E02', '指定された履歴IDが存在しません。', 404); + } + + return $history->toApiArray(); + } catch (ApiException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('UserInformationHistoryService::getById error', [ + 'error' => $e->getMessage(), + 'id' => $id + ]); + throw new ApiException('E99', 'システム内部エラーが発生しました。', 500); + } + } + + /** + * 履歴新規作成 + * + * @param array $data リクエストデータ + * @return array + * @throws ApiException + */ + public function create(array $data): array + { + try { + DB::beginTransaction(); + + $history = UserInformationHistory::create([ + 'user_id' => $data['user_id'], + 'entry_date' => $data['entry_date'], + 'user_information_history' => $data['user_information_history'], + ]); + + DB::commit(); + + Log::info('UserInformationHistory created', [ + 'user_information_history_id' => $history->user_information_history_id, + 'user_id' => $history->user_id + ]); + + return $history->fresh()->toApiArray(); + } catch (\Exception $e) { + DB::rollBack(); + Log::error('UserInformationHistoryService::create error', [ + 'error' => $e->getMessage(), + 'data' => $data + ]); + throw new ApiException('E99', 'システム内部エラーが発生しました。', 500); + } + } + + /** + * 履歴更新 + * + * @param int $id 履歴ID + * @param array $data リクエストデータ + * @return array + * @throws ApiException + */ + public function update(int $id, array $data): array + { + try { + DB::beginTransaction(); + + $history = UserInformationHistory::find($id); + + if (!$history) { + throw new ApiException('E02', '指定された履歴IDが存在しません。', 404); + } + + // 更新可能フィールドのみ抽出 + $updateData = array_filter($data, function ($value, $key) { + return in_array($key, ['user_id', 'entry_date', 'user_information_history']) && $value !== null; + }, ARRAY_FILTER_USE_BOTH); + + $history->update($updateData); + + DB::commit(); + + Log::info('UserInformationHistory updated', [ + 'user_information_history_id' => $id, + 'updated_fields' => array_keys($updateData) + ]); + + return $history->fresh()->toApiArray(); + } catch (ApiException $e) { + DB::rollBack(); + throw $e; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('UserInformationHistoryService::update error', [ + 'error' => $e->getMessage(), + 'id' => $id, + 'data' => $data + ]); + throw new ApiException('E99', 'システム内部エラーが発生しました。', 500); + } + } +} diff --git a/artisan b/artisan new file mode 100644 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..22e29ae --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,22 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // APIキー認証ミドルウェアのエイリアス登録 + $middleware->alias([ + 'api.key' => \App\Http\Middleware\ApiKeyAuthentication::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..3d4d62b --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,6 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.21.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.7", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^10.0.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-07-22T15:41:55+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.6" + }, + "time": "2025-07-07T14:17:42+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + }, + "time": "2025-06-25T13:29:59+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + }, + "time": "2025-05-21T10:34:19+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "mpdf/mpdf", + "version": "v8.2.6", + "source": { + "type": "git", + "url": "https://github.com/mpdf/mpdf.git", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "mpdf/psr-http-message-shim": "^1.0 || ^2.0", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", + "myclabs/deep-copy": "^1.7", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "setasign/fpdi": "^2.1" + }, + "require-dev": { + "mockery/mockery": "^1.3.0", + "mpdf/qrcode": "^1.1.0", + "squizlabs/php_codesniffer": "^3.5.0", + "tracy/tracy": "~2.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-bcmath": "Needed for generation of some types of barcodes", + "ext-xml": "Needed mainly for SVG manipulation", + "ext-zlib": "Needed for compression of embedded resources, such as fonts" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Mpdf\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-only" + ], + "authors": [ + { + "name": "Matěj Humpál", + "role": "Developer, maintainer" + }, + { + "name": "Ian Back", + "role": "Developer (retired)" + } + ], + "description": "PHP library generating PDF files from UTF-8 encoded HTML", + "homepage": "https://mpdf.github.io", + "keywords": [ + "pdf", + "php", + "utf-8" + ], + "support": { + "docs": "https://mpdf.github.io", + "issues": "https://github.com/mpdf/mpdf/issues", + "source": "https://github.com/mpdf/mpdf" + }, + "funding": [ + { + "url": "https://www.paypal.me/mpdf", + "type": "custom" + } + ], + "time": "2025-08-18T08:51:51+00:00" + }, + { + "name": "mpdf/psr-http-message-shim", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-http-message-shim.git", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f", + "shasum": "" + }, + "require": { + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrHttpMessageShim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + }, + { + "name": "Nigel Cunningham", + "email": "nigel.cunningham@technocrat.com.au" + } + ], + "description": "Shim to allow support of different psr/message versions.", + "support": { + "issues": "https://github.com/mpdf/psr-http-message-shim/issues", + "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1" + }, + "time": "2023-10-02T14:34:03+00:00" + }, + { + "name": "mpdf/psr-log-aware-trait", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "shasum": "" + }, + "require": { + "psr/log": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ], + "description": "Trait to allow support of different psr/log versions.", + "support": { + "issues": "https://github.com/mpdf/psr-log-aware-trait/issues", + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0" + }, + "time": "2023-05-03T06:19:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-07-05T12:25:42+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-06-21T15:19:35+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.7" + }, + "time": "2025-06-03T04:55:08+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + }, + "time": "2025-07-27T20:03:57+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.2.6" + }, + "require-dev": { + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-05-08T08:14:37+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.9", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "1b801844becfe648985372cb4b12ad6840245ace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", + "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + }, + "time": "2025-06-23T02:35:06+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.0" + }, + "time": "2025-06-25T14:20:11+00:00" + }, + { + "name": "setasign/fpdi", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2025-08-05T09:57:14+00:00" + }, + { + "name": "simplesoftwareio/simple-qrcode", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-gd": "*", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1", + "phpunit/phpunit": "~9" + }, + "suggest": { + "ext-imagick": "Allows the generation of PNG QrCodes.", + "illuminate/support": "Allows for use within Laravel." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" + }, + "providers": [ + "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SimpleSoftwareIO\\QrCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simple Software LLC", + "email": "support@simplesoftware.io" + } + ], + "description": "Simple QrCode is a QR code generator made for Laravel.", + "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", + "keywords": [ + "Simple", + "generator", + "laravel", + "qrcode", + "wrapper" + ], + "support": { + "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", + "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" + }, + "time": "2021-02-08T20:43:55+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-13T07:48:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-22T09:11:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:26+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T15:07:14+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-28T08:24:55+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-19T08:51:26+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-17T09:11:12+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "8e213820c5fea844ecea29203d2a308019007c15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", + "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-24T20:43:28+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-20T20:19:01+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + }, + "time": "2024-12-21T16:25:41+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "59a123a3d459c5a23055802237cb317f609867e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.3" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-06-16T00:02:10+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0", + "illuminate/contracts": "^10.24|^11.0|^12.0", + "illuminate/log": "^10.24|^11.0|^12.0", + "illuminate/process": "^10.24|^11.0|^12.0", + "illuminate/support": "^10.24|^11.0|^12.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2025-06-05T13:55:57+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-07-10T18:09:32+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.44.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2025-07-04T16:17:06+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.8.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", + "php": "^8.2.0", + "symfony/console": "^7.3.0" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-06-25T02:12:12+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.2" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-06-18T08:56:18+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.3", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.2", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-07-11T04:10:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T06:57:01+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-03T06:57:57+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/api.php b/config/api.php new file mode 100644 index 0000000..3474cfb --- /dev/null +++ b/config/api.php @@ -0,0 +1,43 @@ + array_filter(explode(',', env('API_KEYS', ''))), + + /* + |-------------------------------------------------------------------------- + | Pagination Configuration + |-------------------------------------------------------------------------- + | + | API一覧取得時のページネーション設定 + | + */ + 'pagination' => [ + 'default_per_page' => 20, + 'max_per_page' => 100, + ], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting Configuration + |-------------------------------------------------------------------------- + | + | APIレート制限の設定 + | + */ + 'rate_limit' => [ + 'per_minute' => 60, + 'per_hour' => 1000, + ], +]; diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..182fbeb --- /dev/null +++ b/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'Asia/Tokyo', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..0ba5d5d --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..925f7d2 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..8910562 --- /dev/null +++ b/config/database.php @@ -0,0 +1,174 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/env.php b/config/env.php new file mode 100644 index 0000000..0edd8e3 --- /dev/null +++ b/config/env.php @@ -0,0 +1,6 @@ + env('MAIL_ADMIN', 'admin@example.com'), +]; \ No newline at end of file diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..1776b65 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,79 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem "disks" as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ], + + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; \ No newline at end of file diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..8d94292 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..756305b --- /dev/null +++ b/config/mail.php @@ -0,0 +1,116 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..116bd8d --- /dev/null +++ b/config/queue.php @@ -0,0 +1,112 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..27a3617 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..ba0aa60 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/config/shj1.php b/config/shj1.php new file mode 100644 index 0000000..52bda8e --- /dev/null +++ b/config/shj1.php @@ -0,0 +1,208 @@ + [ + // Google Cloud Vision API(OCR処理用) + 'google_vision' => [ + 'api_key' => env('GOOGLE_VISION_API_KEY', 'dummy_google_vision_api_key_replace_with_real_one'), + 'project_id' => env('GOOGLE_CLOUD_PROJECT_ID', 'dummy-project-id'), + ], + + // Google Maps API(距離計算用) + 'google_maps' => [ + 'api_key' => env('GOOGLE_MAPS_API_KEY', 'dummy_google_maps_api_key_replace_with_real_one'), + 'base_url' => 'https://maps.googleapis.com/maps/api', + ], + ], + + /* + |-------------------------------------------------------------------------- + | OCR Processing Configuration + |-------------------------------------------------------------------------- + | OCR処理関連設定 + */ + 'ocr' => [ + // 文字列類似度の閾値(70%) + 'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 70), + + // 対応身分証明書タイプ + 'supported_id_types' => [ + '免許証', + '健康保険証', + 'パスポート', + '学生証', + 'その他' + ], + + // OCR結果のキャッシュ時間(分) + 'cache_duration' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | File Storage Configuration + |-------------------------------------------------------------------------- + | ファイル保存設定 + */ + 'storage' => [ + // 本人確認写真の保存ディスク(Laravel Storage使用) + 'photo_disk' => 'public', + + // 写真ファイルの公開URL用パス + 'photo_public_path' => '/storage/photo', + + // 許可されるファイル拡張子 + 'allowed_extensions' => ['jpg', 'jpeg', 'png', 'pdf'], + + // 最大ファイルサイズ(MB) + 'max_file_size' => env('SHJ1_MAX_FILE_SIZE', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Distance Check Configuration + |-------------------------------------------------------------------------- + | 距離チェック設定 + */ + 'distance' => [ + // デフォルト距離制限(メートル) + // ※注意:park表にdistance_between_two_pointsフィールドが存在しないため、 + //     この値をデフォルトとして使用 + 'default_limit_meters' => env('SHJ1_DEFAULT_DISTANCE_LIMIT', 800), + + // 距離計算の精度(小数点以下桁数) + 'calculation_precision' => 2, + + // Park表の実際のフィールド名 + 'park_latitude_field' => 'park_latitude', + 'park_longitude_field' => 'park_longitude', + 'park_address_field' => 'park_adrs', + + // Google Maps Distance Matrix API設定 + 'google_maps_config' => [ + 'units' => 'metric', // メートル単位 + 'mode' => 'walking', // 徒歩での距離 + 'language' => 'ja', // 日本語 + 'region' => 'jp', // 日本地域 + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Category Configuration + |-------------------------------------------------------------------------- + | 利用者分類設定 + */ + 'user_categories' => [ + // 対象外とする分類名3の値(実際の運用に合わせて調整) + 'excluded_categories' => [ + '高齢者', + '障がい者', + '生活保護', + '中国', + '母子家庭' + ], + + // 通常処理対象の分類名3(実際のDB値に基づく) + 'normal_category' => '該当なし', + + // Usertype表の実際のフィールド名 + 'category_field' => 'usertype_subject3', + ], + + /* + |-------------------------------------------------------------------------- + | Identity Check Status Values + |-------------------------------------------------------------------------- + | 本人確認ステータス値 + */ + 'identity_check_status' => [ + 'auto_ok' => 2, // 自動チェックOK + 'auto_ng' => 5, // 自動チェックNG + 'pending' => 0, // 未確認 + 'manual_ok' => 1, // 手動確認済み + ], + + /* + |-------------------------------------------------------------------------- + | Operator Queue Configuration + |-------------------------------------------------------------------------- + | オペレータキュー設定 + */ + 'operator_queue' => [ + // キュー種別ID + 'queue_types' => [ + 'general' => 1, // 本人確認(社会人) + 'student' => 2, // 本人確認(学生) + ], + + // キューステータスID + 'queue_status' => [ + 'created' => 1, // キュー発生 + 'processing' => 2, // キュー作業中 + 'completed' => 3, // キュー作業済 + ], + + // バッチジョブのオペレータID + 'batch_operator_id' => env('SHJ1_BATCH_OPERATOR_ID', 9999999), + + // デフォルトコメント + 'default_comment' => '本人確認手動処理を行ってください', + ], + + /* + |-------------------------------------------------------------------------- + | Mail Template Configuration + |-------------------------------------------------------------------------- + | メール送信設定 + */ + 'mail' => [ + // 使用プログラムID(バッチ処理:200~299) + // 注意:実際のmail_templateテーブルにSHJ-1用テンプレートを追加する必要あり + 'program_id_success' => env('SHJ1_MAIL_SUCCESS_TEMPLATE_ID', 201), // 本人確認成功時 + 'program_id_failure' => env('SHJ1_MAIL_FAILURE_TEMPLATE_ID', 202), // 本人確認失敗時 + + // Mail Template テーブルの検索条件 + 'template_search' => [ + 'success' => ['pg_id' => env('SHJ1_MAIL_SUCCESS_TEMPLATE_ID', 201), 'internal_id' => 1, 'use_flag' => 1], + 'failure' => ['pg_id' => env('SHJ1_MAIL_FAILURE_TEMPLATE_ID', 202), 'internal_id' => 1, 'use_flag' => 1], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Batch Log Configuration + |-------------------------------------------------------------------------- + | バッチログ設定 + */ + 'batch_log' => [ + 'process_name' => 'SHJ-1本人確認自動処理', + 'status_success' => 'success', + 'status_error' => 'error', + ], + + /* + |-------------------------------------------------------------------------- + | Regular Contract 800M Flag + |-------------------------------------------------------------------------- + | 定期契約800M設定 + */ + 'contract_800m' => [ + 'violation_flag' => 1, // 800M違反フラグ(distance > limit時に設定) + 'normal_flag' => 0, // 通常フラグ + ], +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..d01a0ef --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,23 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..506b9a3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/assets/privacy_disclosure.pdf b/public/assets/privacy_disclosure.pdf new file mode 100644 index 0000000..fd215b3 --- /dev/null +++ b/public/assets/privacy_disclosure.pdf @@ -0,0 +1,150 @@ +%PDF-1.3 +1 0 obj +<< +/CreationDate (D:20081106111308+09'00') +/Creator +/ModDate (D:20081106111308+09'00') +/Producer (Xelo PDF Library) +>> +endobj +2 0 obj +<< +/Pages 3 0 R +/Type /Catalog +>> +endobj +3 0 obj +<< +/Count 1 +/Kids [ 4 0 R ] +/Type /Pages +>> +endobj +4 0 obj +<< +/Contents 11 0 R +/MediaBox [ 0 0 595 842 ] +/Parent 3 0 R +/Resources << +/Font << +/F1 5 0 R +/F2 8 0 R +>> +>> +/Type /Page +>> +endobj +5 0 obj +<< +/BaseFont /MS-Gothic +/DescendantFonts [ 6 0 R ] +/Encoding /UniJIS-UCS2-HW-H +/Subtype /Type0 +/Type /Font +>> +endobj +6 0 obj +<< +/BaseFont /MS-Gothic +/CIDSystemInfo << +/Ordering (Japan1) +/Registry (Adobe) +/Supplement 2 +>> +/DW 1000 +/FontDescriptor 7 0 R +/Subtype /CIDFontType2 +/Type /Font +/W [ 97 97 500 99 99 500 101 104 500 107 107 500 109 114 500 117 117 500 119 121 500 123 123 500 126 126 500 129 129 500 134 134 500 139 154 500 157 185 500 187 214 500 216 225 500 227 229 500 231 243 500 245 322 500 324 325 500 327 389 500 7483 7490 500 7492 7493 500 7496 7497 500 7500 7501 500 7504 7505 500 7509 7510 500 7512 7513 500 7517 7518 500 7520 7521 500 7524 7525 500 7528 7529 500 7532 7533 500 7536 7537 500 7540 7541 500 7543 7544 500 7546 7553 500 8009 8019 500 8025 8025 500 8195 8195 500 8210 8214 500 8230 8258 500 8261 8263 500 8304 8305 500 8309 8311 500 9355 9355 500 9361 9393 500 9395 9395 500 9400 9400 500 9404 9405 500 9407 9407 500 9412 9412 500 9416 9418 500 9421 9421 500 9423 9423 500 9426 9426 500 9429 9429 500 9432 9432 500 9436 9438 500 9441 9442 500 10502 10502 500 11855 11855 500 11859 11859 500 12089 12089 500 12091 12092 500 12099 12100 500 12111 12111 500 12118 12118 500 12120 12122 500 12184 12190 500 12194 12195 500 12200 12206 500 12215 12215 500 12237 12237 500 12239 12239 500 12254 [ 500 ] ] +>> +endobj +7 0 obj +<< +/Ascent 859 +/CapHeight 859 +/Descent -140 +/Flags 5 +/FontBBox [ 0 -137 1000 859 ] +/FontName /MS-Gothic +/ItalicAngle 0 +/StemV 87 +/Type /FontDescriptor +>> +endobj +8 0 obj +<< +/BaseFont /MS-PGothic +/DescendantFonts [ 9 0 R ] +/Encoding /UniJIS-UCS2-H +/Subtype /Type0 +/Type /Font +>> +endobj +9 0 obj +<< +/BaseFont /MS-PGothic +/CIDSystemInfo << +/Ordering (Japan1) +/Registry (Adobe) +/Supplement 2 +>> +/DW 1000 +/FontDescriptor 10 0 R +/Subtype /CIDFontType2 +/Type /Font +/W [ 1 [ 304 218 500 ] 4 6 500 7 [ 593 203 304 ] 10 [ 304 500 ] 12 [ 500 203 500 203 500 ] 17 26 500 27 28 203 29 31 500 32 [ 453 667 632 636 664 648 566 550 679 640 246 542 597 539 742 640 707 617 707 625 601 589 640 632 742 601 589 566 335 589 335 414 304 414 476 496 500 496 500 304 460 500 210 218 460 210 734 500 507 496 ] 82 [ 496 347 460 351 500 476 648 460 476 457 234 226 234 ] 97 [ 503 ] 99 [ 234 ] 101 [ 218 500 667 500 ] 107 [ 511 ] 109 [ 667 500 ] 111 [ 500 433 460 500 ] 117 117 203 119 120 203 121 [ 347 ] 123 [ 667 ] 126 [ 453 ] 129 [ 667 ] 134 [ 203 ] 139 [ 667 355 539 667 ] 143 [ 667 238 667 210 308 507 667 542 ] 152 [ 667 640 667 ] 157 158 238 159 [ 503 238 511 ] 162 163 511 164 169 632 170 [ 664 566 ] 172 174 566 175 [ 335 289 433 335 648 640 667 ] 182 185 667 187 190 640 191 [ 589 542 476 ] 194 198 476 199 203 500 204 205 292 206 [ 433 359 511 500 507 ] 211 214 507 216 219 500 220 [ 476 496 476 601 589 566 ] 227 [ 460 558 457 ] 231 [ 500 ] 325 [ 304 ] 327 331 441 332 [ 546 523 445 480 468 515 523 503 437 500 640 617 566 625 597 636 562 652 539 621 523 664 589 636 644 554 527 601 ] 360 361 601 362 [ 460 644 597 578 648 492 636 515 546 613 640 605 453 660 507 609 664 640 519 558 511 656 566 558 589 562 250 230 ] 633 637 664 638 640 500 643 648 500 651 652 746 653 [ 734 699 ] 660 [ 960 ] 662 662 500 670 691 500 776 778 500 780 789 683 790 [ 714 777 742 757 710 632 773 769 273 605 753 628 933 769 804 710 804 757 742 617 769 714 980 652 648 ] 815 [ 648 574 601 562 601 562 296 578 621 250 ] 825 [ 250 593 250 937 621 605 ] 831 [ 605 601 378 570 335 621 511 777 519 496 507 746 941 804 945 601 707 750 902 804 945 ] 854 [ 843 902 589 816 945 980 796 894 765 882 765 ] 865 [ 765 960 980 ] 870 [ 921 960 921 ] 873 [ 921 863 902 804 953 957 902 ] 880 [ 902 765 882 902 941 ] 891 893 960 903 [ 890 ] 905 906 980 907 [ 804 843 ] 910 [ 843 980 726 863 804 746 863 ] 918 [ 843 863 ] 923 [ 855 960 757 898 652 824 753 941 742 894 808 933 824 921 960 964 804 941 929 960 796 890 ] 947 948 898 949 [ 902 964 914 980 804 882 765 921 910 960 734 863 921 886 960 648 707 941 910 824 929 707 ] 974 [ 765 863 ] 976 [ 863 804 882 ] 979 [ 882 945 ] 981 982 945 983 [ 921 953 ] 985 [ 953 902 667 976 718 898 804 980 812 960 628 726 808 746 ] 1000 [ 851 863 765 941 ] 1006 [ 804 863 960 726 777 ] 7483 7490 664 7492 7493 664 7496 7497 664 7500 7501 664 7504 7505 664 7509 7510 664 7512 7513 664 7517 7518 664 7520 7521 664 7524 7525 664 7528 7529 664 7532 7533 664 7536 7537 664 7540 7541 664 7543 7544 664 7546 7553 664 7958 [ 824 ] 8009 8010 500 8011 8012 554 8013 8014 531 8015 [ 500 664 ] 8017 [ 664 601 617 ] 8025 [ 453 ] 8195 8195 500 8210 [ 500 667 ] 8212 8214 667 8230 8232 664 8233 [ 667 664 ] 8235 8236 664 8237 [ 667 664 ] 8239 8240 664 8241 [ 667 664 ] 8243 8250 664 8251 8254 667 8255 8258 500 8261 8263 664 8304 [ 453 664 ] 8309 8310 664 8311 [ 585 ] 8313 [ 910 960 964 921 ] 9355 [ 621 ] 9361 [ 476 355 500 ] 9364 [ 500 507 632 363 640 566 667 511 ] 9372 9374 511 9375 9376 500 9377 9393 289 9395 [ 566 ] 9400 [ 453 ] 9404 9405 640 9407 [ 500 ] 9412 [ 421 ] 9416 9418 500 9421 [ 667 ] 9423 9423 500 9426 [ 500 ] 9429 [ 664 ] 9432 [ 437 ] 9436 [ 500 503 554 ] 9441 [ 472 636 ] 10502 [ 500 ] 11855 [ 664 ] 11859 [ 500 ] 12089 [ 472 ] 12091 [ 500 523 ] 12099 [ 351 664 ] 12111 [ 386 ] 12118 12118 500 12120 12122 500 12184 12190 500 12194 12195 500 12200 [ 664 667 664 ] 12203 12206 664 12215 [ 394 ] 12237 12237 269 12239 [ 269 ] 12254 [ 277 ] ] +>> +endobj +10 0 obj +<< +/Ascent 859 +/CapHeight 859 +/Descent -140 +/Flags 4 +/FontBBox [ -121 -137 996 859 ] +/FontName /MS-PGothic +/ItalicAngle 0 +/StemV 87 +/Type /FontDescriptor +>> +endobj +11 0 obj +<< +/Filter /FlateDecode +/Length 2315 +>> +stream +xK$E,XAPʀiEPp/~}].UʌGFeŏ/Orz~R??6Rڅ/^26,.)ũR/=?!K4ý_Aь>y)F4g*Y-Hfd͈يCtrtY b Fښ.rV  ɍpv ޞ$I7*! {y&GA-9 ӟOo>jA|yD/ո!%1dCQNMbrI2B\!O2bG#EQ6:#e5G5+sትսݮ]pVv X쇈A].֮6Z\YuoIn7p?ntre Iq1o)dPpz?"F| Lp?"N,`r/4{L)Ę"R1E$/RqMݾn1! @tr ΐrObPHsObIzQbPgŠvs '7tFNb+KTai[$ >6@#.{rhv8%8xطφb+(GX`Go_ڊh|†zaӰvڸa"ʆ%Wkxr,,}G..lNtAlKa;]_ٰK-ÝblK2mb %XΔES/]Q_`}:zǨMlXjƶ=º;#^dž7h7) uckKm}8ww ]fVjLOQma v=s=yl(-ǍK[ &R`X眩{j;O;2U.釭r=alǝ+=9 vٯ<+=쌼09ttsyZ 9_eL닯έq"jϏS+[se[B4ӹc8`I/1 ffL2: ] +/Info 1 0 R +/Root 2 0 R +/Size 12 +>> +startxref +8334 +%%EOF diff --git a/public/assets/shachou.png b/public/assets/shachou.png new file mode 100644 index 0000000..b0146f2 Binary files /dev/null and b/public/assets/shachou.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/footer.html b/public/footer.html new file mode 100644 index 0000000..1113be7 --- /dev/null +++ b/public/footer.html @@ -0,0 +1,72 @@ + + + + + header + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/news.html b/public/news.html new file mode 100644 index 0000000..0ac743a --- /dev/null +++ b/public/news.html @@ -0,0 +1,87 @@ + + + + + header + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/views/emails/inquiry_manager.blade.php b/resources/views/emails/inquiry_manager.blade.php new file mode 100644 index 0000000..bf52590 --- /dev/null +++ b/resources/views/emails/inquiry_manager.blade.php @@ -0,0 +1,10 @@ +問い合わせを受け付けました。 +====================================== +【お問い合わせ内容】 +氏名:{{ $name }} +メールアドレス:{{ $email }} +電話番号:{{ $tel }} +お問い合わせ概要:{{ $subject }} +お問い合わせ駐輪場:{{ $parking }} +お問い合わせ詳細:{{ $detail }} +====================================== \ No newline at end of file diff --git a/resources/views/emails/inquiry_user.blade.php b/resources/views/emails/inquiry_user.blade.php new file mode 100644 index 0000000..8127b92 --- /dev/null +++ b/resources/views/emails/inquiry_user.blade.php @@ -0,0 +1,25 @@ +{{ $name }}様 + +この度はSo-Managerにお問い合わせいただき、誠にありがとうございます。 +お問い合わせ内容を受け付けました。順次対応させていただきますが、問い合わせの混雑状況や内容により、 +お返事に最大一週間程度お時間をいただく場合や、ご返信できない場合がございます。 + +====================================== +【お問い合わせ内容】 +氏名:{{ $name }} +メールアドレス:{{ $email }} +電話番号:{{ $tel }} +お問い合わせ概要:{{ $subject }} +お問い合わせ駐輪場:{{ $parking }} +お問い合わせ詳細:{{ $detail }} +====================================== + +お急ぎの場合や、Eメールで承れない項目については、下記コールセンターまでお問い合わせをお願いいたします。 + +―――――――――――――――――――――――――――――――――――――― +【お問い合わせ先】 +So-Managerコールセンター(毎日12時~22時) +03-5856-4720 +―――――――――――――――――――――――――――――――――――――― +※本メールアドレスは送信専用となり、ご返信には回答致しかねますのでご了承下さい。 +※本メールにお心当たりが無い場合には、コールセンターまでご連絡くださいますようお願い致します。 \ No newline at end of file diff --git a/resources/views/emails/member_regist_complete.blade.php b/resources/views/emails/member_regist_complete.blade.php new file mode 100644 index 0000000..0d724b7 --- /dev/null +++ b/resources/views/emails/member_regist_complete.blade.php @@ -0,0 +1,23 @@ +{{ $user_name }}様 + +So-Managerへのユーザー登録ありがとうございました。 +ご登録頂いたメールアドレスと下記のパスワードで、マイページへログインしてご利用ください。 +操作方法は、マイページ内の<このページの使い方>をご覧ください。 + +{{ $user_name }}様のパスワード +---------------- +{{ $user_pass }} +---------------- + +https://so-manager.com/swo8_1 + +【お問い合わせ先】 +・電話でのお問い合わせ +So-Managerコールセンター(毎日12時~22時) +03-5856-4720 + +・メールでのお問合せ(専用フォームよりお問合せください) +https://so-manager.com/swo7_1 + +※本メールアドレスは送信専用となり、ご返信には回答致しかねますのでご了承下さい。 +※本メールにお心当たりが無い場合には、コールセンターまでご連絡くださいますようお願い致します。 \ No newline at end of file diff --git a/resources/views/emails/member_regist_info.blade.php b/resources/views/emails/member_regist_info.blade.php new file mode 100644 index 0000000..ad904d7 --- /dev/null +++ b/resources/views/emails/member_regist_info.blade.php @@ -0,0 +1,21 @@ +!!ご注意!! +まだ会員登録は完了していませんのでご注意ください。 + +このたびはSo-Managerユーザ登録をお手続きいただきまして、 +誠にありがとうございます。 + +下記URLにアクセスしてご登録手続き(無料)を完了させて下さい。 + +▼会員登録お手続きページ▼ +{!! $url !!} + +【お問い合わせ先】 +・電話でのお問い合わせ +So-Managerコールセンター(毎日12時~22時) +03-5856-4720 + +・メールでのお問合せ(専用フォームよりお問合せください) +https://so-manager.com/swo7_1 + +※本メールアドレスは送信専用となり、ご返信には回答致しかねますのでご了承下さい。 +※本メールにお心当たりが無い場合には、コールセンターまでご連絡くださいますようお願い致します。 \ No newline at end of file diff --git a/resources/views/emails/password_remind.blade.php b/resources/views/emails/password_remind.blade.php new file mode 100644 index 0000000..9a07b3e --- /dev/null +++ b/resources/views/emails/password_remind.blade.php @@ -0,0 +1,27 @@ +{{ $name }}様 + +So-Manager自動応答システムです。 +パスワードを再発行しました。 +下記パスワードでログインしてください。 + +---------------- +{{ $pass }} +---------------- + +ログインURL: +https://so-manager.com/swo8_1 + +パスワードはログイン後に変更できます。以下の場所で書き換えてご利用ください。 +https://so-manager.com/swc1 + + +【お問い合わせ先】 +・電話でのお問い合わせ +So-Managerコールセンター(毎日12時~22時) +03-5856-4720 + +・メールでのお問合せ(専用フォームよりお問合せください) +https://so-manager.com/swo7_1 + +※本メールアドレスは送信専用となり、ご返信には回答致しかねますのでご了承下さい。 +※本メールにお心当たりが無い場合には、コールセンターまでご連絡くださいますようお願い致します。 \ No newline at end of file diff --git a/resources/views/emails/reservation_cancelled.blade.php b/resources/views/emails/reservation_cancelled.blade.php new file mode 100644 index 0000000..87e3cf3 --- /dev/null +++ b/resources/views/emails/reservation_cancelled.blade.php @@ -0,0 +1,22 @@ +{!! nl2br(e( +$user_name . ' 様 + +So-Manager自動応答システムです。 + +下記駐輪場の空き待ちのキャンセルを受け付けました。 + +空き駐輪場: ' . $reserve->park_name . ' +車種区分 : ' . $reserve->psection_subject . ' +駐輪分類 : ' . $reserve->ptype_subject . ' + +So-Manager.comのユーザー登録は解約されておりません。別の駐輪場でのご契約や空き待ちをご希望の際には、マイページをご活用ください。 + + +■お問合せ先■ +So-Managerコールセンター(ソーマネージャーコールセンター) +●電話:03-5856-4720 +●メールでのお問合せ(専用フォームよりお問合わせください) + +※本メールアドレスは送信専用です。ご返信には回答致しかねますのでご了承ください。 +※本メールにお心あたりのない場合には、コールセンターまでご連絡くださいますようお願い致します。' +)) !!} \ No newline at end of file diff --git a/resources/views/emails/user_edit_verify.blade.php b/resources/views/emails/user_edit_verify.blade.php new file mode 100644 index 0000000..0f1ac24 --- /dev/null +++ b/resources/views/emails/user_edit_verify.blade.php @@ -0,0 +1,10 @@ +{!! nl2br(e( +$user->user_name . ' 様 + +ユーザー情報変更を変更する + +' . $verifyUrl . ' +→URLをクリックすると変更が完了します。 + +※このURLの有効期限は、24時間です。' +)) !!} \ No newline at end of file diff --git a/resources/views/emails/withdraw_complete.blade.php b/resources/views/emails/withdraw_complete.blade.php new file mode 100644 index 0000000..3615341 --- /dev/null +++ b/resources/views/emails/withdraw_complete.blade.php @@ -0,0 +1,22 @@ +{!! nl2br(e( +$user_name . ' 様 + +「So-Manager」をご利用いただきましてありがとうございます。 +お客様の退会手続きを受付けました。 +ご利用ありがとうございました。 + +■ユーザー名 +' . $user_primemail . ' + +退会日:' . $user_quitday . ' + +今度ともSo-Managerを宜しくお願い致します。 + +■お問合せ先■ +So-Managerコールセンター(ソーマネージャーコールセンター) +●電話:03-5856-4720 +●メールでのお問合せ(専用フォームよりお問合わせください) + +※本メールアドレスは送信専用です。ご返信には回答致しかねますのでご了承ください。 +※本メールにお心あたりのない場合には、コールセンターまでご連絡くださいますようお願い致します。' +)) !!} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..0d76ac1 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,38 @@ +group(function () { + + /* + |-------------------------------------------------------------------------- + | API 7, 8, 9 - ユーザーインフォメーション履歴 + |-------------------------------------------------------------------------- + | + | GET /api/user-information-history - 一覧取得 + | GET /api/user-information-history/{id} - 単一取得 + | POST /api/user-information-history - 新規追加 + | PUT /api/user-information-history/{id} - 更新 + | + */ + Route::get('user-information-history', [UserInformationHistoryController::class, 'index']); + Route::get('user-information-history/{id}', [UserInformationHistoryController::class, 'show']) + ->where('id', '[0-9]+'); + Route::post('user-information-history', [UserInformationHistoryController::class, 'store']); + Route::put('user-information-history/{id}', [UserInformationHistoryController::class, 'update']) + ->where('id', '[0-9]+'); + +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..97835e4 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +}