diff --git a/.env b/.env index bd29908..39ea480 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://so-manager-dev.com/public/ +APP_URL=http://www-somanager.localhost:81/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja APP_FAKER_LOCALE=ja_JP @@ -22,9 +22,9 @@ LOG_LEVEL=debug DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 -DB_DATABASE=krgm -DB_USERNAME=krgm_user -DB_PASSWORD=StrongDbP@ss2 +DB_DATABASE=somanager_admin +DB_USERNAME=root +DB_PASSWORD= SESSION_DRIVER=database SESSION_LIFETIME=120 @@ -62,4 +62,37 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false -VITE_APP_NAME="${APP_NAME}" \ No newline at end of file +VITE_APP_NAME="${APP_NAME}" + + + +# ----------------------------------------------- +# ----------------------------------------------- +#以下は追加項目 + +# ----------------------------------------------- +# SHJ-1系 +# OCR類似度閾値(0-100、デフォルト70) +SHJ1_OCR_SIMILARITY_THRESHOLD=70 + +# 距離制限デフォルト値(メートル、デフォルト800) +SHJ1_DEFAULT_DISTANCE_LIMIT=3000 + +# Debug Mode(開発時のみtrue) +SHJ1_DEBUG_MODE=false + + +# ファイルサイズ制限(MB、デフォルト10) +SHJ1_MAX_FILE_SIZE=10 + + +# ----------------------------------------------- +# API系 + +# Google Cloud Vision API Configuration (OCR用) +GOOGLE_VISION_API_KEY=AIzaSyBz0aqzyBXfZwwmXc4EpJDHqcfnmSv3_2g +GOOGLE_CLOUD_PROJECT_ID=midyear-acre-471109-e5 + +# Google Maps API Configuration (距離計算用) +GOOGLE_MAPS_API_KEY=AIzaSyBz0aqzyBXfZwwmXc4EpJDHqcfnmSv3_2g + diff --git a/.gitignore b/.gitignore index 7de6258..fb6f629 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,28 @@ -/.phpunit.cache +# Laravel /node_modules -/public/build /public/hot /public/storage /storage/*.key -/storage/pail /vendor .env .env.backup .env.production -.phpactor.json .phpunit.result.cache Homestead.json Homestead.yaml +auth.json npm-debug.log yarn-error.log -/auth.json -/.fleet -/.idea -/.nova -/.vscode -/.zed -.gitignore \ No newline at end of file + +# 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/app/Console/Commands/ShjBatchLogCommand.php b/app/Console/Commands/ShjBatchLogCommand.php new file mode 100644 index 0000000..8d103e5 --- /dev/null +++ b/app/Console/Commands/ShjBatchLogCommand.php @@ -0,0 +1,238 @@ +info('SHJ-8 バッチ処理ログ登録を開始します。'); + + // 引数取得 + $deviceId = (int) $this->argument('device_id'); + $processName = $this->argument('process_name'); + $jobName = $this->argument('job_name'); + $status = $this->argument('status'); + $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, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ]); + + // 【処理1】入力パラメーターをチェックする + $paramCheckResult = $this->validateParameters($deviceId, $processName, $jobName, $status, $createdDate, $updatedDate); + if (!$paramCheckResult['valid']) { + $this->error('パラメータエラー: ' . $paramCheckResult['message']); + + // 仕様書【判断1】パラメーターNG時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: ' . $paramCheckResult['message']); + + return self::FAILURE; + } + + // 【処理2】統一BatchLogを使用してログ登録 + $batchLog = BatchLog::createBatchLog( + $processName, // 実際のプロセス名を使用 + $status, + [ + 'device_id' => $deviceId, + 'job_name' => $jobName, + 'status_comment' => BatchLog::getSuccessComment(), + 'input_created_date' => $createdDate, + 'input_updated_date' => $updatedDate, + 'shj8_params' => [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ] + ], + $jobName . ':' . BatchLog::getSuccessComment() + ); + + $endTime = now(); + $this->info('SHJ-8 バッチ処理ログ登録が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ-8 バッチ処理ログ登録完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'batch_log_id' => $batchLog->id + ]); + + // 仕様書【処理3】正常終了時の結果出力 + $this->line('処理結果: 0'); // 0 = 正常終了 + $this->line('異常情報: '); // 正常時は空文字 + + return self::SUCCESS; + + } catch (\Exception $e) { + $this->error('SHJ-8 バッチ処理ログ登録で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-8 バッチ処理ログ登録例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // 仕様書【処理3】異常終了時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: エラー: ' . $e->getMessage()); + + return self::FAILURE; + } + } + + /** + * 【処理1】パラメータの妥当性を検証 + * + * 仕様書に基づく検証内容: + * - デバイスID: 必須、数値、device表に存在するか + * - プロセス名: 「プロセス名」「ジョブ名」いずれか必須 + * - ジョブ名: 「プロセス名」「ジョブ名」いずれか必須 + * - ステータス: 必須 + * - 登録日時: 必須、yyyy/mm/dd形式 + * - 更新日時: 必須、yyyy/mm/dd形式 + * + * @param int $deviceId デバイスID + * @param string $processName プロセス名 + * @param string $jobName ジョブ名 + * @param string $status ステータス + * @param string $createdDate 登録日時 + * @param string $updatedDate 更新日時 + * @return array 検証結果 ['valid' => bool, 'message' => string] + */ + private function validateParameters(int $deviceId, string $processName, string $jobName, string $status, string $createdDate, string $updatedDate): array + { + // デバイスID存在チェック + if ($deviceId <= 0) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: デバイスIDは正の整数である必要があります' + ]; + } + + if (!Device::exists($deviceId)) { + return [ + 'valid' => false, + 'message' => "パラメーターNG: デバイスID {$deviceId} が存在しません" + ]; + } + + // プロセス名とジョブ名のいずれか必須チェック + if (empty($processName) && empty($jobName)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: プロセス名またはジョブ名のいずれかは必須です' + ]; + } + + // ステータス必須チェック + if (empty($status)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: ステータスは必須です' + ]; + } + + // 日付形式チェック + if (!$this->isValidDateFormat($createdDate)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: 登録日時の形式が正しくありません(yyyy/mm/dd)' + ]; + } + + if (!$this->isValidDateFormat($updatedDate)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: 更新日時の形式が正しくありません(yyyy/mm/dd)' + ]; + } + + return [ + 'valid' => true, + 'message' => 'パラメーターチェックOK' + ]; + } + + /** + * 日付形式の検証 + * + * @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/ShjElevenCommand.php b/app/Console/Commands/ShjElevenCommand.php new file mode 100644 index 0000000..2e739f2 --- /dev/null +++ b/app/Console/Commands/ShjElevenCommand.php @@ -0,0 +1,182 @@ +shjElevenService = $shjElevenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 集計単位每个の契約台数を算出する + * 2. 取得件数判定 + * 3. ゾーンマスタを取得する + * 4. 取得判定とゾーンマスタ登録 + * 5. 契約台数チェック(限界台数超過判定) + * 6. 契約台数を反映する + * 7. バッチ処理ログを作成する + * + * @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) { + // 対象なしの結果を設定する + $this->info('契約台数算出対象なしのため処理を終了します。'); + + // バッチ処理ログを作成 + $this->shjElevenService->createBatchLog( + 'success', + [], + '契約台数算出対象なし', + 0, + 0, + 0 + ); + + Log::info('SHJ-11 現在契約台数集計完了(対象なし)', [ + 'end_time' => now(), + 'duration_seconds' => $startTime->diffInSeconds(now()) + ]); + + return self::SUCCESS; + } + + // 【処理2・3】ゾーンマスタ処理(取得・登録・更新) + $this->info('【処理2】ゾーンマスタ処理を実行しています...'); + $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']}件"); + + // 【処理4】バッチ処理ログを作成する + $this->shjElevenService->createBatchLog( + 'success', + $processResult['parameters'], + '現在契約台数集計処理完了', + $countResults, + $processResult['created_zones'] + $processResult['updated_zones'], + 0 + ); + + 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'] + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-11 現在契約台数集計でエラーが発生しました: ' . $processResult['message']); + + // エラー時のバッチログ作成 + $this->shjElevenService->createBatchLog( + 'error', + $processResult['parameters'] ?? [], + $processResult['message'], + $countResults, + $processResult['created_zones'] ?? 0, + 1 + ); + + Log::error('SHJ-11 現在契約台数集計エラー', [ + 'error' => $processResult['message'], + 'details' => $processResult['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-11 現在契約台数集計で予期しないエラーが発生しました: ' . $e->getMessage()); + + // 例外時のバッチログ作成 + $this->shjElevenService->createBatchLog( + 'error', + [], + 'システムエラー: ' . $e->getMessage(), + 0, + 0, + 1 + ); + + Log::error('SHJ-11 現在契約台数集計例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjFourBCheckCommand.php b/app/Console/Commands/ShjFourBCheckCommand.php new file mode 100644 index 0000000..b9f7e5e --- /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}時間以内"); + + // バッチログ作成 + $batch = BatchLog::createBatchLog( + 'shj4b_check', + BatchLog::STATUS_START, + [ + 'command' => 'shj4b:check', + 'options' => [ + 'dry_run' => $isDryRun, + 'limit' => $limit, + 'hours' => $hours, + ], + 'start_time' => $startTime, + ], + 'SHJ-4B チェックコマンド開始' + ); + + try { + // 未処理の決済トランザクション取得 + $unprocessedSettlements = $this->getUnprocessedSettlements($hours, $limit); + + $this->info("未処理決済トランザクション: " . $unprocessedSettlements->count() . "件"); + + if ($unprocessedSettlements->isEmpty()) { + $this->info("処理対象なし"); + + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4B チェック完了 - 処理対象なし', + 'success_count' => 0, + ]); + + return 0; + } + + // 対象一覧表示 + $this->displayTargets($unprocessedSettlements); + + if ($isDryRun) { + $this->info("ドライランモードのため、実際の処理はスキップします"); + + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4B チェック完了 - ドライラン', + 'success_count' => 0, + 'parameters' => json_encode(['targets' => $unprocessedSettlements->pluck('settlement_transaction_id')->toArray()]), + ]); + + return 0; + } + + // 実際の処理実行 + $processed = $this->processSettlements($unprocessedSettlements); + + $this->info("処理完了: {$processed['success']}件成功, {$processed['failed']}件失敗"); + + $batch->update([ + 'status' => $processed['failed'] > 0 ? BatchLog::STATUS_ERROR : BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => "SHJ-4B チェック完了 - 成功:{$processed['success']}件, 失敗:{$processed['failed']}件", + 'success_count' => $processed['success'], + 'error_count' => $processed['failed'], + 'parameters' => json_encode($processed), + ]); + + 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(), + ]); + + $batch->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => 'SHJ-4B チェック失敗: ' . $e->getMessage(), + 'error_details' => $e->getTraceAsString(), + 'error_count' => 1, + ]); + + 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. batch_logで処理完了記録があるかチェック + $processedInBatch = BatchLog::where('process_name', 'shj4b') + ->where('status', BatchLog::STATUS_SUCCESS) + ->where('parameters', 'like', '%"settlement_transaction_id":' . $settlement->settlement_transaction_id . '%') + ->exists(); + + if ($processedInBatch) { + return true; + } + + // 3. 現在キューに入っているかチェック(簡易版) + // 注: より正確にはRedis/DBキューの内容を確認する必要がある + $recentJobDispatched = BatchLog::where('process_name', 'shj4b') + ->where('parameters', '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(), + ]; + } +} 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..539e806 --- /dev/null +++ b/app/Console/Commands/ShjMailSendCommand.php @@ -0,0 +1,177 @@ +shjMailSendService = $shjMailSendService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 入力パラメーターをチェックする + * 2. メール送信テンプレート情報を取得する + * 3. メールを送信する + * 4. 処理結果を返却する + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ メール送信処理を開始します。'); + Log::info('SHJ メール送信処理開始', [ + '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'); + + // 【処理1】パラメータ検証 + if (!$this->validateParameters($mailAddress, $backupMailAddress, $mailTemplateId)) { + $this->error('パラメータが不正です。'); + return self::FAILURE; + } + + // SHJメール送信処理実行 + $result = $this->shjMailSendService->executeMailSend($mailAddress, $backupMailAddress, $mailTemplateId); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ メール送信処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ メール送信処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ メール送信処理でエラーが発生しました: ' . $result['message']); + Log::error('SHJ メール送信処理エラー', [ + 'error' => $result['message'], + 'details' => $result['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ メール送信処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ メール送信処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * 【処理1】パラメータの妥当性を検証 + * + * 仕様書に基づく検証内容: + * - メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須 + * - メールテンプレートID: 必須 + * + * @param mixed $mailAddress メールアドレス + * @param mixed $backupMailAddress 予備メールアドレス + * @param mixed $mailTemplateId メールテンプレートID + * @return bool 検証結果 + */ + private function validateParameters($mailAddress, $backupMailAddress, $mailTemplateId): bool + { + // メールテンプレートIDチェック + if (empty($mailTemplateId)) { + $this->error('メールテンプレートIDは必須です。'); + return false; + } + + // 数値形式チェック(メールテンプレートID) + if (!is_numeric($mailTemplateId)) { + $this->error('メールテンプレートIDは数値である必要があります。'); + return false; + } + + // 正の整数チェック(メールテンプレートID) + if ($mailTemplateId <= 0) { + $this->error('メールテンプレートIDは正の整数である必要があります。'); + return false; + } + + // メールアドレスチェック(いずれか必須) + if (empty($mailAddress) && empty($backupMailAddress)) { + $this->error('メールアドレスまたは予備メールアドレスのいずれかは必須です。'); + return false; + } + + // メールアドレス形式チェック + if (!empty($mailAddress) && !filter_var($mailAddress, FILTER_VALIDATE_EMAIL)) { + $this->error('メールアドレスの形式が正しくありません。'); + return false; + } + + if (!empty($backupMailAddress) && !filter_var($backupMailAddress, FILTER_VALIDATE_EMAIL)) { + $this->error('予備メールアドレスの形式が正しくありません。'); + return false; + } + + return true; + } +} diff --git a/app/Console/Commands/ShjNineCommand.php b/app/Console/Commands/ShjNineCommand.php new file mode 100644 index 0000000..11222e9 --- /dev/null +++ b/app/Console/Commands/ShjNineCommand.php @@ -0,0 +1,206 @@ +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 + { + // 集計種別チェック + $allowedTypes = ['daily', 'monthly', 'yearly']; + if (!in_array($type, $allowedTypes)) { + $this->error('集計種別は daily, monthly, yearly のいずれかを指定してください。'); + return false; + } + + // 対象日形式チェック(指定されている場合) + if ($targetDate && !$this->isValidDateFormat($targetDate)) { + $this->error('対象日の形式が正しくありません(YYYY-MM-DD形式で指定してください)。'); + return false; + } + + return true; + } + + /** + * 集計対象日を決定 + * + * @param string $type 集計種別 + * @param string|null $targetDate 指定日 + * @return string 集計対象日 + */ + private function determineAggregationDate(string $type, ?string $targetDate): string + { + if ($targetDate) { + return $targetDate; + } + + // パラメータ指定がない場合のデフォルト設定 + switch ($type) { + case 'daily': + // 日次:昨日(本日の1日前) + return now()->subDay()->format('Y-m-d'); + + case 'monthly': + // 月次:前月の最終日 + return now()->subMonth()->endOfMonth()->format('Y-m-d'); + + case 'yearly': + // 年次:前年の最終日 + return now()->subYear()->endOfYear()->format('Y-m-d'); + + default: + 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..4bb40bd --- /dev/null +++ b/app/Console/Commands/ShjOneCommand.php @@ -0,0 +1,142 @@ +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-8共通処理 + $batchLog = BatchLog::createBatchLog( + 'SHJ-1本人確認自動処理', + BatchLog::STATUS_START, + $parameters, + 'SHJ-1処理開始: 利用者ID=' . $userId . ', 駐輪場ID=' . $parkId + ); + + // SHJ-1 メイン処理を実行 + $result = $this->shjOneService->execute($userId, $parkId); + + // 処理結果の表示 + $this->displayResult($result); + + // バッチログ完了 - 実際の処理結果に基づく + $logStatus = $result['log_status'] ?? ($result['system_success'] ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_ERROR); + $batchLog->update([ + 'status' => $logStatus, + 'end_time' => now(), + 'message' => $result['message'], + 'success_count' => $result['stats']['success_count'] ?? 0, + 'error_count' => $result['stats']['error_count'] ?? 0, + 'execution_count' => $result['stats']['processed_count'] ?? 1 + ]); + + $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()); + + // エラーログ記録 + if (isset($batchLog)) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $e->getMessage(), + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + 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..1a81125 --- /dev/null +++ b/app/Console/Commands/ShjTenCommand.php @@ -0,0 +1,236 @@ +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'); + + 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; + } + } + + /** + * パラメータの妥当性を検証 + * + * @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日 + * + * @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)))); + + return [ + 'type' => 'yearly', + 'fiscal_year' => $year, + 'start_date' => $startDate, + 'end_date' => $endDate, + '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; + + return [ + 'type' => 'monthly', + 'fiscal_year' => $fiscalYear, + 'target_year' => $year, + 'target_month' => $month, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'summary_type' => 2, // 月次 + 'target_label' => "{$year}年{$month}月" + ]; + } + + throw new \InvalidArgumentException("不正な集計種別: {$type}"); + } +} 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..10b0705 --- /dev/null +++ b/app/Console/Commands/ShjTwelveCommand.php @@ -0,0 +1,177 @@ +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('未払い者対象なしのため処理を終了します。'); + + // バッチ処理ログを作成 + $this->shjTwelveService->createBatchLog( + 'success', + [], + '未払い者対象なし', + 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['notification_count']}件"); + $this->info("オペレーターキュー追加件数: {$processResult['queue_count']}件"); + + // 【処理3】バッチ処理ログを作成する + $this->shjTwelveService->createBatchLog( + 'success', + $processResult['parameters'], + '未払い者通知処理完了', + $unpaidCount, + $processResult['notification_count'] + $processResult['queue_count'], + 0 + ); + + Log::info('SHJ-12 未払い者通知処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'processed_count' => $unpaidCount, + 'notification_count' => $processResult['notification_count'], + 'queue_count' => $processResult['queue_count'] + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-12 未払い者通知処理でエラーが発生しました: ' . $processResult['message']); + + // エラー時のバッチログ作成 + $this->shjTwelveService->createBatchLog( + 'error', + $processResult['parameters'] ?? [], + $processResult['message'], + $unpaidCount, + $processResult['notification_count'] ?? 0, + 1 + ); + + Log::error('SHJ-12 未払い者通知処理エラー', [ + 'error' => $processResult['message'], + 'details' => $processResult['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-12 未払い者通知処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + // 例外時のバッチログ作成 + $this->shjTwelveService->createBatchLog( + 'error', + [], + 'システムエラー: ' . $e->getMessage(), + 0, + 0, + 1 + ); + + Log::error('SHJ-12 未払い者通知処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Http/Controllers/RegularContractCreateController.php b/app/Http/Controllers/RegularContractCreateController.php index 7dd2675..7a25a56 100644 --- a/app/Http/Controllers/RegularContractCreateController.php +++ b/app/Http/Controllers/RegularContractCreateController.php @@ -4,10 +4,13 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Session; use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Artisan; +use Exception; use function redirect; class RegularContractCreateController extends Controller @@ -631,17 +634,18 @@ class RegularContractCreateController extends Controller ->withInput(); } - // おもて画像保存 + // おもて画像保存(Laravel Storageを使用) $front = $request->file('user_idcard'); - $filename_front = uniqid('photo1_') . '.' . $front->getClientOriginalExtension(); + $filename_front = uniqid('photo1_') . '.' . $front->getClientOriginalExtension(); $front->storeAs('photo', $filename_front, 'public'); - // userテーブルに保存 本人確認書類チェック済フラグや更新日時も更新項目に追加 + + // userテーブルに保存(チェック済フラグはSHJ-1処理後に設定) $updateData = [ 'photo_filename1' => $filename_front, 'user_idcard' => $request->idcard_type, - 'user_idcard_chk_flag' => 1, 'updated_at' => now(), ]; + // ウラ画像がある場合保存し更新項目に追加 if ($request->hasFile('user_idcard2')) { $back = $request->file('user_idcard2'); @@ -650,24 +654,76 @@ class RegularContractCreateController extends Controller $updateData['photo_filename2'] = $filename_back; } DB::table('user')->where('user_id', $user_id)->update($updateData); + + // SHJ-1 本人確認自動処理を実行 $user = DB::table('user')->where('user_id', $user_id)->first(); $park = DB::table('park')->where('park_id', $request->park_id)->first(); $psection = DB::table('psection')->where('psection_id', $request->psection_id)->first(); $usertype = DB::table('usertype')->where('user_categoryid', $user->user_categoryid)->first(); - \Log::info('新規定期契約-契約情報確認画面にアクセス', [ + + // user_idからuser_seqを取得してSHJ-1に渡す + $user_seq = $user->user_seq; + $park_id = $request->park_id; + + \Log::info('SHJ-1バッチ処理開始', [ 'user_id' => $user_id, + 'user_seq' => $user_seq, + 'park_id' => $park_id, + 'contract_id' => $contract_id ]); - return view('regular_contract.create_confirm', [ - 'contract_id' => $request->contract_id, - 'user' => $user, - 'park' => $park, - 'psection' => $psection, - 'usertype' => $usertype, - 'user_name' => $user->user_name, - 'active_menu' => 'SWC-8-1' - ]); + try { + // SHJ-1 コマンドを同期実行 + $exitCode = Artisan::call('shj:one', [ + 'user_id' => $user_seq, + 'park_id' => $park_id + ]); + + \Log::info('SHJ-1バッチ処理完了', [ + 'exit_code' => $exitCode, + 'user_seq' => $user_seq, + 'park_id' => $park_id + ]); + + // 処理結果に基づいて遷移先を決定 + if ($exitCode === 0) { + // 成功の場合 + return redirect("/regular-contract/upload_identity_success?contract_id={$contract_id}"); + // return view('regular_contract.create_confirm', [ + // 'contract_id' => $request->contract_id, + // 'user' => $user, + // 'park' => $park, + // 'psection' => $psection, + // 'usertype' => $usertype, + // 'user_name' => $user->user_name, + // 'active_menu' => 'SWC-8-1' + // ]); + } else { + // 失敗の場合 または、学生証 と その他 の場合 + return redirect("/regular-contract/upload_identity_fail?contract_id={$contract_id}"); + + // return view('regular_contract.create_confirm', [ + // 'contract_id' => $request->contract_id, + // 'user' => $user, + // 'park' => $park, + // 'psection' => $psection, + // 'usertype' => $usertype, + // 'user_name' => $user->user_name, + // 'active_menu' => 'SWC-8-1' + // ]); + + } + + } catch (\Exception $e) { + \Log::error('SHJ-1バッチ処理でエラー発生', [ + 'error' => $e->getMessage(), + 'user_seq' => $user_seq, + 'park_id' => $park_id + ]); + + return redirect("/regular-contract/upload_identity_fail?contract_id={$contract_id}"); + } } public function createConfirmNext($contract_id) @@ -786,4 +842,554 @@ class RegularContractCreateController extends Controller // 完了後はウェルネット決済画面(仮)へリダイレクト return redirect()->route('wellnet.payment'); } + + /** + * SHJ-1本人確認処理成功ページ表示!!!!テスト後に削除して!!!!viewも!!!!! + */ + public function showUploadIdentitySuccess(Request $request) + { + $contractId = $request->get('contract_id'); + $userId = Session::get('user_id'); + + // 詳細情報を取得 + $debugInfo = $this->getShjDebugInfo($userId, $contractId); + + return view('regular_contract.upload_identity_success', compact('debugInfo', 'contractId')); + } + + /** + * SHJ-1本人確認処理失敗ページ表示!!!!テスト後に削除して!!!!viewも!!!!! + */ + public function showUploadIdentityFail(Request $request) + { + $contractId = $request->get('contract_id'); + $userId = Session::get('user_id'); + + // 詳細情報を取得 + $debugInfo = $this->getShjDebugInfo($userId, $contractId); + + return view('regular_contract.upload_identity_fail', compact('debugInfo', 'contractId')); + } + + /** + * SHJ-1デバッグ情報取得!!!!テスト後に削除して!!!! + */ + private function getShjDebugInfo($userId, $contractId) + { + try { + // ユーザー情報取得 + $user = DB::table('user')->where('user_id', $userId)->first(); + + // 契約情報取得 + $contract = DB::table('regular_contract')->where('user_id', $user->user_id ?? 0)->first(); + + // 駐輪場情報取得 + $park = null; + if ($contract) { + $park = DB::table('park')->where('park_id', $contract->park_id)->first(); + } + + // 最新のSHJ-1バッチログ取得 + $batchLog = DB::table('batch_log') + ->where('process_name', 'SHJ-1本人確認自動処理') + ->orderBy('created_at', 'desc') + ->first(); + + // 最新のログファイルから詳細ログを取得 + $logContent = $this->getRecentShjLogs(); + + // ログからAPI結果情報を解析 + $apiResults = $this->parseApiResultsFromLogs($logContent); + + return [ + 'user' => $user, + 'contract' => $contract, + 'park' => $park, + 'batch_log' => $batchLog, + 'detailed_logs' => $logContent, + 'recent_logs' => $logContent, // 为调试页面提供日志内容访问 + 'timestamp' => now()->format('Y-m-d H:i:s'), + // API結果情報 + 'ocr_text_length' => $apiResults['ocr_text_length'], + 'ocr_text_preview' => $apiResults['ocr_text_preview'], + 'ocr_full_text' => $apiResults['ocr_full_text'], + 'ocr_threshold' => $apiResults['ocr_threshold'], + 'name_similarity' => $apiResults['name_similarity'], + 'address_similarity' => $apiResults['address_similarity'], + 'name_passed' => $apiResults['name_passed'], + 'address_passed' => $apiResults['address_passed'], + 'matched_address_type' => $apiResults['matched_address_type'] ?? null, // 新增:匹配成功的地址类型 + 'name_match_attempts' => $apiResults['name_match_attempts'], + 'address_match_attempts' => $apiResults['address_match_attempts'], + 'best_name_match' => $apiResults['best_name_match'], + 'best_address_match' => $apiResults['best_address_match'], + 'name_match_details' => $apiResults['name_match_details'], + 'address_match_details' => $apiResults['address_match_details'], + 'ocr_debug_info' => $apiResults['ocr_debug_info'] ?? null, + // 移除重复分析逻辑,只显示SHJ-1的结果 + 'calculated_distance' => $apiResults['calculated_distance'], + 'distance_text' => $apiResults['distance_text'] ?? null, + 'distance_limit' => $apiResults['distance_limit'] ?? null, + 'distance_start_address' => $apiResults['distance_start_address'] ?? null, + 'distance_end_address' => $apiResults['distance_end_address'] ?? null, + 'distance_passed' => $apiResults['distance_passed'], + 'maps_api_status' => $apiResults['maps_api_status'], + 'maps_api_error' => $apiResults['maps_api_error'] + ]; + + } catch (Exception $e) { + \Log::error('Debug info error: ' . $e->getMessage()); + return [ + 'error' => 'デバッグ情報の取得に失敗しました: ' . $e->getMessage(), + 'timestamp' => now()->format('Y-m-d H:i:s') + ]; + } + } + + /** + * 最新のSHJ-1関連ログを取得!!!!テスト後に削除して!!!! + */ + private function getRecentShjLogs() + { + try { + $logPath = storage_path('logs/laravel.log'); + if (!file_exists($logPath)) { + return '日志文件未找到'; + } + + $logs = file_get_contents($logPath); + $lines = explode("\n", $logs); + + // 最新の1000行からSHJ-1関連ログを抽出(さらに範囲拡大) + $recentLines = array_slice($lines, -1000); + $shjLogs = []; + + foreach ($recentLines as $line) { + if (strpos($line, 'SHJ-1') !== false || + strpos($line, 'GoogleVision') !== false || + strpos($line, 'Google Maps') !== false || + strpos($line, 'バッチ処理') !== false) { + $shjLogs[] = $line; + } + } + + return implode("\n", array_slice($shjLogs, -200)); // 最新200行(さらに拡大) + + } catch (Exception $e) { + return 'ログ取得エラー: ' . $e->getMessage(); + } + } + + /** + * ログからAPI結果情報を解析!!!!テスト後に削除して!!!! + */ + private function parseApiResultsFromLogs($logContent) + { + $results = [ + 'ocr_text_length' => 'N/A', + 'ocr_text_preview' => 'N/A', + 'ocr_threshold' => 'N/A', + 'name_similarity' => 'N/A', + 'address_similarity' => 'N/A', + 'name_passed' => false, + 'address_passed' => false, + 'calculated_distance' => 'N/A', + 'distance_passed' => false, + 'maps_api_status' => '成功', + 'maps_api_error' => 'なし', + // 新增详细OCR信息 + 'ocr_full_text' => 'N/A', + 'name_match_attempts' => [], + 'address_match_attempts' => [], + 'best_name_match' => 'N/A', + 'best_address_match' => 'N/A', + 'match_details' => 'N/A' + ]; + + try { + // OCR処理完了ログから文字数とプレビューを抽取(清理格式) + if (preg_match('/GoogleVision OCR処理完了.*"text_length":(\d+).*"text_preview":"([^"]*)"/', $logContent, $matches)) { + $results['ocr_text_length'] = $matches[1]; + // 清理OCR文本,移除可能的日志污染 + $cleanText = $matches[2]; + // 移除可能混入的日志时间戳和标记 + $cleanText = preg_replace('/\[\d{4}-\d{2}-\d{2}.*?\].*?local\.INFO.*?$/', '', $cleanText); + // 移除末尾的不完整JSON片段 + $cleanText = preg_replace('/\s*\{?\s*$/', '', $cleanText); + // 清理换行和多余空格 + $cleanText = trim($cleanText); + $results['ocr_text_preview'] = $cleanText; + } + + // 尝试从GoogleVision日志提取完整的OCR文本 + if (preg_match('/GoogleVision OCR処理完了.*"text_full":"([^"]*)"/', $logContent, $matches)) { + $fullText = $matches[1]; + // 解码JSON转义字符 + $fullText = str_replace('\n', "\n", $fullText); + $fullText = str_replace('\r', "\r", $fullText); + $fullText = str_replace('\"', '"', $fullText); + $fullText = str_replace('\\\\', '\\', $fullText); + $results['ocr_full_text'] = $fullText; + } + + // 尝试从SHJ-1日志提取完整的OCR文本(多种模式匹配) + $patterns = [ + '/SHJ-1 完全OCR認識結果.*"ocr_full_text":"([^"]*)"/', // 原始日文版本 + '/螳悟・OCR隱崎ュ倡オ先棡.*"ocr_full_text":"([^"]*)"/', // 日文编码版本 + '/OCR認識結果.*"ocr_full_text":"([^"]*)"/', // 简化匹配 + '/"ocr_full_text":"([^"]*)"/', // 最宽泛匹配 + ]; + + foreach ($patterns as $pattern) { + if (empty($results['ocr_full_text']) && preg_match($pattern, $logContent, $matches)) { + $encodedText = $matches[1]; + // Base64解码 + $fullText = base64_decode($encodedText); + if ($fullText !== false && strlen($fullText) > 10) { + $results['ocr_full_text'] = $fullText; + + // 简化OCR信息显示 - 只显示基本信息 + $results['ocr_debug_info'] = [ + 'decoded_length' => strlen($fullText), + 'preview' => substr($fullText, 0, 200) + ]; + break; // 找到后停止尝试其他模式 + } + } + } + + // 尝试从OCR认识内容详细日志提取(备选方案,适配日文编码) + if (empty($results['ocr_full_text']) && preg_match('/OCR隱崎ュ伜・螳ケ隧ウ邏ー.*"raw_text":"OCR_TEXT_START>([^<]*) $match[1], + 'score' => round(floatval($match[2]), 2), + 'found_in_ocr' => $match[3] + ]; + } + } + $results['name_match_details'] = $nameMatchDetails; + + // 提取住所匹配详细信息(适配日文编码) + $addressMatchDetails = []; + if (preg_match_all('/菴乗園繝槭ャ繝√Φ繧ー隧ウ邏ー.*"target_address":"([^"]*)".*"address_type":"([^"]*)".*"similarity_score":([^,}]*).*"ocr_contains_address":"([^"]*)"/', $logContent, $addressMatches, PREG_SET_ORDER)) { + foreach ($addressMatches as $match) { + $addressMatchDetails[] = [ + 'target' => $match[1], + 'type' => $match[2], + 'score' => round(floatval($match[3]), 2), + 'found_in_ocr' => $match[4] + ]; + } + } + $results['address_match_details'] = $addressMatchDetails; + + // 从新的SHJ-1日志格式中提取信息 + + // 提取OCR抽出结果 + if (preg_match('/SHJ-1 OCR抽出成功.*"extracted_name":"([^"]*)".*"extracted_address":"([^"]*)".*"ocr_value":"([^"]*)"/', $logContent, $matches)) { + $results['extracted_name'] = $matches[1]; + $results['extracted_address'] = $matches[2]; + $results['extracted_ocr_value'] = $matches[3]; + } + + // 提取居住住所照合结果 + if (preg_match('/SHJ-1 居住住所照合.*"resident_address":"([^"]*)".*"similarity":([^,}]*)/', $logContent, $matches)) { + $results['resident_address'] = $matches[1]; + $results['resident_similarity'] = round(floatval($matches[2]), 2); + } + + // 提取関連住所照合结果 + if (preg_match('/SHJ-1 関連住所照合.*"related_address":"([^"]*)".*"similarity":([^,}]*)/', $logContent, $matches)) { + $results['related_address'] = $matches[1]; + $results['related_similarity'] = round(floatval($matches[2]), 2); + } + + // 提取最終OCR結果 + if (preg_match('/SHJ-1 OCR照合(成功|失敗)/', $logContent, $matches)) { + $results['final_ocr_result'] = $matches[1]; + $results['address_passed'] = ($matches[1] === '成功'); + $results['name_passed'] = ($matches[1] === '成功'); // 新SHJ-1逻辑中,成功表示整体成功 + } + + // 如果OCR文本为空或太短,提供说明 + if (empty($results['ocr_text_preview']) || strlen($results['ocr_text_preview']) < 5) { + $results['ocr_text_preview'] = '(OCR認識内容が短いか、表示できない文字が含まれています)'; + } + + // 表面画像処理完了の詳細結果を抽取 + if (preg_match('/SHJ-1 表面画像処理完了.*"front_result":\{"name_matches":\[([^\]]*)\],"address_matches":\[([^\]]*)\]\}/', $logContent, $matches)) { + $nameMatches = explode(',', $matches[1]); + $addressMatches = explode(',', $matches[2]); + + $results['name_match_attempts'] = array_map(function($val) { + return round(floatval($val), 2); + }, $nameMatches); + + $results['address_match_attempts'] = array_map(function($val) { + return round(floatval($val), 2); + }, $addressMatches); + } + + // OCR類似度計算結果の詳細情報を抽取 + if (preg_match('/SHJ-1 OCR類似度計算結果.*"best_name_match":([^,}]*).*"best_address_match":([^,}]*).*"all_name_matches":\[([^\]]*)\].*"all_address_matches":\[([^\]]*)\]/', $logContent, $matches)) { + $results['best_name_match'] = round(floatval($matches[1]), 2); + $results['best_address_match'] = round(floatval($matches[2]), 2); + + $allNameMatches = explode(',', $matches[3]); + $allAddressMatches = explode(',', $matches[4]); + + $results['name_match_attempts'] = array_map(function($val) { + return round(floatval($val), 2); + }, $allNameMatches); + + $results['address_match_attempts'] = array_map(function($val) { + return round(floatval($val), 2); + }, $allAddressMatches); + } + + // OCR閾値チェックログから類似度情報を抽出(新しい順序匹配対応) + if (preg_match('/SHJ-1 OCR閾値チェック.*"threshold":"?([^",}]*)"?.*"name_match":([^,}]*).*"address_match":([^,}]*).*"name_passed":([^,}]*).*"address_passed":([^,}]*).*"matched_address_type":"?([^",}]*)"?/', $logContent, $matches)) { + $results['ocr_threshold'] = $matches[1]; + $results['name_similarity'] = round(floatval($matches[2]), 2); + $results['address_similarity'] = round(floatval($matches[3]), 2); + $results['name_passed'] = $matches[4] === 'true'; + $results['address_passed'] = $matches[5] === 'true'; + $results['matched_address_type'] = $matches[6] ?: null; + } + + // Google Maps API エラーチェック + if (strpos($logContent, 'Google Maps distance calculation error') !== false) { + $results['maps_api_status'] = 'エラー'; + if (preg_match('/Google Maps distance calculation error.*"error":"([^"]*)"/', $logContent, $matches)) { + $results['maps_api_error'] = $matches[1]; + } + } else if (strpos($logContent, 'Distance calculation failed') !== false) { + $results['maps_api_status'] = 'アドレス未発見'; + $results['maps_api_error'] = 'NOT_FOUND'; + } + + // 距離計算結果を抽取(最新の詳細ログから) + if (preg_match_all('/SHJ-1 距離計算完了.*"calculated_distance_meters":(\d+).*"distance_text":"([^"]*)".*"limit_meters":"?([^",}]*)"?.*"within_limit":([^,}]*)/', $logContent, $allMatches, PREG_SET_ORDER)) { + // 最後の(最新の)マッチを使用 + $matches = end($allMatches); + $results['calculated_distance'] = $matches[1]; // 距離メートル + $results['distance_text'] = $matches[2]; // Google Mapsテキスト + $results['distance_limit'] = $matches[3]; // 制限値 + $results['distance_passed'] = $matches[4] === 'true'; + } else { + // ログから具体的な結果が取得できない場合はデフォルト値を設定 + // ※重要:API成功≠距離制限内ではないため、明示的にfalseにする + $results['distance_passed'] = false; + if (strpos($logContent, 'Distance check error') === false && + strpos($logContent, 'Google Maps distance calculation error') === false) { + $results['calculated_distance'] = '計算成功(制限値詳細はログで確認)'; + $results['maps_api_status'] = '成功(但制限確認要)'; + } else { + $results['calculated_distance'] = '計算失敗'; + $results['maps_api_status'] = 'エラー'; + } + } + + // 距離計算開始ログから起点・終点住所を抽取 + if (preg_match('/SHJ-1 距離計算開始.*"user_address":"([^"]*)".*"park_address":"([^"]*)"/', $logContent, $matches)) { + $results['distance_start_address'] = $matches[1]; + $results['distance_end_address'] = $matches[2]; + } + + } catch (Exception $e) { + \Log::error('API結果解析エラー: ' . $e->getMessage()); + } + + return $results; + } + + // 不再需要的分析方法已移除 - 只显示SHJ-1的结果 + + /** + * 废弃的方法(已不再使用) + */ + private function manualOcrAnalysis($logContent, $user = null) + { + $analysis = [ + 'found_base64' => false, + 'decoded_success' => false, + 'decoded_text' => '', + 'contains_yamada' => false, + 'contains_taro' => false, + 'contains_tokyo' => false, + 'contains_osaka' => false, + 'full_analysis' => 'Analysis failed', + 'corrected_matching' => null + ]; + + try { + // 分割日志内容为行数组,按时间倒序搜索最新的OCR结果 + $logLines = explode("\n", $logContent); + $logLines = array_reverse($logLines); // 从最新的开始搜索 + + // 查找最新的Base64编码OCR结果 + $patterns = [ + '/SHJ-1 完全OCR認識結果.*"ocr_full_text":"([^"]*)"/', + '/SHJ-1.*OCR.*結果.*"ocr_full_text":"([^"]*)"/', + '/"ocr_full_text":"([^"]*)"/' + ]; + + foreach ($logLines as $line) { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $line, $matches)) { + $analysis['found_base64'] = true; + $base64Text = $matches[1]; + + // Base64解码 + $decodedText = base64_decode($base64Text); + if ($decodedText !== false && strlen($decodedText) > 10) { + $analysis['decoded_success'] = true; + $analysis['decoded_text'] = $decodedText; + + // 内容分析 + $analysis['contains_yamada'] = strpos($decodedText, '山田') !== false; + $analysis['contains_taro'] = strpos($decodedText, '太郎') !== false; + $analysis['contains_tokyo'] = strpos($decodedText, '東京') !== false; + $analysis['contains_osaka'] = strpos($decodedText, '大阪') !== false; + + // ユーザー提案:空白と改行を除去して比較 + $analysis['corrected_matching'] = $this->performCorrectedMatching($decodedText, $user); + + // 詳細分析(実際のユーザーデータから期待値を取得) + if ($user) { + $expectedName = $user->user_name ?? ""; + $expectedAddress = ($user->user_regident_pre ?? '') . + ($user->user_regident_city ?? '') . + ($user->user_regident_add ?? '') ?: ""; + } else { + $expectedName = ""; + $expectedAddress = ""; + } + + $analysis['full_analysis'] = + "OCR認識テキスト長: " . strlen($decodedText) . "文字\n" . + "期待氏名: '$expectedName'\n" . + "期待住所: '$expectedAddress'\n" . + "山田を含む: " . ($analysis['contains_yamada'] ? 'YES' : 'NO') . "\n" . + "太郎を含む: " . ($analysis['contains_taro'] ? 'YES' : 'NO') . "\n" . + "東京を含む: " . ($analysis['contains_tokyo'] ? 'YES' : 'NO') . "\n" . + "大阪を含む: " . ($analysis['contains_osaka'] ? 'YES' : 'NO') . "\n" . + "認識完全内容: " . substr($decodedText, 0, 200) . "..."; + + // 成功解码最新OCR结果后立即返回 + return $analysis; + } + // 解码失败时,继续搜索其他条目 + } + } + } + + } catch (Exception $e) { + $analysis['full_analysis'] = 'OCR分析エラー: ' . $e->getMessage(); + } + + return $analysis; + } + + /** + * 修正マッチングアルゴリズム実行(空白・改行除去) + */ + private function performCorrectedMatching($ocrText, $user = null) + { + // ユーザー情報から期待値を取得、デフォルトはテスト用 + if ($user) { + $expectedName = $user->user_name ?? ""; + $expectedAddress = ($user->user_regident_pre ?? '') . + ($user->user_regident_city ?? '') . + ($user->user_regident_add ?? '') ?: ""; + } else { + $expectedName = ""; + $expectedAddress = ""; + } + + // 統一的テキスト正規化関数 + $normalize = function($text) { + // 全空白文字と改行を除去(全角スペース含む) + $text = preg_replace('/[\s\x{3000}]+/u', '', $text); // \x{3000}は全角スペース + // 一般的な空白文字を明示的に除去 + $text = str_replace([' ', ' ', "\t", "\n", "\r"], '', $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); + return $text; + }; + + // 正規化処理 + $normalizedOcr = $normalize($ocrText); + $normalizedExpectedName = $normalize($expectedName); + $normalizedExpectedAddr = $normalize($expectedAddress); + + // 使用"住所"分割OCR文本 + $addressKeyword = '住所'; + $ocrParts = explode($addressKeyword, $normalizedOcr, 2); + + $personalInfoSection = $ocrParts[0] ?? ''; // "住所"前の個人情報欄 + $addressSection = isset($ocrParts[1]) ? $addressKeyword . $ocrParts[1] : ''; // "住所"後の住所欄 + + // 分区マッチング計算 + $nameMatch = $this->calculateSimpleMatch($normalizedExpectedName, $personalInfoSection); + $addrMatch = $this->calculateSimpleMatch($normalizedExpectedAddr, $addressSection); + + return [ + 'original_ocr' => substr($ocrText, 0, 100) . '...', + 'normalized_ocr' => substr($normalizedOcr, 0, 100) . '...', + 'normalized_expected_name' => $normalizedExpectedName, + 'normalized_expected_addr' => $normalizedExpectedAddr, + 'personal_info_section' => substr($personalInfoSection, 0, 80) . '...', + 'address_section' => substr($addressSection, 0, 80) . '...', + 'name_match_score' => $nameMatch, + 'addr_match_score' => $addrMatch, + 'name_passed' => $nameMatch >= 70, + 'addr_passed' => $addrMatch >= 70, + 'overall_result' => ($nameMatch >= 70 && $addrMatch >= 70) ? 'PASS' : 'FAIL' + ]; + } + + /** + * シンプルマッチング計算 + */ + private function calculateSimpleMatch($expected, $haystack) + { + if (empty($expected)) return 0; + + // 1. 完全包含チェック + if (strpos($haystack, $expected) !== false) { + return 100; + } + + // 2. 文字包含率 + $expectedChars = mb_str_split($expected, 1, 'UTF-8'); + $foundChars = 0; + + foreach ($expectedChars as $char) { + if (mb_strpos($haystack, $char, 0, 'UTF-8') !== false) { + $foundChars++; + } + } + + $charRate = ($foundChars / count($expectedChars)) * 100; + + // 3. 類似度計算 + similar_text($expected, $haystack, $similarRate); + + // 最高スコアを返す + return max($charRate, $similarRate); + } } diff --git a/app/Jobs/ProcessSettlementJob.php b/app/Jobs/ProcessSettlementJob.php new file mode 100644 index 0000000..2d3c5f2 --- /dev/null +++ b/app/Jobs/ProcessSettlementJob.php @@ -0,0 +1,183 @@ +settlementTransactionId = $settlementTransactionId; + $this->context = $context; + } + + /** + * ジョブを実行 + * + * SHJ-4Bサービスを使用して決済トランザクション処理を実行 + * + * @return void + */ + public function handle() + { + $startTime = now(); + + // バッチログの開始記録 + $batch = BatchLog::createBatchLog( + 'shj4b', + BatchLog::STATUS_START, + [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'context' => $this->context, + 'job_id' => $this->job->getJobId(), + ], + 'SHJ-4B ProcessSettlementJob start' + ); + + try { + Log::info('SHJ-4B ProcessSettlementJob開始', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'context' => $this->context, + 'start_time' => $startTime, + ]); + + // SHJ-4Bサービスを使用して決済トランザクション処理を実行 + $shjFourBService = app(ShjFourBService::class); + $result = $shjFourBService->processSettlementTransaction( + $this->settlementTransactionId, + $this->context + ); + + // 処理結果に基づいてバッチログを更新 + if ($result['success']) { + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4B ProcessSettlementJob completed successfully', + 'success_count' => 1, + 'parameters' => json_encode([ + 'result' => $result, + ]), + ]); + + Log::info('SHJ-4B ProcessSettlementJob完了', [ + 'settlement_transaction_id' => $this->settlementTransactionId, + 'execution_time' => now()->diffInSeconds($startTime), + 'result' => $result, + ]); + } else { + // ビジネスロジック上の問題(エラーではない) + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4B ProcessSettlementJob completed with issues: ' . $result['reason'], + 'success_count' => 0, + 'parameters' => json_encode([ + 'result' => $result, + 'requires_manual_action' => true, + ]), + ]); + + 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(), + ]); + + // バッチログのエラー記録 + $batch->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => 'SHJ-4B ProcessSettlementJob failed: ' . $e->getMessage(), + 'error_details' => $e->getTraceAsString(), + 'error_count' => 1, + ]); + + // ジョブを失敗させて再試行を促す + 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(), + ]); + + // 最終失敗時の追加処理があればここに記述 + // 例:管理者への通知、障害キューへの登録など + } +} diff --git a/app/Models/Batch/BatchLog.php b/app/Models/Batch/BatchLog.php new file mode 100644 index 0000000..e8001b6 --- /dev/null +++ b/app/Models/Batch/BatchLog.php @@ -0,0 +1,159 @@ + 'integer', + 'start_time' => 'datetime', + 'end_time' => 'datetime', + 'parameters' => 'array', + 'execution_count' => 'integer', + 'success_count' => 'integer', + 'error_count' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * ステータスの定数 + */ + const STATUS_START = 'start'; // 開始 + const STATUS_RUNNING = 'running'; // 実行中 + const STATUS_SUCCESS = 'success'; // 成功 + const STATUS_ERROR = 'error'; // エラー + const STATUS_WARNING = 'warning'; // 警告 + const STATUS_CANCELLED = 'cancelled'; // キャンセル + + + + /** + * 通用バッチログ作成メソッド + * + * 任意のバッチ処理で使用可能な統一ログ記録機能 + * 実際の実行コマンド名をそのまま記録 + * + * @param string $processName プロセス名(shj1, shj9:daily等) + * @param string $status ステータス + * @param array $parameters パラメーター配列 + * @param string $message メッセージ + * @param array $additionalData 追加データ(device_id等) + * @return BatchLog 作成されたバッチログ + */ + public static function createBatchLog( + string $processName, + string $status, + array $parameters = [], + string $message = '', + array $additionalData = [] + ): BatchLog { + // パラメーターに追加データをマージ + $allParameters = array_merge($parameters, $additionalData, [ + 'executed_at' => now()->toISOString() + ]); + + return self::create([ + 'process_name' => $processName, + 'status' => $status, + 'start_time' => now(), + 'end_time' => ($status === self::STATUS_SUCCESS || $status === self::STATUS_ERROR) ? now() : null, + 'parameters' => $allParameters, + 'message' => $message, + 'error_details' => ($status === self::STATUS_ERROR) ? $message : null, + 'execution_count' => 1, + 'success_count' => $status === self::STATUS_SUCCESS ? 1 : 0, + 'error_count' => $status === self::STATUS_ERROR ? 1 : 0 + ]); + } + + + + + + + + /** + * 成功時のステータスコメント生成 + * + * @return string ステータスコメント + */ + public static function getSuccessComment(): string + { + return '処理成功'; + } + + /** + * エラー時のステータスコメント生成 + * + * @param string $errorMessage エラーメッセージ + * @return string ステータスコメント + */ + public static function getErrorComment(string $errorMessage): string + { + return 'エラー: ' . $errorMessage; + } + + /** + * バッチログの文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'BatchLog[ID:%d, Process:%s, Status:%s, Time:%s]', + $this->id, + $this->process_name, + $this->status, + $this->start_time ? $this->start_time->format('Y-m-d H:i:s') : 'N/A' + ); + } +} \ No newline at end of file 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..c23cda5 --- /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', + 'turnsum_count' => 'integer', + 'turnsum' => '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(turnsum_count) as total_turnsum_count, + SUM(turnsum) as total_turnsum, + 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, + 'turnsum_count' => 0, + 'turnsum' => 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->turnsum + + $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..f9cff6f --- /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->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/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/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..d5194b1 --- /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->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/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; - // 一括登録・一括更新可能なカラムの指定 - protected $fillable = []; + // 本人確認チェックフラグの定数 + const USER_ID_CARD_CHK_FLG = [ + 0 => '未確認', + 1 => '確認済み' + ]; - // 一括取得時の対象外項目 - protected $hidden = []; + // 身分証明書種別の定数 + 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 []; + 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/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/ShjServiceProvider.php b/app/Providers/ShjServiceProvider.php new file mode 100644 index 0000000..b4e7161 --- /dev/null +++ b/app/Providers/ShjServiceProvider.php @@ -0,0 +1,40 @@ +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..f08c833 --- /dev/null +++ b/app/Services/GoogleMapsService.php @@ -0,0 +1,184 @@ +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, + ]; + + $response = Http::timeout(30)->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..0170e0b --- /dev/null +++ b/app/Services/GoogleVisionService.php @@ -0,0 +1,331 @@ +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'] // 日本語優先 + ] + ] + ] + ]; + + $response = Http::timeout(30) + ->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/ShjElevenService.php b/app/Services/ShjElevenService.php new file mode 100644 index 0000000..fe0bb23 --- /dev/null +++ b/app/Services/ShjElevenService.php @@ -0,0 +1,432 @@ +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 テーブルとの複合条件 JOIN + ->join('price_a 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], // 有効契約 + ['T2.park_close_flag', '=', 0], // 駐輪場未閉鎖 + ]) + // 契約有効期間内の条件 + ->whereRaw("date_format(now(), '%y %m %d') BETWEEN T1.contract_periods AND T1.contract_periode") + ->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(), + 'sql_conditions' => [ + 'contract_flag' => 1, + 'park_close_flag' => 0, + '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. 取得判定 → 存在しない場合は新規登録 + * 3. 契約台数チェック(限界台数超過判定) + * 4. 契約台数を反映する(ゾーンマスタ更新) + * + * @param array $contractCounts 契約台数集計結果 + * @return array 処理結果 + */ + public function processZoneManagement(array $contractCounts): array + { + $createdZones = 0; + $updatedZones = 0; + $overCapacityCount = 0; + $errors = []; + $processParameters = []; + + try { + DB::beginTransaction(); + + foreach ($contractCounts as $contractData) { + try { + // 【処理2】ゾーンマスタを取得する + $zoneData = $this->getZoneData($contractData); + + if (!$zoneData) { + // 【判断2】ゾーンマスタが存在しない場合 → 新規登録 + $createResult = $this->createZoneData($contractData); + if ($createResult['success']) { + $createdZones++; + } + $zoneData = $createResult['zone_data']; + } + + // 【判断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】契約台数を反映する(ゾーンマスタ更新) + $updateResult = $this->updateZoneContractCount($contractData); + if ($updateResult['success']) { + $updatedZones++; + } + + // 処理パラメータ記録 + $processParameters[] = [ + 'park_id' => $contractData->park_id, + 'psection_id' => $contractData->psection_id, + 'ptype_id' => $contractData->ptype_id, + 'zone_id' => $contractData->zone_id, + 'contract_count' => $contractData->cnt, + 'is_over_capacity' => $isOverCapacity, + 'zone_created' => !$zoneData && $createResult['success'] ?? false, + 'zone_updated' => $updateResult['success'] ?? false + ]; + + } catch (\Exception $e) { + $errors[] = [ + '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() + ]); + } + } + + DB::commit(); + + return [ + 'success' => true, + 'created_zones' => $createdZones, + 'updated_zones' => $updatedZones, + 'over_capacity_count' => $overCapacityCount, + 'parameters' => $processParameters, + 'errors' => $errors, + 'message' => '現在契約台数集計処理完了' + ]; + + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('SHJ-11 ゾーンマスタ管理処理全体エラー', [ + 'error' => $e->getMessage(), + 'processed_count' => count($processParameters) + ]); + + return [ + 'success' => false, + 'created_zones' => $createdZones, + 'updated_zones' => $updatedZones, + 'over_capacity_count' => $overCapacityCount, + 'parameters' => $processParameters, + 'errors' => $errors, + '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' => null, // デフォルトnull + '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' => 'SHJ-11' // 更新オペレータID + ]; + + $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 { + 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() + ]; + } + } + + /** + * 【処理4】バッチ処理ログを作成する + * + * 統一BatchLogシステムを使用してSHJ-11の実行ログを記録 + * + * @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 { + BatchLog::createBatchLog( + 'SHJ-11', + $status, + $parameters, + $message, + [ + 'execution_count' => $executionCount, + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'process_type' => '現在契約台数集計', + 'executed_at' => now()->toISOString() + ] + ); + + } catch (\Exception $e) { + Log::error('SHJ-11 バッチログ作成エラー', [ + 'error' => $e->getMessage(), + 'status' => $status, + 'message' => $message + ]); + } + } +} diff --git a/app/Services/ShjFourBService.php b/app/Services/ShjFourBService.php new file mode 100644 index 0000000..491cb42 --- /dev/null +++ b/app/Services/ShjFourBService.php @@ -0,0 +1,849 @@ + $settlementTransactionId, + 'context' => $context, + 'start_time' => $startTime, + ]); + + try { + // 【前処理】決済トランザクション取得 + $settlement = $this->getSettlementTransaction($settlementTransactionId); + + // 【処理1】定期契約マスタの対象レコード取得 + // 【判断0】取得判定(登録済み判定を含む) + $contractResult = $this->judgeTargetContract($settlement); + + if (!$contractResult['found']) { + // 対象レコードなしの場合 + return $this->handleNoTargetRecord($settlement, $contractResult); + } + + if ($contractResult['already_processed']) { + // 登録済みの場合 + return $this->handleAlreadyProcessed($settlement, $contractResult); + } + + $contract = $contractResult['contract']; + + // 【判断1】授受状態チェック + $statusResult = $this->judgeReceiptStatus($settlement, $contract); + + if (!$statusResult['valid']) { + // 授受状態が異常な場合 + return $this->handleInvalidStatus($settlement, $contract, $statusResult); + } + + // 【判断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); + + return $result; + + } catch (\Throwable $e) { + Log::error('SHJ-4B 決済トランザクション処理失敗', [ + 'settlement_transaction_id' => $settlementTransactionId, + 'context' => $context, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + 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.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: batch_logで同一決済の処理完了記録があるか + $existingBatchLog = BatchLog::where('process_name', 'shj4b') + ->where('status', BatchLog::STATUS_SUCCESS) + ->where('parameters', 'like', '%"settlement_transaction_id":' . $settlement->settlement_transaction_id . '%') + ->exists(); + + if ($existingBatchLog) { + return [ + 'processed' => true, + 'reason' => "batch_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実行処理(新規のみ) + * + * @param object $contract + * @return array + */ + private function triggerShjThirteen($contract): array + { + // TODO: SHJ-13の具体的な処理を実装 + // 現在はプレースホルダー + + Log::info('SHJ-4B SHJ-13実行処理', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'park_id' => $contract->park_id, + ]); + + return [ + 'triggered' => true, + 'method' => 'placeholder', + 'message' => 'SHJ-13処理は実装予定です', + 'contract_id' => $contract->contract_id, + ]; + } + + /** + * 利用者メール送信処理 + * + * @param SettlementTransaction $settlement + * @param object $contract + * @param array $amountResult + * @return array + */ + private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, array $amountResult): array + { + // TODO: 実際のメール送信処理を実装 + // 現在はプレースホルダー + + $mailType = ($amountResult['comparison'] === self::AMOUNT_MATCH) ? 'success' : 'error'; + + Log::info('SHJ-4B 利用者メール送信処理', [ + 'contract_id' => $contract->contract_id, + 'user_id' => $contract->user_id, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'mail_type' => $mailType, + 'amount_comparison' => $amountResult['comparison'], + ]); + + return [ + 'sent' => true, + 'method' => 'placeholder', + 'mail_type' => $mailType, + 'message' => '利用者メール送信処理は実装予定です', + ]; + } + + /** + * オペレーターキューへの登録 + * + * @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' => 'オペレーターキュー登録処理は実装予定です', + ]; + } +} diff --git a/app/Services/ShjFourCService.php b/app/Services/ShjFourCService.php new file mode 100644 index 0000000..63cf75e --- /dev/null +++ b/app/Services/ShjFourCService.php @@ -0,0 +1,406 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->batchLogModel = $batchLogModel; + } + + /** + * 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 + { + $batchLogId = null; + + try { + // バッチ処理開始ログ作成(実際のコマンド名を記録) + $batchLog = BatchLog::createBatchLog( + 'shj4c', + BatchLog::STATUS_START, + [ + 'park_id' => $parkId, + 'ptype_id' => $ptypeId, + 'psection_id' => $psectionId + ], + 'SHJ-4C 室割当処理開始' + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-4C 室割当処理開始', [ + 'batch_log_id' => $batchLogId, + 'park_id' => $parkId, + 'ptype_id' => $ptypeId, + 'psection_id' => $psectionId + ]); + + // 【処理1】ゾーン情報取得 + $zoneInfo = $this->getZoneInformation($parkId, $ptypeId, $psectionId); + + if (empty($zoneInfo)) { + $message = '対象のゾーン情報が見つかりません'; + + // バッチログ更新(通用方法使用) + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $message, + 'error_details' => $message, + 'error_count' => 1 + ]); + + return [ + 'success' => false, + 'message' => $message, + 'batch_log_id' => $batchLogId + ]; + } + + // 【判断1】割当判定処理 + $allocationResult = $this->performAllocationJudgment($zoneInfo, $parkId, $ptypeId, $psectionId); + + if (!$allocationResult['can_allocate']) { + // 割当NGの場合、対象事室番号を設定 + $this->setTargetRoomNumber($allocationResult['target_room_number']); + + // バッチログ更新(警告) + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => '割当処理NG: ' . $allocationResult['reason'], + 'success_count' => 1 // 処理は成功したが割当NGのため + ]); + + return [ + 'success' => true, + 'message' => '割当判定完了(割当NG)', + 'allocation_result' => $allocationResult, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理2】割当実行(割当OKの場合) + $executionResult = $this->executeAllocation($zoneInfo, $allocationResult); + + // バッチ処理完了ログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4C 室割当処理正常完了', + 'success_count' => 1 + ]); + + Log::info('SHJ-4C 室割当処理完了', [ + 'batch_log_id' => $batchLogId, + 'execution_result' => $executionResult + ]); + + // 【処理3】処理結果返却 + return [ + 'success' => true, + 'message' => 'SHJ-4C 室割当処理が正常に完了しました', + 'zone_info' => $zoneInfo, + 'allocation_result' => $allocationResult, + 'execution_result' => $executionResult, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-4C 室割当処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-4C 室割当処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理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_contracted_count as zone_contracted_count', + 'T1.zone_capacity as zone_capacity', + 'T1.zone_permitted_count as zone_permitted_count', + '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】割当判定処理 + * + * ゾーン内の定期契約マスタから該当レコード数を取得し、 + * 定期契約マスタから最適な車室番号を選定する割当判定を実行 + * + * @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) { + // 各ゾーンに対する定期契約情報取得 + $contractInfo = $this->getRegularContractInfo($parkId, $zone->zone_id, $ptypeId, $psectionId); + + // 割当可能性判定 + if ($this->canAllocateToZone($zone, $contractInfo)) { + return [ + 'can_allocate' => true, + 'zone_id' => $zone->zone_id, + 'zone_name' => $zone->zone_name, + 'contract_info' => $contractInfo, + 'reason' => '割当OK: ゾーンID ' . $zone->zone_id + ]; + } + } + + // 全ゾーンで割当NGの場合 + $targetRoomNumber = $this->generateTargetRoomNumber($parkId, $ptypeId, $psectionId); + + return [ + 'can_allocate' => false, + 'target_room_number' => $targetRoomNumber, + 'reason' => '全ゾーンで割当できませんでした' + ]; + + } catch (\Exception $e) { + Log::error('割当判定処理エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 定期契約情報取得 + * + * 設計書のSQLクエリに基づく定期契約マスタ検索 + * + * @param int $parkId 駐輪場ID + * @param int $zoneId ゾーンID + * @param int $ptypeId 駐輪分類ID + * @param int $psectionId 車種区分ID + * @return array 定期契約情報 + */ + private function getRegularContractInfo(int $parkId, int $zoneId, int $ptypeId, int $psectionId): array + { + $currentDate = Carbon::now()->format('Y-m-d'); + + $contractInfo = DB::table('regular_contract as T1') + ->select(['T1.contract_id']) + ->where('T1.park_id', $parkId) + ->where('T1.zone_id', $zoneId) + ->where('T1.ptype_id', $ptypeId) + ->where('T1.psection_id', $psectionId) + ->where('T1.place_no', '!=', '') + ->where(function ($query) use ($currentDate) { + $query->where(function ($subQuery) use ($currentDate) { + // パターンA: 処理1.更新期間開始日 <= 処理1.更新期間終了日 の場合 + $subQuery->whereRaw("date_format(now(), '%y-%m-%d') >= date_format(now(), '%y-%m-%d')") + ->whereRaw("date_format(now(), '%y-%m-%d') <= date_format(now(), '%y-%m-%d')"); + }) + ->orWhere(function ($subQuery) use ($currentDate) { + // パターンB: その他の場合 + $subQuery->whereRaw("date_format(now(), '%y-%m-%d') >= date_format(now(), '%y-%m-%d')"); + }); + }) + ->where('T1.contract_flag', 1) + ->get() + ->toArray(); + + return $contractInfo; + } + + /** + * ゾーン割当可能性判定 + * + * @param object $zone ゾーン情報 + * @param array $contractInfo 契約情報 + * @return bool 割当可能かどうか + */ + private function canAllocateToZone($zone, array $contractInfo): bool + { + $contractedCount = count($contractInfo); + $capacity = $zone->zone_capacity; + + // 空きがある場合は割当可能 + return $contractedCount < $capacity; + } + + /** + * 対象事室番号生成 + * + * @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 + ]); + + // 実際の事室番号設定ロジックをここに実装 + // 具体的な仕様が必要な場合は後で追加実装 + } + + /** + * 割当実行 + * + * @param array $zoneInfo ゾーン情報 + * @param array $allocationResult 割当判定結果 + * @return array 実行結果 + */ + private function executeAllocation(array $zoneInfo, array $allocationResult): array + { + // 割当実行の具体的なロジックを実装 + // 設計書に詳細仕様があれば追加実装 + + return [ + 'executed' => true, + 'zone_id' => $allocationResult['zone_id'], + 'message' => '割当実行完了' + ]; + } + + +} \ No newline at end of file diff --git a/app/Services/ShjMailSendService.php b/app/Services/ShjMailSendService.php new file mode 100644 index 0000000..64d9513 --- /dev/null +++ b/app/Services/ShjMailSendService.php @@ -0,0 +1,545 @@ +mailTemplateModel = $mailTemplateModel; + $this->batchLogModel = $batchLogModel; + } + + /** + * SHJ メール送信処理メイン実行 + * + * 処理フロー: + * 【処理1】入力パラメーターをチェックする + * 【処理2】メール送信テンプレート情報を取得する + * 【判断2】取得結果判定 + * 【処理3】メールを送信する + * 【処理4】処理結果を返却する + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param int $mailTemplateId メールテンプレートID + * @return array 処理結果 + */ + public function executeMailSend(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array + { + $batchLogId = null; + + try { + // バッチ処理開始ログ作成(実際のコマンド名を記録) + $batchLog = BatchLog::createBatchLog( + 'shj-mail-send', + BatchLog::STATUS_START, + [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ], + 'SHJ メール送信処理開始' + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ メール送信処理開始', [ + 'batch_log_id' => $batchLogId, + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ]); + + // 【処理1】入力パラメーターをチェックする + $paramCheckResult = $this->checkInputParameters($mailAddress, $backupMailAddress, $mailTemplateId); + if (!$paramCheckResult['valid']) { + $this->updateBatchLog($batchLogId, 'error', $paramCheckResult['message']); + + return [ + 'success' => false, + 'result_code' => 1, // 異常終了 + 'message' => $paramCheckResult['message'], + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理2】メール送信テンプレート情報を取得する + $templateInfo = $this->getMailTemplateInfo($mailTemplateId); + + // 【判断2】取得結果判定 + if (empty($templateInfo)) { + $message = "メールテンプレートが存在しません。テンプレートID: {$mailTemplateId}"; + $this->updateBatchLog($batchLogId, 'error', $message); + + return [ + 'success' => false, + 'result_code' => 1, // 異常終了 + 'message' => $message, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理3】メールを送信する + $mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo); + + if (!$mailSendResult['success']) { + $this->updateBatchLog($batchLogId, 'error', $mailSendResult['message']); + + return [ + 'success' => false, + 'result_code' => 1, // 異常終了 + 'message' => $mailSendResult['message'], + 'batch_log_id' => $batchLogId + ]; + } + + // バッチ処理完了ログ更新 + $this->updateBatchLog($batchLogId, 'success', 'SHJ メール送信処理正常完了'); + + Log::info('SHJ メール送信処理完了', [ + 'batch_log_id' => $batchLogId, + 'mail_send_result' => $mailSendResult + ]); + + // 【処理4】処理結果を返却する + return [ + 'success' => true, + 'result_code' => 0, // 正常終了 + 'message' => 'SHJ メール送信処理が正常に完了しました', + 'mail_send_result' => $mailSendResult, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ メール送信処理でエラーが発生: ' . $e->getMessage(); + + if ($batchLogId) { + $this->updateBatchLog($batchLogId, 'error', $errorMessage); + } + + Log::error('SHJ メール送信処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'result_code' => 1, // 異常終了 + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】入力パラメーターをチェックする + * + * 仕様書に基づく詳細チェック: + * - メールアドレス形式チェック + * - テンプレートID存在性チェック + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param int $mailTemplateId メールテンプレートID + * @return array チェック結果 + */ + private function checkInputParameters(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array + { + try { + // メールアドレス存在チェック(いずれか必須) + if (empty($mailAddress) && empty($backupMailAddress)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: メールアドレスまたは予備メールアドレスのいずれかは必須です' + ]; + } + + // メールアドレス形式チェック + if (!empty($mailAddress) && !filter_var($mailAddress, FILTER_VALIDATE_EMAIL)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: メールアドレスの形式が正しくありません' + ]; + } + + if (!empty($backupMailAddress) && !filter_var($backupMailAddress, FILTER_VALIDATE_EMAIL)) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: 予備メールアドレスの形式が正しくありません' + ]; + } + + // メールテンプレートID形式チェック + if ($mailTemplateId <= 0) { + return [ + 'valid' => false, + 'message' => 'パラメーターNG: メールテンプレートIDは正の整数である必要があります' + ]; + } + + Log::info('入力パラメーターチェック完了', [ + 'mail_address' => $mailAddress, + 'backup_mail_address' => $backupMailAddress, + 'mail_template_id' => $mailTemplateId + ]); + + return [ + 'valid' => true, + 'message' => 'パラメーターチェックOK' + ]; + + } catch (\Exception $e) { + Log::error('入力パラメーターチェックエラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'valid' => false, + 'message' => 'パラメーターチェック中にエラーが発生しました: ' . $e->getMessage() + ]; + } + } + + /** + * 【処理2】メール送信テンプレート情報を取得する + * + * 仕様書に基づくSQLクエリ: + * SELECT エリアマネージャー同報, bccアドレス, 件名, 本文 + * FROM メール送信テンプレート + * WHERE 使用プログラムID = 入力パラメーター使用プログラムID + * AND 使用フラグ = 1 + * + * @param int $mailTemplateId メールテンプレートID(使用プログラムIDとして扱う) + * @return MailTemplate|null メールテンプレート情報 + */ + private function getMailTemplateInfo(int $mailTemplateId): ?MailTemplate + { + try { + // 仕様書に記載されたSQLクエリに基づくメールテンプレート情報取得 + // 注意: 仕様書では「使用プログラムID」を条件にしているが、 + // 入力パラメーターは「メールテンプレートID」なので、pg_idで検索 + $templateInfo = $this->mailTemplateModel::where('pg_id', $mailTemplateId) + ->where('use_flag', 1) + ->first(); + + if ($templateInfo) { + Log::info('メールテンプレート情報取得完了', [ + 'mail_template_id' => $mailTemplateId, + 'template_found' => true, + 'subject' => $templateInfo->getSubject() + ]); + } else { + Log::warning('メールテンプレート情報取得結果', [ + 'mail_template_id' => $mailTemplateId, + 'template_found' => false, + 'message' => 'テンプレートが見つかりません' + ]); + } + + return $templateInfo; + + } catch (\Exception $e) { + Log::error('メールテンプレート情報取得エラー', [ + 'mail_template_id' => $mailTemplateId, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【処理3】メールを送信する + * + * 仕様書に基づくmb_send_mail関数使用: + * - 送信者: 処理2で取得したメールアドレス(処理1で予備メールアドレス) + * - タイトル: 処理2で取得したタイトル + * - 本文: 処理2で取得した本文(※現在の文字列は「So-Manager一般的なWebサイト内部処理」参照) + * - 追加ヘッダ: 処理2で取得したbccアドレス(※値が設定されている場合のみ) + * + * @param string $mailAddress メールアドレス + * @param string $backupMailAddress 予備メールアドレス + * @param MailTemplate $templateInfo テンプレート情報 + * @return array 送信結果 + */ + private function sendMail(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array + { + try { + // 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス) + $toAddress = !empty($mailAddress) ? $mailAddress : $backupMailAddress; + + // メール内容取得 + $subject = $templateInfo->getSubject() ?? ''; + $message = $templateInfo->getText() ?? ''; + + // 追加ヘッダ設定 + $headers = $this->buildMailHeaders($templateInfo); + + Log::info('メール送信準備完了', [ + 'to_address' => $toAddress, + 'subject' => $subject, + 'has_bcc' => !empty($templateInfo->getBccAddress()), + 'manager_cc_enabled' => $templateInfo->isManagerCcEnabled() + ]); + + // mb_send_mail関数を使用してメール送信 + $sendResult = mb_send_mail( + $toAddress, // 送信先 + $subject, // 件名 + $message, // 本文 + $headers // 追加ヘッダ + ); + + if ($sendResult) { + Log::info('メール送信成功', [ + 'to_address' => $toAddress, + 'subject' => $subject + ]); + + return [ + 'success' => true, + 'message' => 'メール送信が正常に完了しました', + 'to_address' => $toAddress, + 'subject' => $subject + ]; + } else { + $errorMessage = 'mb_send_mail関数でメール送信に失敗しました'; + Log::error('メール送信失敗', [ + 'to_address' => $toAddress, + 'subject' => $subject, + 'error' => $errorMessage + ]); + + return [ + 'success' => false, + 'message' => $errorMessage + ]; + } + + } catch (\Exception $e) { + $errorMessage = 'メール送信中にエラーが発生: ' . $e->getMessage(); + Log::error('メール送信エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage + ]; + } + } + + /** + * メールヘッダを構築 + * + * 仕様書に基づく設定: + * - BCCアドレス: 値が設定されている場合のみ追加 + * - エリアマネージャー同報: フラグが有効な場合の処理 + * + * @param MailTemplate $templateInfo テンプレート情報 + * @return string メールヘッダ + */ + private function buildMailHeaders(MailTemplate $templateInfo): string + { + $headers = []; + + // BCCアドレス設定(値が設定されている場合のみ) + $bccAddress = $templateInfo->getBccAddress(); + if (!empty($bccAddress)) { + $headers[] = "Bcc: {$bccAddress}"; + } + + // エリアマネージャー同報フラグが有効な場合 + // ※具体的な処理内容は仕様書に詳細がないため、ログ出力のみ + if ($templateInfo->isManagerCcEnabled()) { + Log::info('エリアマネージャー同報フラグが有効です', [ + '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); + } + + /** + * バッチログ作成 + * + * @param string $processName プロセス名 + * @param string $status ステータス + * @param array $params パラメータ + * @return int バッチログID + */ + private function createBatchLog(string $processName, string $status, array $params = []): int + { + return $this->batchLogModel->create([ + 'process_name' => $processName, + 'status' => $status, + 'start_time' => now(), + 'parameters' => json_encode($params), + 'message' => 'バッチ処理開始' + ])->id; + } + + /** + * バッチログ更新 + * + * @param int $batchLogId バッチログID + * @param string $status ステータス + * @param string $message メッセージ + * @return void + */ + private function updateBatchLog(int $batchLogId, string $status, string $message): void + { + $this->batchLogModel->where('id', $batchLogId)->update([ + 'status' => $status, + 'end_time' => now(), + 'message' => $message + ]); + } + + /** + * 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 + ]; + } + } +} \ No newline at end of file diff --git a/app/Services/ShjNineService.php b/app/Services/ShjNineService.php new file mode 100644 index 0000000..f5e0957 --- /dev/null +++ b/app/Services/ShjNineService.php @@ -0,0 +1,595 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->earningsSummaryModel = $earningsSummaryModel; + $this->psectionModel = $psectionModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + } + + /** + * SHJ-9 売上集計処理メイン実行 + * + * 処理フロー: + * 【処理1】集計対象を設定する + * 【処理2】駐輪場マスタを取得する + * 【判断1】取得件数判定 + * 【処理3】車種区分毎に算出する + * 【判断2】取得判定 + * 【処理4】売上集計結果を削除→登録する + * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する + * + * @param string $type 集計種別(daily/monthly/yearly) + * @param string $aggregationDate 集計対象日 + * @return array 処理結果 + */ + public function executeEarningsAggregation(string $type, string $aggregationDate): array + { + $batchLogId = null; + + try { + // 【処理1】集計対象を設定する + $aggregationTarget = $this->setAggregationTarget($type, $aggregationDate); + + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj9', + BatchLog::STATUS_START, + [ + 'type' => $type, + 'aggregation_date' => $aggregationDate, + 'aggregation_target' => $aggregationTarget + ], + "SHJ-9 売上集計処理開始 ({$type})" + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-9 売上集計処理開始', [ + 'batch_log_id' => $batchLogId, + 'type' => $type, + 'aggregation_date' => $aggregationDate, + 'aggregation_target' => $aggregationTarget + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $message = '売上集計(' . $this->getTypeLabel($type) . '):駐輪場マスタが存在していません。'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 // 処理は成功したが対象なし + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => 0, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $parkSummaryRecords = $this->processEarningsForPark($park, $aggregationTarget, $type); + + if ($parkSummaryRecords > 0) { + $processedParks++; + $summaryRecords += $parkSummaryRecords; + } + } + + // 【判断2】取得判定 + if ($summaryRecords === 0) { + $message = '対象なしの結果を設定する(契約)'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => $processedParks, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // バッチ処理完了ログ更新 + $completionMessage = "SHJ-9 売上集計処理正常完了 ({$type}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => $completionMessage, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($completionMessage, $batchLogId); + + Log::info('SHJ-9 売上集計処理完了', [ + 'batch_log_id' => $batchLogId, + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-9 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-9 売上集計処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-9 売上集計処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】集計対象を設定する + * + * @param string $type 集計種別 + * @param string $aggregationDate 集計対象日 + * @return array 集計対象情報 + */ + private function setAggregationTarget(string $type, string $aggregationDate): array + { + $date = Carbon::parse($aggregationDate); + + switch ($type) { + case 'daily': + return [ + 'type' => 'daily', + 'start_date' => $date->format('Y-m-d'), + 'end_date' => $date->format('Y-m-d'), + 'summary_type' => '日次' + ]; + + case 'monthly': + return [ + 'type' => 'monthly', + 'start_date' => $date->startOfMonth()->format('Y-m-d'), + 'end_date' => $date->endOfMonth()->format('Y-m-d'), + 'summary_type' => '月次' + ]; + + case 'yearly': + return [ + 'type' => 'yearly', + 'start_date' => $date->startOfYear()->format('Y-m-d'), + 'end_date' => $date->endOfYear()->format('Y-m-d'), + 'summary_type' => '年次' + ]; + + default: + throw new \InvalidArgumentException("不正な集計種別: {$type}"); + } + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * 仕様書のSQLクエリに基づく駐輪場情報取得 + * SELECT 駐輪場ID, 駐輪場名 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ <> 1 + * ORDER BY 駐輪場ふりがな + * + * @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 string $type 集計種別 + * @return int 作成された集計レコード数 + */ + private function processEarningsForPark($park, array $aggregationTarget, string $type): int + { + try { + // 【処理4】既存の売上集計結果を削除 + $this->deleteExistingSummary($park->park_id, $aggregationTarget); + + // 【処理3】車種区分毎に算出する + $psections = $this->getPsectionInformation(); + $summaryRecords = 0; + + foreach ($psections as $psection) { + $earningsData = $this->calculateEarningsForPsection( + $park->park_id, + $psection->psection_id, + $aggregationTarget + ); + + if ($this->hasEarningsData($earningsData)) { + // 売上集計結果を登録 + $this->createEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $type); + $summaryRecords++; + } + } + + Log::info('駐輪場売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords + ]); + + return $summaryRecords; + + } catch (\Exception $e) { + Log::error('駐輪場売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 車種区分情報取得 + * + * @return array 車種区分情報 + */ + private function getPsectionInformation(): array + { + return DB::table('psection') + ->select(['psection_id', 'psection_subject']) + ->get() + ->toArray(); + } + + /** + * 【処理3】車種区分毎に売上を算出する + * + * 4つの項目を計算: + * ①売上・件数 + * ②一時金売上 + * ③解約返戻金 + * ④再発行金額・件数 + * + * @param int $parkId 駐輪場ID + * @param int $psectionId 車種区分ID + * @param array $aggregationTarget 集計対象 + * @return array 売上データ + */ + private function calculateEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // ①売上・件数(billing_amount) + $salesData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as sales_count'), + DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNull('contract_cancelday') + ->first(); + + // ②一時金売上(contract_money) + $temporaryData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as temporary_count'), + DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNotNull('contract_money') + ->where('contract_money', '>', 0) + ->first(); + + // ③解約返戻金(refunds) + $refundData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as refund_count'), + DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->whereBetween('contract_cancelday', [$startDate, $endDate]) + ->whereNotNull('refunds') + ->where('refunds', '>', 0) + ->first(); + + // ④再発行金額・件数(seal_reissue_request) + $reissueData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as reissue_count'), + DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('seal_reissue_request', 1) + ->whereBetween('updated_at', [$startDate, $endDate]) + ->first(); + + return [ + 'sales_count' => $salesData->sales_count ?? 0, + 'sales_amount' => $salesData->sales_amount ?? 0, + 'temporary_count' => $temporaryData->temporary_count ?? 0, + 'temporary_amount' => $temporaryData->temporary_amount ?? 0, + 'refund_count' => $refundData->refund_count ?? 0, + 'refund_amount' => $refundData->refund_amount ?? 0, + 'reissue_count' => $reissueData->reissue_count ?? 0, + 'reissue_amount' => $reissueData->reissue_amount ?? 0 + ]; + } + + /** + * 売上データの存在チェック + * + * @param array $earningsData 売上データ + * @return bool データが存在するかどうか + */ + private function hasEarningsData(array $earningsData): bool + { + return $earningsData['sales_count'] > 0 || + $earningsData['temporary_count'] > 0 || + $earningsData['refund_count'] > 0 || + $earningsData['reissue_count'] > 0; + } + + /** + * 【処理4】既存の売上集計結果を削除 + * + * @param int $parkId 駐輪場ID + * @param array $aggregationTarget 集計対象 + * @return void + */ + private function deleteExistingSummary(int $parkId, array $aggregationTarget): void + { + DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_start_date', $aggregationTarget['start_date']) + ->where('summary_end_date', $aggregationTarget['end_date']) + ->delete(); + } + + /** + * 売上集計結果を登録 + * + * @param object $park 駐輪場情報 + * @param object $psection 車種区分情報 + * @param array $aggregationTarget 集計対象 + * @param array $earningsData 売上データ + * @param string $type 集計種別 + * @return void + */ + private function createEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, string $type): void + { + DB::table('earnings_summary')->insert([ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], + 'summary_start_date' => $aggregationTarget['start_date'], + 'summary_end_date' => $aggregationTarget['end_date'], + 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 + 'psection_id' => $psection->psection_id, + 'usertype_subject1' => $psection->psection_subject, + 'regular_new_count' => $earningsData['sales_count'], + 'regular_new_amount' => $earningsData['sales_amount'], + 'turnsum' => $earningsData['temporary_amount'], + 'turnsum_count' => $earningsData['temporary_count'], + 'refunds' => $earningsData['refund_amount'], + 'reissue_count' => $earningsData['reissue_count'], + 'reissue_amount' => $earningsData['reissue_amount'], + 'summary_note' => "SHJ-9 {$type} 売上集計結果", + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => 0 // システム処理 + ]); + } + + /** + * 【処理5】オペレータキュー作成 + * + * @param string $message メッセージ + * @param int $batchLogId バッチログID + * @return void + */ + private function createOperatorQueue(string $message, int $batchLogId): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 9, // SHJ-9用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $message, + 'que_status' => 1, // 完了 + 'que_status_comment' => 'バッチ処理完了', + 'work_instructions' => "SHJ-9売上集計処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'batch_log_id' => $batchLogId, + 'message' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 集計種別のラベル取得 + * + * @param string $type 集計種別 + * @return string ラベル + */ + private function getTypeLabel(string $type): string + { + switch ($type) { + case 'daily': + return '日次'; + case 'monthly': + return '月次'; + case 'yearly': + return '年次'; + default: + return $type; + } + } +} diff --git a/app/Services/ShjOneService.php b/app/Services/ShjOneService.php new file mode 100644 index 0000000..4091f57 --- /dev/null +++ b/app/Services/ShjOneService.php @@ -0,0 +1,995 @@ +googleVisionService = $googleVisionService; + $this->googleMapsService = $googleMapsService; + $this->mailSendService = $mailSendService; + } + + /** + * 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 + ]); + return [ + 'system_success' => false, + 'message' => '利用者が見つかりません', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + } + + 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' => '免許証以外または写真なしまたは既に処理済み' + ]); + return [ + 'system_success' => false, + 'message' => '処理対象外の利用者です(免許証以外または写真なし)', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + } + + $park = $this->getParkRecord($parkId); + if (!$park) { + Log::error('SHJ-1 駐輪場データ取得失敗', [ + 'park_id' => $parkId + ]); + return [ + 'system_success' => false, + 'message' => '駐輪場が見つかりません', + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + } + + 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(); // 対象外処理後はコミット + 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(); + + return $identityResult; + + } catch (Exception $e) { + DB::rollBack(); + Log::error('SHJ-1処理エラー', [ + 'user_id' => $userId, + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + + return [ + 'system_success' => false, + 'message' => 'システムエラーが発生しました: ' . $e->getMessage(), + 'stats' => ['error_count' => 1, 'processed_count' => 0] + ]; + } + } + + /** + * 利用者レコード取得(設計書の条件に従ってフィルタリング) + */ + 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' => '本人確認自動処理が成功しました', + '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のため手動処理キューを作成しました', + '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 { + $this->mailSendService->executeMailSend( + $user->user_primemail, + $user->user_submail, + config('shj1.mail.program_id_success') + ); + } catch (Exception $e) { + Log::error('Success email sending failed', [ + 'user_id' => $user->user_seq, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 失敗メール送信 + */ + private function sendFailureEmail(User $user, Park $park): void + { + try { + $this->mailSendService->executeMailSend( + $user->user_primemail, + $user->user_submail, + config('shj1.mail.program_id_failure') + ); + } catch (Exception $e) { + Log::error('Failure email sending failed', [ + '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; + } + +} \ No newline at end of file diff --git a/app/Services/ShjSixService.php b/app/Services/ShjSixService.php new file mode 100644 index 0000000..9ec63af --- /dev/null +++ b/app/Services/ShjSixService.php @@ -0,0 +1,710 @@ +deviceModel = $deviceModel; + $this->hardwareCheckLogModel = $hardwareCheckLogModel; + $this->printJobLogModel = $printJobLogModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + $this->mailSendService = $mailSendService; + } + + /** + * SHJ-6 サーバ死活監視処理メイン実行 + * + * 処理フロー: + * 【処理1】サーバ死活監視(DBアクセス) + * 【処理2】デバイス管理マスタを取得する + * 【処理3】デバイス毎のハードウェア状態を取得する + * 【処理4】プリンタ制御プログラムログを取得する + * 【判断3】エラーログ有無 + * 【処理5】バッチ処理ログを作成する + * ※ 異常時は共通A処理を実行 + * + * @return array 処理結果 + */ + public function executeServerMonitoring(): array + { + $batchLogId = null; + $warnings = []; + $errorDetails = []; + + try { + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj6', + BatchLog::STATUS_START, + [], + 'SHJ-6 サーバ死活監視処理開始' + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-6 サーバ死活監視処理開始', [ + 'batch_log_id' => $batchLogId + ]); + + // 【処理1】サーバ死活監視(DBアクセス) + $dbAccessResult = $this->checkDatabaseAccess(); + if (!$dbAccessResult['success']) { + // DB接続NGの場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'DB接続エラー: ' . $dbAccessResult['message']); + + return [ + 'success' => false, + 'message' => 'データベース接続エラーが発生しました', + 'error_details' => [$dbAccessResult['message']], + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理2】デバイス管理マスタを取得する + $devices = $this->getDeviceManagementData(); + Log::info('デバイス管理マスタ取得完了', [ + 'device_count' => count($devices) + ]); + + // 【処理3】デバイス毎のハードウェア状態を取得する + $hardwareStatusResult = $this->getHardwareStatus($devices); + if (!$hardwareStatusResult['success']) { + // ハードウェア状態取得できなかった場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'ハードウェア状態取得エラー: ' . $hardwareStatusResult['message']); + $warnings[] = 'ハードウェア状態の一部で異常を検出しました'; + $errorDetails[] = $hardwareStatusResult['message']; + } + + // 【処理4】プリンタ制御プログラムログを取得する + $printerLogResult = $this->getPrinterControlLogs(); + + // 【判断3】エラーログ有無 + if ($printerLogResult['has_errors']) { + // エラーログ有の場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'プリンタエラーログ検出: ' . $printerLogResult['error_summary']); + $warnings[] = 'プリンタ制御でエラーが検出されました'; + $errorDetails[] = $printerLogResult['error_summary']; + } + + // 【処理5】バッチ処理ログを作成する + $monitoringSummary = $this->createMonitoringSummary($devices, $hardwareStatusResult, $printerLogResult); + + $status = empty($warnings) ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_WARNING; + $message = empty($warnings) ? + 'SHJ-6 サーバ死活監視処理正常完了' : + 'SHJ-6 サーバ死活監視処理完了(警告あり)'; + + $batchLog->update([ + 'status' => $status, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + Log::info('SHJ-6 サーバ死活監視処理完了', [ + 'batch_log_id' => $batchLogId, + 'monitoring_summary' => $monitoringSummary, + 'warnings' => $warnings + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-6 サーバ死活監視処理が完了しました', + 'monitoring_summary' => $monitoringSummary, + 'warnings' => $warnings, + 'error_details' => $errorDetails, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-6 サーバ死活監視処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + // 例外発生時も共通A処理実行 + $this->executeCommonProcessA($batchLogId, $errorMessage); + + Log::error('SHJ-6 サーバ死活監視処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'error_details' => [$e->getMessage()], + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】サーバ死活監視(DBアクセス) + * + * @return array アクセス結果 + */ + private function checkDatabaseAccess(): array + { + try { + // 設定マスタテーブルへの簡単なクエリでDB接続確認 + $result = DB::select('SELECT 1 as test'); + + if (empty($result)) { + return [ + 'success' => false, + 'message' => 'データベースクエリの結果が空です' + ]; + } + + Log::info('データベース接続確認成功'); + + return [ + 'success' => true, + 'message' => 'データベース接続正常' + ]; + + } catch (\Exception $e) { + Log::error('データベース接続エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => $e->getMessage() + ]; + } + } + + /** + * 【処理2】デバイス管理マスタを取得する + * + * @return array デバイス情報 + */ + private function getDeviceManagementData(): array + { + try { + $devices = DB::table('device') + ->select([ + 'device_id', + 'park_id', + 'device_type', + 'device_subject', + 'device_identifier', + 'device_work', + 'device_workstart', + 'device_replace', + 'device_remarks', + 'operator_id' + ]) + ->where('device_workstart', '<=', now()) + ->whereNull('device_replace') + ->get() + ->toArray(); + + Log::info('デバイス管理マスタ取得完了', [ + 'device_count' => count($devices) + ]); + + return $devices; + + } catch (\Exception $e) { + Log::error('デバイス管理マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【処理3】デバイス毎のハードウェア状態を取得する + * + * @param array $devices デバイス一覧 + * @return array ハードウェア状態結果 + */ + private function getHardwareStatus(array $devices): array + { + try { + $normalDevices = 0; + $abnormalDevices = 0; + $abnormalDetails = []; + + foreach ($devices as $device) { + $latestStatus = HardwareCheckLog::getLatestStatusByDevice($device->device_id); + + if (!$latestStatus) { + $abnormalDevices++; + $abnormalDetails[] = "デバイスID {$device->device_id}: ハードウェア状態ログが存在しません"; + continue; + } + + if ($latestStatus->isNormal()) { + $normalDevices++; + } else { + $abnormalDevices++; + $abnormalDetails[] = "デバイスID {$device->device_id}: {$latestStatus->getStatusNameAttribute()} - {$latestStatus->status_comment}"; + } + } + + Log::info('ハードウェア状態取得完了', [ + 'total_devices' => count($devices), + 'normal_devices' => $normalDevices, + 'abnormal_devices' => $abnormalDevices + ]); + + return [ + 'success' => $abnormalDevices === 0, + 'total_devices' => count($devices), + 'normal_devices' => $normalDevices, + 'abnormal_devices' => $abnormalDevices, + 'abnormal_details' => $abnormalDetails, + 'message' => $abnormalDevices > 0 ? + "{$abnormalDevices}台のデバイスで異常を検出" : + '全デバイス正常' + ]; + + } catch (\Exception $e) { + Log::error('ハードウェア状態取得エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => $e->getMessage(), + 'abnormal_details' => ["ハードウェア状態取得中にエラーが発生: " . $e->getMessage()] + ]; + } + } + + /** + * 【処理4】プリンタ制御プログラムログを取得する + * + * @return array プリンタログ結果 + */ + private function getPrinterControlLogs(): array + { + try { + // 過去15分間のエラーログを取得 + $errorLogs = PrintJobLog::getRecentErrorLogs(); + + $hasErrors = $errorLogs->count() > 0; + $errorSummary = ''; + $errorDetails = []; + + if ($hasErrors) { + $errorsByCode = $errorLogs->groupBy('error_code'); + $errorSummaryParts = []; + + foreach ($errorsByCode as $errorCode => $logs) { + $count = $logs->count(); + $errorSummaryParts[] = "エラーコード{$errorCode}: {$count}件"; + + foreach ($logs as $log) { + $errorDetails[] = sprintf( + "[%s] %s - エラーコード: %d, %s", + $log->created_at->format('Y-m-d H:i:s'), + $log->process_name, + $log->error_code, + $log->status_comment + ); + } + } + + $errorSummary = implode(', ', $errorSummaryParts); + } + + Log::info('プリンタ制御プログラムログ取得完了', [ + 'monitoring_period_minutes' => self::PRINTER_LOG_MONITOR_MINUTES, + 'error_logs_count' => $errorLogs->count(), + 'has_errors' => $hasErrors + ]); + + return [ + 'success' => true, + 'has_errors' => $hasErrors, + 'error_count' => $errorLogs->count(), + 'error_summary' => $errorSummary, + 'error_details' => $errorDetails, + 'monitoring_period' => self::PRINTER_LOG_MONITOR_MINUTES . '分間' + ]; + + } catch (\Exception $e) { + Log::error('プリンタ制御プログラムログ取得エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'has_errors' => true, + 'error_summary' => 'ログ取得エラー: ' . $e->getMessage(), + 'error_details' => ["プリンタログ取得中にエラーが発生: " . $e->getMessage()] + ]; + } + } + + /** + * 監視結果サマリーを作成 + * + * @param array $devices デバイス一覧 + * @param array $hardwareResult ハードウェア結果 + * @param array $printerResult プリンタ結果 + * @return string 監視サマリー + */ + private function createMonitoringSummary(array $devices, array $hardwareResult, array $printerResult): string + { + $summary = [ + 'デバイス数: ' . count($devices), + 'ハードウェア正常: ' . ($hardwareResult['normal_devices'] ?? 0) . '台', + 'ハードウェア異常: ' . ($hardwareResult['abnormal_devices'] ?? 0) . '台', + 'プリンタエラー: ' . ($printerResult['error_count'] ?? 0) . '件' + ]; + + return implode(', ', $summary); + } + + /** + * 共通A処理:監視結果を反映 + * + * @param int|null $batchLogId バッチログID + * @param string $alertMessage アラートメッセージ + * @return void + */ + private function executeCommonProcessA(?int $batchLogId, string $alertMessage): void + { + try { + Log::info('共通A処理開始', [ + 'batch_log_id' => $batchLogId, + 'alert_message' => $alertMessage + ]); + + // 【共通判断1】DB反映可否判定 + $canReflectToDb = $this->canReflectToDatabase(); + + if ($canReflectToDb) { + // 【共通処理1】オペレータキューを登録する + $this->registerOperatorQueue($alertMessage, $batchLogId); + + // 【共通処理2】メール送信対象オペレータを取得する + $operators = $this->getMailTargetOperators(); + + // 【共通判断2】送信対象有無 + if (!empty($operators)) { + foreach ($operators as $operator) { + $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); + } + } + + // 【共通処理3】駐輪場管理者を取得する + $parkManagers = $this->getParkManagers(); + + // 【共通判断3】送信対象有無 + if (!empty($parkManagers)) { + foreach ($parkManagers as $manager) { + $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); + } + } + + } else { + // DB反映NGの場合は固定メールアドレスに送信 + $this->sendAlertMail(self::FIXED_EMAIL_ADDRESS, $alertMessage, 'システム管理者'); + } + + Log::info('共通A処理完了', [ + 'batch_log_id' => $batchLogId + ]); + + } catch (\Exception $e) { + Log::error('共通A処理エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * DB反映可否判定 + * + * @return bool 反映可能かどうか + */ + private function canReflectToDatabase(): bool + { + try { + // 簡単なINSERTテストでDB反映可否を確認 + DB::beginTransaction(); + $testId = DB::table('operator_que')->insertGetId([ + 'que_class' => 6, + 'que_comment' => 'DB反映テスト', + 'que_status' => 0, + 'created_at' => now(), + 'updated_at' => now() + ]); + + // テストレコードを削除 + DB::table('operator_que')->where('que_id', $testId)->delete(); + DB::commit(); + + return true; + + } catch (\Exception $e) { + DB::rollBack(); + Log::warning('DB反映不可', ['error' => $e->getMessage()]); + return false; + } + } + + /** + * オペレータキューを登録 + * + * @param string $alertMessage アラートメッセージ + * @param int|null $batchLogId バッチログID + * @return void + */ + private function registerOperatorQueue(string $alertMessage, ?int $batchLogId): void + { + try { + OperatorQue::create([ + 'que_class' => 6, // SHJ-6用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $alertMessage, + 'que_status' => 0, // 待機中 + 'que_status_comment' => 'システム監視アラート', + 'work_instructions' => "SHJ-6監視処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー登録完了', [ + 'batch_log_id' => $batchLogId, + 'alert_message' => $alertMessage + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー登録エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * メール送信対象オペレータを取得 + * + * @return array オペレータ一覧 + */ + private function getMailTargetOperators(): array + { + try { + // user_typeがオペレータのユーザーを取得(仮の条件) + $operators = DB::table('users') + ->select(['user_id', 'email', 'name']) + ->where('user_type', 'operator') // 実際のテーブル構造に合わせて調整 + ->whereNotNull('email') + ->where('email', '!=', '') + ->get() + ->map(function ($user) { + return [ + 'user_id' => $user->user_id, + 'email' => $user->email, + 'name' => $user->name ?? 'オペレータ' + ]; + }) + ->toArray(); + + Log::info('メール送信対象オペレータ取得完了', [ + 'operator_count' => count($operators) + ]); + + return $operators; + + } catch (\Exception $e) { + Log::error('メール送信対象オペレータ取得エラー', [ + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * 駐輪場管理者を取得 + * + * @return array 駐輪場管理者一覧 + */ + private function getParkManagers(): array + { + try { + // 駐輪場管理者を取得(仮の条件) + $managers = DB::table('users') + ->select(['user_id', 'email', 'name']) + ->where('user_type', 'park_manager') // 実際のテーブル構造に合わせて調整 + ->whereNotNull('email') + ->where('email', '!=', '') + ->get() + ->map(function ($user) { + return [ + 'user_id' => $user->user_id, + 'email' => $user->email, + 'name' => $user->name ?? '駐輪場管理者' + ]; + }) + ->toArray(); + + Log::info('駐輪場管理者取得完了', [ + 'manager_count' => count($managers) + ]); + + return $managers; + + } catch (\Exception $e) { + Log::error('駐輪場管理者取得エラー', [ + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * アラートメールを送信 + * + * @param string $email メールアドレス + * @param string $alertMessage アラートメッセージ + * @param string $recipientType 受信者タイプ + * @return void + */ + private function sendAlertMail(string $email, string $alertMessage, string $recipientType): void + { + try { + // SHJメール送信機能を使用(メールテンプレートID=1を使用、実際の値に調整) + $result = $this->mailSendService->executeMailSend( + $email, + '', // 予備メールアドレスは空 + 1 // システムアラート用メールテンプレートID + ); + + if ($result['success']) { + Log::info('アラートメール送信成功', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'alert_message' => $alertMessage + ]); + } else { + Log::error('アラートメール送信失敗', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error' => $result['message'] + ]); + } + + } catch (\Exception $e) { + Log::error('アラートメール送信エラー', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error' => $e->getMessage() + ]); + } + } +} diff --git a/app/Services/ShjTenService.php b/app/Services/ShjTenService.php new file mode 100644 index 0000000..f1a59cd --- /dev/null +++ b/app/Services/ShjTenService.php @@ -0,0 +1,583 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->earningsSummaryModel = $earningsSummaryModel; + $this->psectionModel = $psectionModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + } + + /** + * SHJ-10 財政年度売上集計処理メイン実行 + * + * 処理フロー: + * 【処理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 + { + $batchLogId = null; + + try { + // 【処理1】集計対象を設定する(財政年度ベース) + $aggregationTarget = $this->setFiscalAggregationTarget($fiscalPeriod); + + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj10', + BatchLog::STATUS_START, + [ + 'type' => $type, + 'target' => $target, + 'fiscal_period' => $fiscalPeriod, + 'aggregation_target' => $aggregationTarget + ], + "SHJ-10 売上集計処理開始 ({$type}: {$fiscalPeriod['target_label']})" + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-10 売上集計処理開始', [ + 'batch_log_id' => $batchLogId, + 'type' => $type, + 'target' => $target, + 'fiscal_period' => $fiscalPeriod, + 'aggregation_target' => $aggregationTarget + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $typeLabel = $this->getTypeLabel($type); + $message = "売上集計({$typeLabel}):駐輪場マスタが存在していません。"; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 // 処理は成功したが対象なし + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => 0, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $parkSummaryRecords = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod); + + if ($parkSummaryRecords > 0) { + $processedParks++; + $summaryRecords += $parkSummaryRecords; + } + } + + // 【判断2】取得判定 + if ($summaryRecords === 0) { + $message = '対象なしの結果を設定する(契約)'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => $processedParks, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // バッチ処理完了ログ更新 + $completionMessage = "SHJ-10 売上集計処理正常完了 ({$type}: {$fiscalPeriod['target_label']}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => $completionMessage, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($completionMessage, $batchLogId); + + Log::info('SHJ-10 売上集計処理完了', [ + 'batch_log_id' => $batchLogId, + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-10 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-10 売上集計処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-10 売上集計処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理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'], + 'summary_type' => $fiscalPeriod['summary_type'], + 'fiscal_year' => $fiscalPeriod['fiscal_year'], + 'target_label' => $fiscalPeriod['target_label'] + ]; + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * 仕様書のSQLクエリに基づく駐輪場情報取得 + * SELECT 駐輪場ID, 駐輪場名 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ <> 1 + * ORDER BY 駐輪場ふりがな + * + * @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 int 作成された集計レコード数 + */ + private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): int + { + try { + // 【処理4】既存の売上集計結果を削除 + $this->deleteExistingFiscalSummary($park->park_id, $aggregationTarget); + + // 【処理3】車種区分毎に算出する + $psections = $this->getPsectionInformation(); + $summaryRecords = 0; + + foreach ($psections as $psection) { + $earningsData = $this->calculateFiscalEarningsForPsection( + $park->park_id, + $psection->psection_id, + $aggregationTarget + ); + + if ($this->hasEarningsData($earningsData)) { + // 売上集計結果を登録 + $this->createFiscalEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $fiscalPeriod); + $summaryRecords++; + } + } + + Log::info('駐輪場財政年度売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords, + 'fiscal_period' => $fiscalPeriod['target_label'] + ]); + + return $summaryRecords; + + } catch (\Exception $e) { + Log::error('駐輪場財政年度売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 車種区分情報取得 + * + * @return array 車種区分情報 + */ + private function getPsectionInformation(): array + { + return DB::table('psection') + ->select(['psection_id', 'psection_subject']) + ->get() + ->toArray(); + } + + /** + * 【処理3】車種区分毎に財政年度売上を算出する + * + * 4つの項目を計算: + * ①売上・件数 + * ②一時金売上 + * ③解約返戻金 + * ④再発行金額・件数 + * + * @param int $parkId 駐輪場ID + * @param int $psectionId 車種区分ID + * @param array $aggregationTarget 集計対象 + * @return array 売上データ + */ + private function calculateFiscalEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // ①売上・件数(billing_amount) + $salesData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as sales_count'), + DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNull('contract_cancelday') + ->first(); + + // ②一時金売上(contract_money) + $temporaryData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as temporary_count'), + DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNotNull('contract_money') + ->where('contract_money', '>', 0) + ->first(); + + // ③解約返戻金(refunds) + $refundData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as refund_count'), + DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->whereBetween('contract_cancelday', [$startDate, $endDate]) + ->whereNotNull('refunds') + ->where('refunds', '>', 0) + ->first(); + + // ④再発行金額・件数(seal_reissue_request) + $reissueData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as reissue_count'), + DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('seal_reissue_request', 1) + ->whereBetween('updated_at', [$startDate, $endDate]) + ->first(); + + return [ + 'sales_count' => $salesData->sales_count ?? 0, + 'sales_amount' => $salesData->sales_amount ?? 0, + 'temporary_count' => $temporaryData->temporary_count ?? 0, + 'temporary_amount' => $temporaryData->temporary_amount ?? 0, + 'refund_count' => $refundData->refund_count ?? 0, + 'refund_amount' => $refundData->refund_amount ?? 0, + 'reissue_count' => $reissueData->reissue_count ?? 0, + 'reissue_amount' => $reissueData->reissue_amount ?? 0 + ]; + } + + /** + * 売上データの存在チェック + * + * @param array $earningsData 売上データ + * @return bool データが存在するかどうか + */ + private function hasEarningsData(array $earningsData): bool + { + return $earningsData['sales_count'] > 0 || + $earningsData['temporary_count'] > 0 || + $earningsData['refund_count'] > 0 || + $earningsData['reissue_count'] > 0; + } + + /** + * 【処理4】既存の財政年度売上集計結果を削除 + * + * @param int $parkId 駐輪場ID + * @param array $aggregationTarget 集計対象 + * @return void + */ + private function deleteExistingFiscalSummary(int $parkId, array $aggregationTarget): void + { + DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_start_date', $aggregationTarget['start_date']) + ->where('summary_end_date', $aggregationTarget['end_date']) + ->where('summary_type', $aggregationTarget['summary_type']) + ->delete(); + } + + /** + * 財政年度売上集計結果を登録 + * + * @param object $park 駐輪場情報 + * @param object $psection 車種区分情報 + * @param array $aggregationTarget 集計対象 + * @param array $earningsData 売上データ + * @param array $fiscalPeriod 財政期間情報 + * @return void + */ + private function createFiscalEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, array $fiscalPeriod): void + { + DB::table('earnings_summary')->insert([ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], // 1:年次, 2:月次 + 'summary_start_date' => $aggregationTarget['start_date'], + 'summary_end_date' => $aggregationTarget['end_date'], + 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 + 'psection_id' => $psection->psection_id, + 'usertype_subject1' => $psection->psection_subject, + 'regular_new_count' => $earningsData['sales_count'], + 'regular_new_amount' => $earningsData['sales_amount'], + 'turnsum' => $earningsData['temporary_amount'], + 'turnsum_count' => $earningsData['temporary_count'], + 'refunds' => $earningsData['refund_amount'], + 'reissue_count' => $earningsData['reissue_count'], + 'reissue_amount' => $earningsData['reissue_amount'], + 'summary_note' => "SHJ-10 {$fiscalPeriod['target_label']} 財政年度売上集計結果", + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => 0 // システム処理 + ]); + } + + /** + * 【処理5】オペレータキュー作成 + * + * @param string $message メッセージ + * @param int $batchLogId バッチログID + * @return void + */ + private function createOperatorQueue(string $message, int $batchLogId): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 10, // SHJ-10用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $message, + 'que_status' => 1, // 完了 + 'que_status_comment' => 'バッチ処理完了', + 'work_instructions' => "SHJ-10売上集計処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'batch_log_id' => $batchLogId, + 'message' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 集計種別のラベル取得 + * + * @param string $type 集計種別 + * @return string ラベル + */ + private function getTypeLabel(string $type): string + { + switch ($type) { + case 'yearly': + return '年次'; + case 'monthly': + return '月次'; + default: + return $type; + } + } +} diff --git a/app/Services/ShjThreeService.php b/app/Services/ShjThreeService.php new file mode 100644 index 0000000..53cb893 --- /dev/null +++ b/app/Services/ShjThreeService.php @@ -0,0 +1,796 @@ +parkModel = $parkModel; + $this->userModel = $userModel; + $this->contractModel = $contractModel; + $this->batchLogModel = $batchLogModel; + $this->mailSendService = $mailSendService; + } + + /** + * SHJ-3 定期更新リマインダー処理メイン実行 + * + * 処理フロー: + * 【処理0】駐輪場マスタの情報を取得する + * 【判断0】当該駐輪場実行タイミングチェック + * 【処理2】定期更新対象者を取得する + * 【判断2】利用者有無をチェック + * 【処理3】対象者向けにメール送信、またはオペレーターキュー追加処理 + * 【処理4】バッチ処理ログを作成する + * + * @return array 処理結果 + */ + public function executeReminderProcess(): array + { + $batchLogId = null; + $processedParksCount = 0; + $totalTargetUsers = 0; + $mailSuccessCount = 0; + $mailErrorCount = 0; + $operatorQueueCount = 0; + + try { + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj3', + BatchLog::STATUS_START, + [], + 'SHJ-3 定期更新リマインダー処理開始' + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-3 定期更新リマインダー処理開始', [ + 'batch_log_id' => $batchLogId + ]); + + // 【処理0】駐輪場マスタの情報を取得する + $parkList = $this->getParkMasterInfo(); + + if (empty($parkList)) { + $message = '対象の駐輪場マスタが見つかりません'; + + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $message, + 'error_details' => $message, + 'error_count' => 1 + ]); + + 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, + 'batch_log_id' => $batchLogId + ]; + } + + // 各駐輪場に対する処理ループ + foreach ($parkList as $park) { + 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; + } + + $processedParksCount++; + + // 【処理2】定期更新対象者を取得する + $targetUsers = $this->getRegularUpdateTargetUsers($park->park_id); + + // 【判断2】利用者有無をチェック + if (empty($targetUsers)) { + Log::info('利用者なし', [ + 'park_id' => $park->park_id + ]); + continue; + } + + $totalTargetUsers += count($targetUsers); + + // 【処理3】対象者向けにメール送信、またはオペレーターキュー追加処理 + foreach ($targetUsers as $targetUser) { + $processResult = $this->processTargetUser($targetUser); + + if ($processResult['type'] === 'mail_success') { + $mailSuccessCount++; + } elseif ($processResult['type'] === 'mail_error') { + $mailErrorCount++; + } elseif ($processResult['type'] === 'operator_queue') { + $operatorQueueCount++; + } + } + + Log::info('駐輪場処理完了', [ + 'park_id' => $park->park_id, + 'target_users_count' => count($targetUsers) + ]); + } + + // 【処理4】バッチ処理ログを作成する(SHJ-8呼び出し) + $this->createShjBatchLog([ + 'processed_parks_count' => $processedParksCount, + 'total_target_users' => $totalTargetUsers, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'operator_queue_count' => $operatorQueueCount + ]); + + // バッチ処理完了ログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-3 定期更新リマインダー処理正常完了', + 'success_count' => 1, + 'parameters' => [ + 'processed_parks_count' => $processedParksCount, + 'total_target_users' => $totalTargetUsers, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'operator_queue_count' => $operatorQueueCount, + 'executed_at' => now()->toISOString() + ] + ]); + + Log::info('SHJ-3 定期更新リマインダー処理完了', [ + 'batch_log_id' => $batchLogId, + 'processed_parks_count' => $processedParksCount, + 'total_target_users' => $totalTargetUsers + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-3 定期更新リマインダー処理が正常に完了しました', + 'processed_parks_count' => $processedParksCount, + 'total_target_users' => $totalTargetUsers, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'operator_queue_count' => $operatorQueueCount, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-3 定期更新リマインダー処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-3 定期更新リマインダー処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'processed_parks_count' => $processedParksCount, + 'total_target_users' => $totalTargetUsers, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'operator_queue_count' => $operatorQueueCount, + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理0】駐輪場マスタの情報を取得する + * + * 仕様書に基づくSQLクエリ: + * SELECT 駐輪場ID, 駐輪場名, 更新期間開始日, 更新期間開始時, + * 更新期間終了日, 更新期間終了時, リマインダー種別, リマインダー時間 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ = 0 + * ORDER BY 駐輪場ふりがな asc + * + * @return array 駐輪場マスタ情報 + */ + private function getParkMasterInfo(): array + { + try { + $parkInfo = DB::table('park') + ->select([ + 'park_id', + 'park_name', + 'renew_start_date', + 'renew_start_time', + 'renew_end_date', + 'renew_end_time', + 'reminder_type', + 'reminder_time' + ]) + ->where('park_close_flag', 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】当該駐輪場実行タイミングチェック + * + * 仕様書に基づく複雑なリマインダー時期判定: + * - パターンA: 月を跨らない場合(更新期間開始日 <= 更新期間終了日) + * - パターンB: 月を跨る場合(更新期間開始日 > 更新期間終了日) + * - リマインダー種別による実行判定 + * + * @param object $park 駐輪場情報 + * @return array 実行タイミング判定結果 + */ + private function checkExecutionTiming($park): array + { + try { + $today = Carbon::now(); + $currentDate = $today->format('Y-m-d'); + + // 更新期間の日付を取得 + $startDate = Carbon::parse($park->renew_start_date); + $endDate = Carbon::parse($park->renew_end_date); + + Log::info('実行タイミングチェック開始', [ + 'park_id' => $park->park_id, + 'current_date' => $currentDate, + 'start_date' => $startDate->format('Y-m-d'), + 'end_date' => $endDate->format('Y-m-d'), + 'reminder_type' => $park->reminder_type + ]); + + // パターン判定: 月を跨るかどうか + $isPatternA = $startDate->lte($endDate); // パターンA: 月を跨らない + $isPatternB = !$isPatternA; // パターンB: 月を跨る + + // 現在日付が更新期間内かチェック + $isWithinUpdatePeriod = false; + + if ($isPatternA) { + // パターンA: 更新期間開始日 <= 現在日 <= 更新期間終了日 + $isWithinUpdatePeriod = $today->between($startDate, $endDate); + } else { + // パターンB: 月を跨る場合の判定 + // 現在日 >= 更新期間開始日 OR 現在日 <= 更新期間終了日 + $isWithinUpdatePeriod = $today->gte($startDate) || $today->lte($endDate); + } + + if (!$isWithinUpdatePeriod) { + return [ + 'should_execute' => false, + 'reason' => '更新期間外のため実行対象外', + 'pattern' => $isPatternA ? 'A' : 'B' + ]; + } + + // リマインダー種別による実行判定 + $reminderExecutionResult = $this->checkReminderTiming($park, $today, $startDate, $endDate, $isPatternA); + + Log::info('実行タイミングチェック完了', [ + 'park_id' => $park->park_id, + 'should_execute' => $reminderExecutionResult['should_execute'], + 'pattern' => $isPatternA ? 'A' : 'B', + 'reminder_result' => $reminderExecutionResult + ]); + + return $reminderExecutionResult; + + } catch (\Exception $e) { + Log::error('実行タイミングチェックエラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * リマインダー時期の詳細判定 + * + * 仕様書の複雑なリマインダー種別判定を実装 + * パターンA/Bに基づく詳細な日付計算ロジック + * + * @param object $park 駐輪場情報 + * @param Carbon $today 現在日 + * @param Carbon $startDate 更新期間開始日 + * @param Carbon $endDate 更新期間終了日 + * @param bool $isPatternA パターンAかどうか + * @return array リマインダー実行判定結果 + */ + private function checkReminderTiming($park, Carbon $today, Carbon $startDate, Carbon $endDate, bool $isPatternA): array + { + $reminderType = $park->reminder_type ?? 0; + + // リマインダー種別 = 0 の場合は実行しない + if ($reminderType == 0) { + return [ + 'should_execute' => false, + 'reason' => 'リマインダー種別=0のため実行対象外' + ]; + } + + // 仕様書に基づく詳細なリマインダー時期判定 + $executionCheck = $this->performDetailedReminderCheck($park, $today, $startDate, $endDate, $isPatternA); + + return [ + 'should_execute' => $executionCheck['should_execute'], + 'reason' => $executionCheck['reason'], + 'reminder_type' => $reminderType, + 'pattern' => $isPatternA ? 'A' : 'B', + 'execution_details' => $executionCheck + ]; + } + + /** + * 仕様書に基づく詳細なリマインダー実行判定 + * + * 複雑な条件分岐を含む実行フラグ判定処理 + * - パターンA/Bによる分岐 + * - リマインダー種別による日数計算 + * - 月跨ぎ処理の考慮 + * + * @param object $park 駐輪場情報 + * @param Carbon $today 現在日 + * @param Carbon $startDate 更新期間開始日 + * @param Carbon $endDate 更新期間終了日 + * @param bool $isPatternA パターンAかどうか + * @return array 実行判定詳細結果 + */ + private function performDetailedReminderCheck($park, Carbon $today, Carbon $startDate, Carbon $endDate, bool $isPatternA): array + { + $reminderType = $park->reminder_type ?? 0; + + // 内部変数 更新パターン判定 + $updatePattern = ''; + + if ($isPatternA) { + // パターンA: 月を跨らない場合 + if ($startDate->lte($endDate)) { + $updatePattern = 'A'; + } else { + $updatePattern = 'B'; // 実際はパターンBになる + } + } else { + // パターンB: 月を跨る場合 + $updatePattern = 'B'; + } + + // 実行フラグ判定処理 + $executionFlag = $this->calculateExecutionFlag($today, $startDate, $endDate, $reminderType, $updatePattern); + + Log::info('詳細リマインダー判定完了', [ + 'park_id' => $park->park_id, + 'reminder_type' => $reminderType, + 'update_pattern' => $updatePattern, + 'execution_flag' => $executionFlag, + 'today' => $today->format('Y-m-d'), + 'start_date' => $startDate->format('Y-m-d'), + 'end_date' => $endDate->format('Y-m-d') + ]); + + return [ + 'should_execute' => $executionFlag['should_execute'], + 'reason' => $executionFlag['reason'], + 'update_pattern' => $updatePattern, + 'execution_details' => $executionFlag + ]; + } + + /** + * 実行フラグ計算処理 + * + * 仕様書の複雑な分岐条件に基づく実行判定 + * + * @param Carbon $today 現在日 + * @param Carbon $startDate 更新期間開始日 + * @param Carbon $endDate 更新期間終了日 + * @param int $reminderType リマインダー種別 + * @param string $updatePattern 更新パターン(A/B) + * @return array 実行フラグ判定結果 + */ + private function calculateExecutionFlag(Carbon $today, Carbon $startDate, Carbon $endDate, int $reminderType, string $updatePattern): array + { + // リマインダー種別による実行判定 + switch ($reminderType) { + case 1: // -1日前 + return $this->checkReminderType1($today, $startDate, $endDate, $updatePattern); + case 2: // -2日前 + return $this->checkReminderType2($today, $startDate, $endDate, $updatePattern); + default: + return [ + 'should_execute' => false, + 'reason' => "未対応のリマインダー種別: {$reminderType}" + ]; + } + } + + /** + * リマインダー種別=1(-1日前)の判定 + * + * @param Carbon $today 現在日 + * @param Carbon $startDate 更新期間開始日 + * @param Carbon $endDate 更新期間終了日 + * @param string $updatePattern 更新パターン + * @return array 判定結果 + */ + private function checkReminderType1(Carbon $today, Carbon $startDate, Carbon $endDate, string $updatePattern): array + { + // 更新期間終了日の1日前が実行日 + $executionDate = $endDate->copy()->subDay(); + + if ($today->isSameDay($executionDate)) { + return [ + 'should_execute' => true, + 'reason' => '-1日前のリマインダー実行日', + 'execution_date' => $executionDate->format('Y-m-d') + ]; + } + + return [ + 'should_execute' => false, + 'reason' => "リマインダー実行日({$executionDate->format('Y-m-d')})ではない(現在: {$today->format('Y-m-d')})", + 'execution_date' => $executionDate->format('Y-m-d') + ]; + } + + /** + * リマインダー種別=2(-2日前)の判定 + * + * @param Carbon $today 現在日 + * @param Carbon $startDate 更新期間開始日 + * @param Carbon $endDate 更新期間終了日 + * @param string $updatePattern 更新パターン + * @return array 判定結果 + */ + private function checkReminderType2(Carbon $today, Carbon $startDate, Carbon $endDate, string $updatePattern): array + { + // 更新期間終了日の2日前が実行日 + $executionDate = $endDate->copy()->subDays(2); + + if ($today->isSameDay($executionDate)) { + return [ + 'should_execute' => true, + 'reason' => '-2日前のリマインダー実行日', + 'execution_date' => $executionDate->format('Y-m-d') + ]; + } + + return [ + 'should_execute' => false, + 'reason' => "リマインダー実行日({$executionDate->format('Y-m-d')})ではない(現在: {$today->format('Y-m-d')})", + 'execution_date' => $executionDate->format('Y-m-d') + ]; + } + + /** + * 【処理2】定期更新対象者を取得する + * + * 仕様書に基づく複雑なSQLクエリ: + * 定期契約マスタ T1 と 利用者マスタ T2 を結合して + * 更新対象者の情報を取得する + * + * @param int $parkId 駐輪場ID + * @return array 定期更新対象者情報 + */ + private function getRegularUpdateTargetUsers(int $parkId): array + { + try { + $currentDate = Carbon::now()->format('Y-m-d'); + + // 仕様書に記載されたSQLクエリに基づく対象者取得 + $targetUsers = DB::table('regular_contract as T1') + ->select([ + 'T1.contract_id as 定期契約内ID', + 'T1.park_id as 駐輪場ID', + 'T2.user_id as 利用者ID', + '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_id') + ->where('T1.park_id', $parkId) + ->where('T1.contract_periode', '<=', $currentDate) // 更新可能日チェック + ->where('T1.contract_cancel_flag', 0) // 解約フラグ = 0 + ->where('T2.user_quit_flag', 0) // 退会フラグ = 0 + ->where('T1.contract_flag', 1) // 承認フラグ = 1 + ->whereNull('T1.contract_permission') // 更新済フラグ is null + ->get() + ->toArray(); + + Log::info('定期更新対象者取得完了', [ + 'park_id' => $parkId, + 'target_users_count' => count($targetUsers) + ]); + + return $targetUsers; + + } catch (\Exception $e) { + Log::error('定期更新対象者取得エラー', [ + 'park_id' => $parkId, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【処理3】対象者の処理実行 + * + * 手動登録フラグによって処理を分岐: + * - = 0 (ウェブ申込み): SHJ-7メール送信を呼び出し + * - その他: 内部変数のカウントアップ(オペレーターキュー処理) + * + * @param object $targetUser 対象者情報 + * @return array 処理結果 + */ + private function processTargetUser($targetUser): array + { + try { + $manualRegistFlag = $targetUser->手動登録フラグ ?? 1; + + if ($manualRegistFlag == 0) { + // ウェブ申込み: SHJ-7メール送信処理 + return $this->sendReminderMail($targetUser); + } else { + // その他: オペレーターキュー追加処理(内部変数カウントアップ) + Log::info('オペレーターキュー対象者', [ + 'user_id' => $targetUser->利用者ID, + 'contract_id' => $targetUser->定期契約内ID, + 'manual_regist_flag' => $manualRegistFlag + ]); + + return [ + 'type' => 'operator_queue', + 'success' => true, + 'message' => 'オペレーターキュー対象として処理' + ]; + } + + } catch (\Exception $e) { + Log::error('対象者処理エラー', [ + 'user_id' => $targetUser->利用者ID ?? 'unknown', + 'error' => $e->getMessage() + ]); + + 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 + + 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 + ); + + if ($mailResult['success']) { + return [ + 'type' => 'mail_success', + 'success' => true, + 'message' => 'メール送信成功', + 'mail_result' => $mailResult + ]; + } else { + return [ + 'type' => 'mail_error', + 'success' => false, + 'message' => 'メール送信失敗: ' . $mailResult['message'], + 'mail_result' => $mailResult + ]; + } + + } catch (\Exception $e) { + Log::error('リマインダーメール送信エラー', [ + 'user_id' => $targetUser->利用者ID ?? 'unknown', + 'error' => $e->getMessage() + ]); + + return [ + 'type' => 'mail_error', + 'success' => false, + 'message' => 'メール送信エラー: ' . $e->getMessage() + ]; + } + } + + /** + * 【処理4】SHJ-8バッチ処理ログ作成 + * + * 仕様書に基づくSHJ-8共通処理呼び出し + * + * @param array $statistics 処理統計情報 + * @return void + */ + private function createShjBatchLog(array $statistics): void + { + try { + // 仕様書に基づくSHJ-8パラメータ設定 + $deviceId = 9999; // テスト用デバイスID(規格書では"-"だが、既存実装に合わせて9999使用) + $processName = 'SHJ-3定期更新リマインダー'; + $jobName = 'success'; + $status = 'success'; + + // ステータスコメント生成 + $statusComment = "メール正常終了件数: {$statistics['mail_success_count']}" . + " + メール異常終了件数: {$statistics['mail_error_count']}" . + " + キュー登録正常終了件数: {$statistics['operator_queue_count']}"; + + $createdDate = now()->format('Y/m/d'); + $updatedDate = now()->format('Y/m/d'); + + Log::info('SHJ-8バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ]); + + // 共通処理SHJ-8バッチ処理ログ作成を呼び出し + // 注意: 実際の運用では外部コマンド呼び出しまたは専用サービス経由で実行 + BatchLog::createBatchLog( + $processName, + $status, + [ + 'device_id' => $deviceId, + 'job_name' => $jobName, + 'status_comment' => $statusComment, + 'statistics' => $statistics, + 'shj8_params' => [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ] + ], + $statusComment + ); + + } catch (\Exception $e) { + Log::error('SHJ-8バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'statistics' => $statistics + ]); + + // SHJ-8でエラーが発生してもメイン処理は継続 + // エラーログのみ出力 + } + } +} diff --git a/app/Services/ShjTwelveService.php b/app/Services/ShjTwelveService.php new file mode 100644 index 0000000..3380c4c --- /dev/null +++ b/app/Services/ShjTwelveService.php @@ -0,0 +1,335 @@ +shjMailSendService = $shjMailSendService; + } + + /** + * 【処理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_flag', // 手動登録フラグ + 'T2.user_mail', // メールアドレス + 'T2.user_mail_sub', // 予備メールアドレス + 'T2.park_id', // 駐輪場ID (userテーブルから) + 'T3.park_name', // 駐輪場名 + 'T1.billing_amount' // 請求金額 + ]) + ->join('user as T2', function($join) { + $join->on('T1.user_seq', '=', '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】未払い者への通知、またはオペレーターキュー追加処理 + * + * 各未払い者に対して以下の処理を実行: + * 1. メール通知の実行 (SHJ-7連携) + * 2. オペレーターキューへの追加 (opeテーブル) + * + * @param array $unpaidUsers 未払い者リスト + * @return array 処理結果 + */ + public function processUnpaidUserNotifications(array $unpaidUsers): array + { + $notificationCount = 0; + $queueCount = 0; + $errors = []; + $processParameters = []; + + try { + DB::beginTransaction(); + + foreach ($unpaidUsers as $user) { + try { + // メール通知処理 + $mailResult = $this->sendNotificationMail($user); + if ($mailResult['success']) { + $notificationCount++; + } + + // オペレーターキュー追加処理 + $queueResult = $this->addToOperatorQueue($user); + if ($queueResult['success']) { + $queueCount++; + } + + // 処理パラメータ記録 + $processParameters[] = [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'billing_amount' => $user->billing_amount, + 'mail_sent' => $mailResult['success'], + 'queue_added' => $queueResult['success'] + ]; + + } catch (\Exception $e) { + $errors[] = [ + 'contract_id' => $user->contract_id, + 'error' => $e->getMessage() + ]; + + Log::warning('SHJ-12 個別処理エラー', [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'error' => $e->getMessage() + ]); + } + } + + DB::commit(); + + return [ + 'success' => true, + 'notification_count' => $notificationCount, + 'queue_count' => $queueCount, + 'parameters' => $processParameters, + 'errors' => $errors, + 'message' => '未払い者通知処理完了' + ]; + + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('SHJ-12 通知処理全体エラー', [ + 'error' => $e->getMessage(), + 'processed_count' => count($processParameters) + ]); + + return [ + 'success' => false, + 'notification_count' => $notificationCount, + 'queue_count' => $queueCount, + 'parameters' => $processParameters, + 'errors' => $errors, + 'message' => '通知処理エラー: ' . $e->getMessage(), + 'details' => $e->getTraceAsString() + ]; + } + } + + /** + * 未払い者へのメール通知送信 + * + * SHJ-7 メール送信サービスを使用してメール通知を実行 + * + * @param object $user 未払い者情報 + * @return array 送信結果 + */ + private function sendNotificationMail($user): array + { + try { + // メールアドレスの確認 + $emailAddress = $user->user_mail ?: $user->user_mail_sub; + + if (empty($emailAddress)) { + return [ + 'success' => false, + 'message' => 'メールアドレスが設定されていません' + ]; + } + + // SHJ-7 メール送信サービス呼び出し + $mailParams = [ + 'to_email' => $emailAddress, + 'user_name' => $user->user_name, + 'park_name' => $user->park_name, + 'billing_amount' => $user->billing_amount, + 'contract_id' => $user->contract_id + ]; + + $result = $this->shjMailSendService->sendUnpaidNotificationMail($mailParams); + + Log::info('SHJ-12 メール送信結果', [ + 'contract_id' => $user->contract_id, + 'email' => $emailAddress, + 'success' => $result['success'] + ]); + + return $result; + + } catch (\Exception $e) { + Log::error('SHJ-12 メール送信エラー', [ + 'contract_id' => $user->contract_id, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => 'メール送信エラー: ' . $e->getMessage() + ]; + } + } + + /** + * オペレーターキューへの追加 + * + * opeテーブルにオペレーター処理キューとして登録 + * + * @param object $user 未払い者情報 + * @return array 追加結果 + */ + private function addToOperatorQueue($user): array + { + try { + $queueData = [ + 'ope_device_id' => null, // デバイスID (未設定) + 'ope_process_name' => 'SHJ-12', // プロセス名 + 'ope_job_name' => '未払い者通知', // ジョブ名 + 'ope_status' => 'pending', // ステータス + 'ope_comment' => sprintf( + '契約ID:%s ユーザー:%s 金額:%s円', + $user->contract_id, + $user->user_name, + number_format($user->billing_amount) + ), + 'ope_target_user_id' => $user->user_seq, // 対象ユーザーID + 'ope_target_contract_id' => $user->contract_id, // 対象契約ID + 'ope_billing_amount' => $user->billing_amount, // 請求金額 + 'created_at' => now(), + 'updated_at' => now() + ]; + + // opeテーブルに挿入 + DB::table('ope')->insert($queueData); + + Log::info('SHJ-12 オペレーターキュー追加完了', [ + 'contract_id' => $user->contract_id, + 'user_seq' => $user->user_seq, + 'billing_amount' => $user->billing_amount + ]); + + return [ + 'success' => true, + 'message' => 'オペレーターキューに追加しました' + ]; + + } catch (\Exception $e) { + Log::error('SHJ-12 オペレーターキュー追加エラー', [ + 'contract_id' => $user->contract_id, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => 'オペレーターキュー追加エラー: ' . $e->getMessage() + ]; + } + } + + /** + * 【処理3】バッチ処理ログを作成する + * + * 統一BatchLogシステムを使用してSHJ-12の実行ログを記録 + * + * @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 { + BatchLog::createBatchLog( + 'SHJ-12', + $status, + $parameters, + $message, + [ + 'execution_count' => $executionCount, + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'process_type' => '未払い者通知処理', + 'executed_at' => now()->toISOString() + ] + ); + + } catch (\Exception $e) { + Log::error('SHJ-12 バッチログ作成エラー', [ + 'error' => $e->getMessage(), + 'status' => $status, + 'message' => $message + ]); + } + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..3d4d62b 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\ShjServiceProvider::class, ]; diff --git a/config/filesystems.php b/config/filesystems.php index 3d671bd..1776b65 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -1,5 +1,7 @@ [ 'driver' => 'local', - 'root' => storage_path('app/private'), - 'serve' => true, + 'root' => storage_path('app'), 'throw' => false, - 'report' => false, ], 'public' => [ @@ -44,9 +44,9 @@ return [ 'url' => env('APP_URL').'/storage', 'visibility' => 'public', 'throw' => false, - 'report' => false, ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), @@ -57,7 +57,6 @@ return [ 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, - 'report' => false, ], ], @@ -77,4 +76,4 @@ return [ public_path('storage') => storage_path('app/public'), ], -]; +]; \ No newline at end of file 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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9f3091e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2434 @@ +{ + "name": "www", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.7.4", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^1.2.0", + "tailwindcss": "^4.0.0", + "vite": "^6.0.11" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", + "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", + "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", + "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", + "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", + "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", + "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", + "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", + "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", + "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", + "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", + "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", + "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", + "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", + "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", + "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", + "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", + "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", + "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", + "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", + "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", + "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", + "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.2", + "@rollup/rollup-android-arm64": "4.50.2", + "@rollup/rollup-darwin-arm64": "4.50.2", + "@rollup/rollup-darwin-x64": "4.50.2", + "@rollup/rollup-freebsd-arm64": "4.50.2", + "@rollup/rollup-freebsd-x64": "4.50.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", + "@rollup/rollup-linux-arm-musleabihf": "4.50.2", + "@rollup/rollup-linux-arm64-gnu": "4.50.2", + "@rollup/rollup-linux-arm64-musl": "4.50.2", + "@rollup/rollup-linux-loong64-gnu": "4.50.2", + "@rollup/rollup-linux-ppc64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-musl": "4.50.2", + "@rollup/rollup-linux-s390x-gnu": "4.50.2", + "@rollup/rollup-linux-x64-gnu": "4.50.2", + "@rollup/rollup-linux-x64-musl": "4.50.2", + "@rollup/rollup-openharmony-arm64": "4.50.2", + "@rollup/rollup-win32-arm64-msvc": "4.50.2", + "@rollup/rollup-win32-ia32-msvc": "4.50.2", + "@rollup/rollup-win32-x64-msvc": "4.50.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/vite": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/resources/views/regular_contract/upload_identity_fail.blade.php b/resources/views/regular_contract/upload_identity_fail.blade.php new file mode 100644 index 0000000..ab0b33b --- /dev/null +++ b/resources/views/regular_contract/upload_identity_fail.blade.php @@ -0,0 +1,156 @@ + + + + + + 本人確認処理 - 失敗 + + + +
+
+

❌ SHJ-1 本人確認自動処理 - 失敗

+

本人確認処理でエラーが発生しました。手動確認が必要です。

+
+ +
+ 処理日時: {{ $debugInfo['timestamp'] }} +
+ + @if(isset($debugInfo['error'])) +
+
❌ システムエラー
+
{{ $debugInfo['error'] }}
+
+ @else + +
+
👤 ユーザー情報
+
+ユーザーID: {{ $debugInfo['user']->user_id ?? 'N/A' }}
+ユーザー連番: {{ $debugInfo['user']->user_seq ?? 'N/A' }}
+氏名: {{ $debugInfo['user']->user_name ?? 'N/A' }}
+フリガナ: {{ $debugInfo['user']->user_phonetic ?? 'N/A' }}
+居住住所: {{ ($debugInfo['user']->user_regident_pre ?? '') . ($debugInfo['user']->user_regident_city ?? '') . ($debugInfo['user']->user_regident_add ?? '') }}
+関連住所: {{ ($debugInfo['user']->user_relate_pre ?? '') . ($debugInfo['user']->user_relate_city ?? '') . ($debugInfo['user']->user_relate_add ?? '') }}
+本人確認書類: {{ $debugInfo['user']->user_idcard ?? 'N/A' }}
+本人確認フラグ: {{ $debugInfo['user']->user_idcard_chk_flag ?? 'N/A' }}
+確認日時: {{ $debugInfo['user']->user_chk_day ?? 'N/A' }}
+確認オペレータID: {{ $debugInfo['user']->user_chk_opeid ?? 'N/A' }}
+写真ファイル1: {{ $debugInfo['user']->photo_filename1 ?? 'N/A' }}
+写真ファイル2: {{ $debugInfo['user']->photo_filename2 ?? 'N/A' }}
+                
+
+ + + @if($debugInfo['park']) +
+
🚲 駐輪場情報
+
+駐輪場ID: {{ $debugInfo['park']->park_id }}
+駐輪場名: {{ $debugInfo['park']->park_name }}
+住所: {{ $debugInfo['park']->park_adrs }}
+二点間距離設定: {{ $debugInfo['park']->distance_between_two_points ?? 'N/A' }}m
+緯度: {{ $debugInfo['park']->park_latitude ?? 'N/A' }}
+経度: {{ $debugInfo['park']->park_longitude ?? 'N/A' }}
+                
+
+ @endif + + + @if($debugInfo['contract']) +
+
📝 契約情報
+
+契約ID: {{ $contractId }}
+ユーザー連番: {{ $debugInfo['contract']->user_id }}
+駐輪場ID: {{ $debugInfo['contract']->park_id }}
+800mフラグ: {{ $debugInfo['contract']->{'800m_flag'} ?? 'N/A' }}
+作成日時: {{ $debugInfo['contract']->created_at ?? 'N/A' }}
+更新日時: {{ $debugInfo['contract']->updated_at ?? 'N/A' }}
+                
+
+ @endif + + + @if($debugInfo['batch_log']) +
+
⚙️ バッチ処理情報
+
+バッチ名: {{ $debugInfo['batch_log']->process_name }}
+ステータス: {{ $debugInfo['batch_log']->status }}
+開始時刻: {{ $debugInfo['batch_log']->start_time }}
+終了時刻: {{ $debugInfo['batch_log']->end_time }}
+処理件数: {{ $debugInfo['batch_log']->execution_count ?? 0 }}
+成功件数: {{ $debugInfo['batch_log']->success_count ?? 0 }}
+エラー件数: {{ $debugInfo['batch_log']->error_count ?? 0 }}
+メッセージ: {{ $debugInfo['batch_log']->message }}
+エラー詳細: {{ $debugInfo['batch_log']->error_details ?? 'N/A' }}
+                
+
+ @endif + + @if(isset($debugInfo['user']->user_idcard) && $debugInfo['user']->user_idcard === '免許証') + +
+
📋 SHJ-1処理詳細ログ (OCR・距離計算・エラー詳細)
+
{{ $debugInfo['detailed_logs'] }}
+
+ + +
+
🗺️ Google Maps API (距離計算) 処理結果
+
+【距離計算結果】
+起点住所: {{ $debugInfo['distance_start_address'] ?? (($debugInfo['user']->user_regident_pre ?? '') . ($debugInfo['user']->user_regident_city ?? '') . ($debugInfo['user']->user_regident_add ?? '')) }}
+終点住所: {{ $debugInfo['park']->park_adrs ?? ($debugInfo['distance_end_address'] ?? 'N/A') }}
+計算距離: {{ $debugInfo['calculated_distance'] ?? 'N/A' }}m @if(isset($debugInfo['distance_text']))({{ $debugInfo['distance_text'] }})@endif
+距離制限: {{ $debugInfo['distance_limit'] ?? config('shj1.distance.default_limit_meters') }}m
+判定結果: {{ isset($debugInfo['distance_passed']) ? ($debugInfo['distance_passed'] ? '✅ 制限内' : '❌ 制限超過') : 'N/A' }}
+
+【API呼び出し状況】
+API応答: {{ $debugInfo['maps_api_status'] ?? 'ログから確認してください' }}
+エラー詳細: {{ $debugInfo['maps_api_error'] ?? 'なし' }}
+                
+
+ @else + +
+
ℹ️ 処理状況
+
+本人確認書類種別: {{ $debugInfo['user']->user_idcard ?? 'N/A' }}
+
+{{ $debugInfo['user']->user_idcard ?? '不明' }}の場合、SHJ-1による自動OCR処理・距離計算は実行されません。
+
+
+処理ステータス: 手動確認待ち
+                
+
+ @endif + @endif + + +
+ + diff --git a/resources/views/regular_contract/upload_identity_success.blade.php b/resources/views/regular_contract/upload_identity_success.blade.php new file mode 100644 index 0000000..77d1628 --- /dev/null +++ b/resources/views/regular_contract/upload_identity_success.blade.php @@ -0,0 +1,136 @@ + + + + + + 本人確認処理 - 成功 + + + +
+
+

✅ SHJ-1 本人確認自動処理 - 成功

+

本人確認が正常に完了しました

+
+ +
+ 処理日時: {{ $debugInfo['timestamp'] }} +
+ + @if(isset($debugInfo['error'])) +
+
❌ エラー情報
+
{{ $debugInfo['error'] }}
+
+ @else + +
+
👤 ユーザー情報
+
+ユーザーID: {{ $debugInfo['user']->user_id ?? 'N/A' }}
+ユーザー連番: {{ $debugInfo['user']->user_seq ?? 'N/A' }}
+氏名: {{ $debugInfo['user']->user_name ?? 'N/A' }}
+フリガナ: {{ $debugInfo['user']->user_phonetic ?? 'N/A' }}
+居住住所: {{ ($debugInfo['user']->user_regident_pre ?? '') . ($debugInfo['user']->user_regident_city ?? '') . ($debugInfo['user']->user_regident_add ?? '') }}
+関連住所: {{ ($debugInfo['user']->user_relate_pre ?? '') . ($debugInfo['user']->user_relate_city ?? '') . ($debugInfo['user']->user_relate_add ?? '') }}
+本人確認書類: {{ $debugInfo['user']->user_idcard ?? 'N/A' }}
+本人確認フラグ: {{ $debugInfo['user']->user_idcard_chk_flag ?? 'N/A' }}
+確認日時: {{ $debugInfo['user']->user_chk_day ?? 'N/A' }}
+確認オペレータID: {{ $debugInfo['user']->user_chk_opeid ?? 'N/A' }}
+写真ファイル1: {{ $debugInfo['user']->photo_filename1 ?? 'N/A' }}
+写真ファイル2: {{ $debugInfo['user']->photo_filename2 ?? 'N/A' }}
+                
+
+ + + @if($debugInfo['park']) +
+
🚲 駐輪場情報
+
+駐輪場ID: {{ $debugInfo['park']->park_id }}
+駐輪場名: {{ $debugInfo['park']->park_name }}
+住所: {{ $debugInfo['park']->park_adrs }}
+二点間距離設定: {{ $debugInfo['park']->distance_between_two_points ?? 'N/A' }}m
+緯度: {{ $debugInfo['park']->park_latitude ?? 'N/A' }}
+経度: {{ $debugInfo['park']->park_longitude ?? 'N/A' }}
+                
+
+ @endif + + + @if($debugInfo['contract']) +
+
📝 契約情報
+
+契約ID: {{ $contractId }}
+ユーザー連番: {{ $debugInfo['contract']->user_id }}
+駐輪場ID: {{ $debugInfo['contract']->park_id }}
+800mフラグ: {{ $debugInfo['contract']->{'800m_flag'} ?? 'N/A' }}
+作成日時: {{ $debugInfo['contract']->created_at ?? 'N/A' }}
+更新日時: {{ $debugInfo['contract']->updated_at ?? 'N/A' }}
+                
+
+ @endif + + + @if($debugInfo['batch_log']) +
+
⚙️ バッチ処理情報
+
+バッチ名: {{ $debugInfo['batch_log']->process_name }}
+ステータス: {{ $debugInfo['batch_log']->status }}
+開始時刻: {{ $debugInfo['batch_log']->start_time }}
+終了時刻: {{ $debugInfo['batch_log']->end_time }}
+処理件数: {{ $debugInfo['batch_log']->execution_count ?? 0 }}
+成功件数: {{ $debugInfo['batch_log']->success_count ?? 0 }}
+エラー件数: {{ $debugInfo['batch_log']->error_count ?? 0 }}
+メッセージ: {{ $debugInfo['batch_log']->message }}
+                
+
+ @endif + + +
+
📋 SHJ-1処理詳細ログ (OCR・距離計算・判定結果)
+
{{ $debugInfo['detailed_logs'] }}
+
+ + + +
+
🗺️ Google Maps API (距離計算) 処理結果
+
+【距離計算結果】
+起点住所: {{ $debugInfo['distance_start_address'] ?? (($debugInfo['user']->user_regident_pre ?? '') . ($debugInfo['user']->user_regident_city ?? '') . ($debugInfo['user']->user_regident_add ?? '')) }}
+終点住所: {{ $debugInfo['park']->park_adrs ?? ($debugInfo['distance_end_address'] ?? 'N/A') }}
+計算距離: {{ $debugInfo['calculated_distance'] ?? 'N/A' }}m @if(isset($debugInfo['distance_text']))({{ $debugInfo['distance_text'] }})@endif
+距離制限: {{ $debugInfo['distance_limit'] ?? config('shj1.distance.default_limit_meters') }}m
+判定結果: {{ isset($debugInfo['distance_passed']) ? ($debugInfo['distance_passed'] ? '✅ 制限内' : '❌ 制限超過') : 'N/A' }}
+
+【API呼び出し状況】
+API応答: {{ $debugInfo['maps_api_status'] ?? 'ログから確認してください' }}
+エラー詳細: {{ $debugInfo['maps_api_error'] ?? 'なし' }}
+                
+
+ @endif + + +
+ + diff --git a/routes/web.php b/routes/web.php index 9b4a6a1..b6c3a59 100644 --- a/routes/web.php +++ b/routes/web.php @@ -130,6 +130,10 @@ Route::post('/regular_contract/input/check', [RegularContractCreateController::c Route::get('/regular-contract/upload_identity_create', [RegularContractCreateController::class, 'showUploadIdentityCreate'])->name('regular_contract.upload_identity_create'); Route::post('regular_contract/confirm_upload_identity/{contract_id}', [RegularContractCreateController::class, 'confirmUploadIdentity'])->name('regular_contract.confirm_upload_identity'); + +// SHJ-1バッチ処理結果表示ページ +Route::get('/regular-contract/upload_identity_success', [RegularContractCreateController::class, 'showUploadIdentitySuccess'])->name('regular_contract.upload_identity_success'); +Route::get('/regular-contract/upload_identity_fail', [RegularContractCreateController::class, 'showUploadIdentityFail'])->name('regular_contract.upload_identity_fail'); Route::get('regular_contract/create_confirm', [RegularContractCreateController::class, 'createConfirm'])->name('regular_contract.create_confirm'); Route::post('/regular_contract/create_confirm_next/{contract_id}', [RegularContractCreateController::class, 'createConfirmNext'])->name('regular_contract.create_confirm_next'); Route::post('regular_contract/create_select_period', [RegularContractCreateController::class, 'selectPeriod'])