From 41814dd9084f3b2c4ec999485aeffb9ab47e2cff Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Feb 2026 19:25:40 +0900 Subject: [PATCH] =?UTF-8?q?SHJ-4=20SHJ-5=20SHJ-6=20=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E7=82=B9=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- app/Console/Commands/ShjFourCCommand.php | 10 +- .../Api/PaymentCallbackController.php | 88 ++- app/Services/ShjFiveService.php | 503 +++++++------ app/Services/ShjFourBService.php | 689 ++++++++++++------ app/Services/ShjFourCService.php | 67 +- app/Services/ShjMailSendService.php | 68 +- ...00001_add_pplace_columns_to_zone_table.php | 38 + routes/api.php | 4 +- routes/console.php | 3 + 9 files changed, 998 insertions(+), 472 deletions(-) create mode 100644 database/migrations/2026_02_13_000001_add_pplace_columns_to_zone_table.php diff --git a/app/Console/Commands/ShjFourCCommand.php b/app/Console/Commands/ShjFourCCommand.php index 4d84407..02489cb 100644 --- a/app/Console/Commands/ShjFourCCommand.php +++ b/app/Console/Commands/ShjFourCCommand.php @@ -104,10 +104,12 @@ class ShjFourCCommand extends Command return self::SUCCESS; } else { - $this->error('SHJ-4C 室割当処理でエラーが発生しました: ' . $result['message']); - Log::error('SHJ-4C 室割当処理エラー', [ - 'error' => $result['message'], - 'details' => $result['details'] ?? null + // messageキーが無い場合はerror_infoを使用 + $errorMessage = $result['message'] ?? $result['error_info'] ?? '不明なエラー'; + $this->error('SHJ-4C 車室割り当て処理でエラーが発生しました: ' . $errorMessage); + Log::error('SHJ-4C 車室割り当て処理エラー', [ + 'error' => $errorMessage, + 'result' => $result ]); return self::FAILURE; diff --git a/app/Http/Controllers/Api/PaymentCallbackController.php b/app/Http/Controllers/Api/PaymentCallbackController.php index 7653d2e..11be092 100644 --- a/app/Http/Controllers/Api/PaymentCallbackController.php +++ b/app/Http/Controllers/Api/PaymentCallbackController.php @@ -152,6 +152,92 @@ class PaymentCallbackController extends Controller } } + /** + * SHJ-4A ウェルネット収納情報受付 + * + * POST /api/newwipe/callback/shj4a + * 仕様: JOB1(PUSH受信) → JOB2(SHJ-4Bへ渡す) → JOB3(応答返却 result=0) + */ + public function receiveShj4a(Request $request): Response + { + try { + // 疎通確認用 + if ($request->isMethod('get') && $request->all() === []) { + return response('000', 200)->header('Content-Type', 'text/plain'); + } + + // 【JOB1】パラメータ解析 + $params = $this->parseParams($request); + + $retRaw = (string) ($params['ret'] ?? ''); + $scdRaw = (string) ($params['scd'] ?? ''); + $rnoRaw = (string) ($params['rno'] ?? ''); + $kcdRaw = (string) ($params['kcd'] ?? ''); + $pwdRaw = (string) ($params['pwd'] ?? ''); + $ndt = (string) ($params['ndt'] ?? ''); + $tcd = (string) ($params['tcd'] ?? ''); + $ccd = (string) ($params['ccd'] ?? ''); + $pri = trim((string) ($params['pri'] ?? '')); + $zdt = (string) ($params['zdt'] ?? $params['sdt'] ?? ''); + $ifg = (string) ($params['ifg'] ?? ''); + + $ret = trim($retRaw); + $scd = trim($scdRaw); + $rno = trim($rnoRaw); + $kcd = trim($kcdRaw); + $pwd = trim($pwdRaw); + + // 仕様準拠: JOB1->JOB2->JOB3 を維持するため、異常時もJOB2へ渡す + // 受付番号が空の場合はログを残して継続 + if ($rno === '') { + \Illuminate\Support\Facades\Log::warning('SHJ-4A: 受付番号が空'); + } + + // MD5検証(仕様準拠のため、失敗時もログを残してJOB2へ継続) + $secretKey = config('wellnet.callback.md5_secret'); + if (empty($secretKey)) { + if (app()->environment('production')) { + \Illuminate\Support\Facades\Log::critical('WELLNET_MD5_SECRET未設定(SHJ-4A)'); + } + \Illuminate\Support\Facades\Log::warning('WELLNET_MD5_SECRET未設定: MD5検証スキップ(SHJ-4A)'); + } else { + $expectedHashRaw = md5("ret{$retRaw}scd{$scdRaw}rno{$rnoRaw}{$secretKey}"); + $expectedHashTrim = md5("ret{$ret}scd{$scd}rno{$rno}{$secretKey}"); + if ($pwd !== $expectedHashRaw && $pwd !== $expectedHashTrim) { + \Illuminate\Support\Facades\Log::warning('SHJ-4A: MD5検証失敗', [ + 'contract_payment_number' => $rno, + ]); + } + } + + // 【JOB2】SHJ-4Bへ渡す + $shjFourBService = app(\App\Services\ShjFourBService::class); + $shjFourBService->processWellnetPush([ + 'contract_payment_number' => $rno, + 'status' => $ret, + 'pay_code' => $scd, + 'corp_code' => $kcd, + 'mms_date' => $zdt, + 'cvs_code' => $ccd, + 'shop_code' => $tcd, + 'pay_date' => !empty($ndt) ? $this->parseDatetime($ndt) : null, + 'settlement_amount' => $pri, + 'stamp_flag' => $ifg, + 'md5_string' => $pwd, + ]); + + // 【JOB3】応答返却 result=0(正常受付) + return response('000', 200)->header('Content-Type', 'text/plain'); + + } catch (\Throwable $e) { + \Illuminate\Support\Facades\Log::error('SHJ-4A処理エラー: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + // SHJ-4B処理失敗でもWellnetには正常応答(ShjFourBCheckCommandがフォールバック) + return response('000', 200)->header('Content-Type', 'text/plain'); + } + } + /** * リクエストパラメータ解析(短縮名・論理名 両方対応) */ @@ -160,7 +246,7 @@ class PaymentCallbackController extends Controller $params = []; // 短縮名で取得 - $shortNames = ['ret', 'scd', 'rno', 'kcd', 'sdt', 'ccd', 'tcd', 'nno', 'ndt', 'pri', 'ifg', 'pwd']; + $shortNames = ['ret', 'scd', 'rno', 'kcd', 'sdt', 'zdt', 'ccd', 'tcd', 'nno', 'ndt', 'pri', 'ifg', 'pwd']; foreach ($shortNames as $name) { $value = $request->input($name); if ($value !== null) { diff --git a/app/Services/ShjFiveService.php b/app/Services/ShjFiveService.php index 629aa1f..0182e25 100644 --- a/app/Services/ShjFiveService.php +++ b/app/Services/ShjFiveService.php @@ -9,7 +9,7 @@ use App\Models\Device; use Exception; /** - * SHJ-5 空き待ち通知処理サービス + * SHJ-5 駐輪場空きチェック処理サービス * * 駐輪場の空き状況を確認し、空き待ち予約者への通知処理を実行する * 仕様書に基づくバックグラウンド定期バッチ処理 @@ -32,16 +32,17 @@ class ShjFiveService { $this->shjEightService = $shjEightService; } + /** * SHJ-5 メイン処理を実行 - * - * 処理フロー: - * 1. 駐輪場の空き状況を取得する - * 2. 空き状況判定 - * 3. 空き待ち者の情報を取得する - * 4. 取得件数判定 - * 5. 空き待ち者への通知、またはオペレーターキュー追加処理 - * 6. バッチ処理ログを作成する + * + * 処理フロー(仕様SHJ-5-1準拠): + * JOB1: 駐輪場の空き状況を取得する(SQL-1) + * JOB1-STEP1: 空き状況判定(空き台数 > 0 → JOB2、空きなし → ステータスコメント設定) + * JOB2: 空き待ち者の情報を取得する(SQL-2) + * JOB2-STEP1: 取得件数判定(> 0 → JOB3、なし → ステータスコメント設定) + * JOB3: 空き待ち者への通知/オペレーターキュー追加処理 + * JOB4: SHJ-8 バッチ処理ログ作成 * * @return array 処理結果 */ @@ -49,19 +50,20 @@ class ShjFiveService { try { $startTime = now(); - Log::info('SHJ-5 空き待ち通知処理開始'); + Log::info('SHJ-5 駐輪場空きチェック処理開始'); - // 処理統計 - $processedParksCount = 0; - $vacantParksCount = 0; - $totalWaitingUsers = 0; - $notificationSuccessCount = 0; - $operatorQueueCount = 0; - $mailErrors = []; // メール異常終了件数専用 - $errors = []; // 全体エラー収集用 - $allQueueItems = []; // 全オペレーターキュー作成用データ + // 処理統計(グローバル集計用) + $processedParksCount = 0; + $vacantParksCount = 0; + $totalWaitingUsers = 0; + $globalMailSuccessCount = 0; + $globalMailErrorCount = 0; + $globalQueueSuccessCount = 0; + $globalQueueErrorCount = 0; + $errors = []; + $job4LogCount = 0; - // 【処理1】駐輪場の空き状況を取得する + // 【JOB1】駐輪場の空き状況を取得する(SQL-1) $parkVacancyList = $this->getParkVacancyStatus(); Log::info('駐輪場空き状況取得完了', [ 'total_parks' => count($parkVacancyList) @@ -69,7 +71,6 @@ class ShjFiveService // 各駐輪場に対する処理 foreach ($parkVacancyList as $parkVacancyData) { - // 配列をオブジェクトに変換 $parkVacancy = (object) $parkVacancyData; $processedParksCount++; @@ -81,29 +82,38 @@ class ShjFiveService 'vacant_count' => $parkVacancy->vacant_count ]); - // 【判断1】空き状況判定 + $statusCommentNameBase = sprintf( + '%s/%s/%s', + (string) $parkVacancy->park_name, + (string) $parkVacancy->ptype_subject, + (string) $parkVacancy->psection_subject + ); + $statusCommentVacancyBase = $statusCommentNameBase . '/空き台数' . (int) $parkVacancy->vacant_count; + + // 【JOB1-STEP1】空き状況判定 if ($parkVacancy->vacant_count < 1) { - Log::info('空きなし - 処理スキップ', [ - 'park_id' => $parkVacancy->park_id, - 'vacant_count' => $parkVacancy->vacant_count - ]); + // 仕様準拠:空きなしはJOB4へ(ステータスコメントは駐輪場/分類/車種のみ) + $this->createBatchProcessLog('success', $statusCommentNameBase); + $job4LogCount++; + Log::info('空きなし', ['park_id' => $parkVacancy->park_id]); continue; } $vacantParksCount++; - // 【処理2】空き待ち者の情報を取得する + // 【JOB2】空き待ち者の情報を取得する(SQL-2) $waitingUsers = $this->getWaitingUsersInfo( $parkVacancy->park_id, $parkVacancy->psection_id, $parkVacancy->ptype_id ); - // 【判断2】取得件数判定 + // 【JOB2-STEP1】取得件数判定 if (empty($waitingUsers)) { - Log::info('空き待ち者なし', [ - 'park_id' => $parkVacancy->park_id - ]); + // 仕様準拠:空き待ち該当者なしはJOB4へ + $this->createBatchProcessLog('success', $statusCommentVacancyBase . ':空き待ち該当者なし'); + $job4LogCount++; + Log::info('空き待ち者なし', ['park_id' => $parkVacancy->park_id]); continue; } @@ -113,151 +123,120 @@ class ShjFiveService 'waiting_users_count' => count($waitingUsers) ]); - // 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 + // 【JOB3】空き待ち者への通知、またはオペレーターキュー追加処理 $notificationResult = $this->processWaitingUsersNotification( $waitingUsers, $parkVacancy ); - $notificationSuccessCount += $notificationResult['notification_success_count']; - $operatorQueueCount += $notificationResult['operator_queue_count']; - + // グローバル集計 + $globalMailSuccessCount += $notificationResult['mail_success_count']; + $globalMailErrorCount += $notificationResult['mail_error_count']; + $globalQueueSuccessCount += $notificationResult['queue_success_count']; + $globalQueueErrorCount += $notificationResult['queue_error_count']; if (!empty($notificationResult['errors'])) { - $mailErrors = array_merge($mailErrors, $notificationResult['errors']); $errors = array_merge($errors, $notificationResult['errors']); } - // オペレーターキュー作成用データを収集 - if (!empty($notificationResult['queue_items'])) { - $allQueueItems = array_merge($allQueueItems ?? [], $notificationResult['queue_items']); - } + $reserveIdText = !empty($notificationResult['processed_reserve_ids']) + ? implode('、', $notificationResult['processed_reserve_ids']) + : '-'; + $batchComment = (string) ($notificationResult['batch_comment'] ?? ''); + + // 仕様書記載の形式に合わせたステータスコメント + $statusComment = $statusCommentVacancyBase + . '/対象予約ID ' . $reserveIdText + . '/' . $batchComment + . ':メール正常終了件数' . $notificationResult['mail_success_count'] + . '、メール異常終了件数' . $notificationResult['mail_error_count'] + . '、キュー登録正常終了件数' . $notificationResult['queue_success_count'] + . '、キュー登録異常終了件数' . $notificationResult['queue_error_count']; + + $status = ($notificationResult['mail_error_count'] > 0 + || $notificationResult['queue_error_count'] > 0) + ? 'error' + : 'success'; + + $this->createBatchProcessLog($status, $statusComment); + $job4LogCount++; + + Log::info('駐輪場処理完了', [ + 'park_id' => $parkVacancy->park_id, + 'mail_success_count' => $notificationResult['mail_success_count'], + 'mail_error_count' => $notificationResult['mail_error_count'], + 'queue_success_count' => $notificationResult['queue_success_count'], + 'queue_error_count' => $notificationResult['queue_error_count'], + ]); } - // 【処理4】仕様書準拠:先に呼び出し、成功時のみ内部変数を更新 - $queueErrorCount = 0; // キュー登録異常終了件数(累計) - $queueSuccessCount = 0; // キュー登録正常終了件数(累計) - - foreach ($allQueueItems as $queueItem) { - // 仕様書準拠:在呼叫前先計算"如果這次成功會是第幾件",確保記錄反映最新件數 - $predictedSuccessCount = $queueSuccessCount + 1; - $predictedErrorCount = $queueErrorCount; // 暂时保持当前错误计数 - - $queueResult = $this->addToOperatorQueue( - $queueItem['waiting_user'], - $queueItem['park_vacancy'], - $queueItem['batch_comment'], - $notificationSuccessCount, // 最終メール正常終了件数 - $predictedSuccessCount, // 預測成功時的件數(包含本次) - count($mailErrors), // 現在のメール異常終了件数(動態計算) - $predictedErrorCount // 現在のキュー登録異常終了件数 - ); - - // 仕様書:根据实际结果决定是否采用预测值 - if ($queueResult['success']) { - $queueSuccessCount = $predictedSuccessCount; // 采用预测的成功计数 - } else { - $queueErrorCount++; // 失败时递增错误计数 - - // 仕様書:包含具体错误消息,满足"エラーメッセージ/スタックトレースを保持"要求 - $errorDetail = $queueResult['error'] ?? 'Unknown error'; - $queueErrorInfo = sprintf('キュー登録失敗:予約ID:%d - %s', - $queueItem['waiting_user']->reserve_id ?? 0, - $errorDetail - ); - $errors[] = $queueErrorInfo; // 加入总错误统计(包含具体原因) - - Log::error('オペレーターキュー作成失敗', [ - 'user_id' => $queueItem['waiting_user']->user_id, - 'reserve_id' => $queueItem['waiting_user']->reserve_id ?? 0, - 'error' => $errorDetail - ]); - } + // JOB1対象が0件の場合もJOB4ログを残す + if ($processedParksCount === 0) { + $this->createBatchProcessLog('success', '対象駐輪場データなし'); + $job4LogCount++; } $endTime = now(); $duration = $startTime->diffInSeconds($endTime); - Log::info('SHJ-5 空き待ち通知処理完了', [ + Log::info('SHJ-5 駐輪場空きチェック処理完了', [ 'duration_seconds' => $duration, 'processed_parks_count' => $processedParksCount, 'vacant_parks_count' => $vacantParksCount, 'total_waiting_users' => $totalWaitingUsers, - 'notification_success_count' => $notificationSuccessCount, - 'operator_queue_success_count' => $queueSuccessCount, // 仕様書:正常完了件数 - 'queue_error_count' => $queueErrorCount, - 'mail_error_count' => count($mailErrors), // メール異常終了件数(分離) - 'total_error_count' => count($errors) // 全体エラー件数 + 'total_error_count' => count($errors) ]); - // 仕様書に基づく内部変数.ステータスコメント生成 - $statusComment = sprintf( - 'メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', - $notificationSuccessCount, - count($mailErrors), // メール異常終了件数(キュー失敗を除外) - $queueSuccessCount ?? 0, // 実際のキュー登録成功件数 - $queueErrorCount ?? 0 // 実際のキュー登録失敗件数 - ); - - // SHJ-8 バッチ処理ログ作成 - try { - $device = Device::orderBy('device_id')->first(); - $deviceId = $device ? $device->device_id : 1; - $today = now()->format('Y/m/d'); - - $this->shjEightService->execute( - $deviceId, - 'SHJ-5', - 'SHJ-5空き待ち通知', - 'success', - $statusComment, - $today, - $today - ); - - Log::info('SHJ-8 バッチ処理ログ作成完了'); - } catch (Exception $e) { - Log::error('SHJ-8 バッチ処理ログ作成エラー', [ - 'error' => $e->getMessage() - ]); - } - return [ 'success' => true, - 'message' => 'SHJ-5 空き待ち通知処理が正常に完了しました', + 'message' => 'SHJ-5 駐輪場空きチェック処理が正常に完了しました', 'processed_parks_count' => $processedParksCount, 'vacant_parks_count' => $vacantParksCount, 'total_waiting_users' => $totalWaitingUsers, - 'notification_success_count' => $notificationSuccessCount, - 'operator_queue_count' => $queueSuccessCount ?? 0, // 仕様書:正常完了件数を使用 + 'notification_success_count' => $globalMailSuccessCount, + 'operator_queue_count' => $globalQueueSuccessCount, + 'mail_error_count' => $globalMailErrorCount, + 'queue_error_count' => $globalQueueErrorCount, 'error_count' => count($errors), 'errors' => $errors, 'duration_seconds' => $duration, - 'status_comment' => $statusComment // SHJ-8用の完全なステータスコメント + 'job4_log_count' => $job4LogCount, + 'status_comment' => sprintf( + '処理対象駐輪場数:%d/空きあり駐輪場数:%d/メール正常:%d/メール異常:%d/キュー正常:%d/キュー異常:%d', + $processedParksCount, + $vacantParksCount, + $globalMailSuccessCount, + $globalMailErrorCount, + $globalQueueSuccessCount, + $globalQueueErrorCount + ) ]; } catch (Exception $e) { - Log::error('SHJ-5 空き待ち通知処理でエラーが発生', [ + Log::error('SHJ-5 駐輪場空きチェック処理でエラーが発生', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); + // 異常時もJOB4(error)を出力する + $this->createBatchProcessLog('error', 'SHJ-5処理エラー:' . $e->getMessage()); + return [ 'success' => false, - 'message' => 'SHJ-5 空き待ち通知処理でエラーが発生: ' . $e->getMessage(), + 'message' => 'SHJ-5 駐輪場空きチェック処理でエラーが発生: ' . $e->getMessage(), 'error_details' => $e->getMessage() ]; } } /** - * 【処理1】駐輪場の空き状況を取得する + * 【JOB1】駐輪場の空き状況を取得する * * 仕様書に基づくSQL(SQL-1): * - 「駐輪場マスタ」と「ゾーンマスタ」より空き状況を取得する * - zone表から zone_number(現在契約台数)、zone_tolerance(限界収容台数)を取得 * - 空き台数 = 限界収容台数 - 現在契約台数 * - * @return array 駐輪場空き状況リスト + * @return array 駐輪場空き状況リスト(全レコード、フィルターなし) */ private function getParkVacancyStatus(): array { @@ -290,7 +269,7 @@ class ShjFiveService ->get() ->map(function($record) { // 【JOB1-STEP1】空き台数 = 限界収容台数 - 現在契約台数 - $vacant_count = max(0, $record->sum_zone_tolerance - $record->sum_zone_number); + $vacant_count = (int) $record->sum_zone_tolerance - (int) $record->sum_zone_number; return (object)[ 'park_id' => $record->park_id, @@ -306,17 +285,10 @@ class ShjFiveService 'vacant_count' => $vacant_count, // 内部変数.空き台数 ]; }) - ->filter(function($zone) { - // 【判断1】空き状況判定:空きがあるもののみ - return $zone->vacant_count > 0; - }) ->values(); Log::info('駐輪場空き状況取得完了(仕様書SQL-1準拠)', [ - 'total_records' => count($vacancyList), - 'vacant_records' => $vacancyList->filter(function($v) { - return $v->vacant_count > 0; - })->count() + 'total_records' => count($vacancyList) ]); return $vacancyList->toArray(); @@ -330,9 +302,9 @@ class ShjFiveService } /** - * 【処理2】空き待ち者の情報を取得する + * 【JOB2】空き待ち者の情報を取得する * - * 仕様書JOB2に基づくSQL: + * 仕様書JOB2に基づくSQL(SQL-2): * - 空きが発生している「駐輪場ID」「駐輪分類ID」「車種区分ID」で空き待ちしている利用者を抽出する * - reserve表 + user表のみ(仕様書準拠) * - JOIN条件:T1.user_id = T2.user_seq(重要!) @@ -355,11 +327,6 @@ class ShjFiveService 'T2.user_primemail', // メールアドレス 'T2.user_submail', // 予備メールアドレス 'T1.reserve_manual', // 手動通知 - // 以下は処理に必要な追加フィールド - 'T1.park_id', - 'T1.psection_id', - 'T1.ptype_id', - 'T1.reserve_date', ]) // 仕様書準拠:T1.user_id = T2.user_seq(user表のPK) ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') @@ -396,74 +363,80 @@ class ShjFiveService } /** - * 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 - * - * 仕様書に基づく分岐処理: - * - 手動通知フラグ判定(reserve_manual) - * - メール送信成功時のreserve.sent_date更新 - * - 失敗時のオペレーターキュー追加(最終統計で処理) + * 【JOB3】空き待ち者への通知、またはオペレーターキュー追加処理 + * + * 仕様SHJ-5-1準拠の分岐処理: + * - 手動通知 <> 1: SHJ-7メール送信 → 成功時SQL-3更新 / 失敗時はカウント+バッチコメントのみ + * - 手動通知 = 1: SQL-4オペレーターキュー登録 * * @param array $waitingUsers 空き待ち者リスト * @param object $parkVacancy 駐輪場空き情報 - * @return array 通知処理結果 + * @return array 通知処理結果(駐輪場毎の統計) */ private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array { - $notificationSuccessCount = 0; - $operatorQueueCount = 0; + // 仕様準拠:駐輪場毎のカウント + $mailSuccessCount = 0; + $mailErrorCount = 0; + $queueSuccessCount = 0; + $queueErrorCount = 0; $errors = []; - $queueItems = []; // オペレーターキュー作成用データ収集 + $processedReserveIds = []; // 仕様:内部変数.対象予約ID + $batchComments = []; // 仕様:内部変数.バッチコメント + $queuedIds = []; // SQL-4で登録したキューID try { // 空きがある分だけ処理(先着順) $availableSpots = min($parkVacancy->vacant_count, count($waitingUsers)); - + for ($i = 0; $i < $availableSpots; $i++) { - $waitingUserData = $waitingUsers[$i]; - // 配列をオブジェクトに変換 - $waitingUser = (object) $waitingUserData; - + $waitingUser = (object) $waitingUsers[$i]; + $processedReserveIds[] = $waitingUser->reserve_id; + try { // 【仕様判断】手動通知フラグチェック if ($waitingUser->reserve_manual == 1) { - // 手動通知 → オペレーターキュー作成データ収集 - $batchComment = '手動通知フラグ設定のため予約ID:' . $waitingUser->reserve_id; - $queueItems[] = [ - 'waiting_user' => $waitingUser, - 'park_vacancy' => $parkVacancy, - 'batch_comment' => $batchComment - ]; - $operatorQueueCount++; - - Log::info('手動通知フラグによりオペレーターキュー登録予定', [ - 'user_id' => $waitingUser->user_id, - 'reserve_id' => $waitingUser->reserve_id - ]); + // 手動通知 = 1 → SQL-4 オペレーターキュー登録 + $queueResult = $this->addToOperatorQueue($waitingUser, $parkVacancy); + + if ($queueResult['success']) { + // 仕様:キュー登録正常終了件数+1 + $queueSuccessCount++; + if (!empty($queueResult['que_id'])) { + $queuedIds[] = (int) $queueResult['que_id']; + } + Log::info('オペレーターキュー登録成功', [ + 'user_id' => $waitingUser->user_id, + 'reserve_id' => $waitingUser->reserve_id + ]); + } else { + // 仕様:キュー登録異常終了件数+1 → バッチコメント設定 + $queueErrorCount++; + $errorMsg = $queueResult['error'] ?? 'キュー登録エラー'; + $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':' . $errorMsg; + $errors[] = $errorMsg; + } } else { - // 自動通知 → メール送信を試行 + // 手動通知 <> 1 → SHJ-7 メール送信 $mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy); - + if ($mailResult['success']) { - // メール送信成功 → reserve.sent_date更新 + // 仕様:メール送信成功 → SQL-3 reserve更新 → メール正常終了件数+1 $this->updateReserveSentDate($waitingUser->reserve_id); - $notificationSuccessCount++; - + $mailSuccessCount++; + Log::info('空き待ち通知メール送信成功', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id, 'park_id' => $parkVacancy->park_id ]); } else { - // メール送信失敗 → オペレーターキュー作成データ収集 - $shjSevenError = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー'; - $batchComment = $shjSevenError . '予約ID:' . $waitingUser->reserve_id; - $queueItems[] = [ - 'waiting_user' => $waitingUser, - 'park_vacancy' => $parkVacancy, - 'batch_comment' => $batchComment - ]; - $operatorQueueCount++; - $errors[] = $shjSevenError; + // 仕様:メール送信失敗 → メール異常終了件数+1 → バッチコメント設定 → 次のユーザーへ + // ※オペレーターキューは作成しない(仕様準拠) + $mailErrorCount++; + $errorMsg = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー'; + $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':' . $errorMsg; + $errors[] = $errorMsg; } } @@ -473,24 +446,57 @@ class ShjFiveService 'reserve_id' => $waitingUser->reserve_id, 'error' => $e->getMessage() ]); - - // エラー発生時もオペレーターキュー作成データ収集 - $batchComment = 'システムエラー:' . $e->getMessage() . '予約ID:' . $waitingUser->reserve_id; - $queueItems[] = [ - 'waiting_user' => $waitingUser, - 'park_vacancy' => $parkVacancy, - 'batch_comment' => $batchComment - ]; - $operatorQueueCount++; + + // 仕様準拠:分岐に応じて異常件数を加算 + if ((int) $waitingUser->reserve_manual === 1) { + $queueErrorCount++; + } else { + $mailErrorCount++; + } + + $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':システムエラー:' . $e->getMessage(); $errors[] = $e->getMessage(); } } + // SQL関連仕様準拠:que_status_commentに集計情報を反映 + if (!empty($queuedIds)) { + $reserveIdText = !empty($processedReserveIds) + ? implode('、', $processedReserveIds) + : '-'; + $batchCommentText = implode('、', $batchComments); + + $queueStatusComment = sprintf( + '%s/%s/%s/空き台数%d/対象予約ID %s/%s:メール正常終了件数%d、メール異常終了件数%d、キュー登録正常終了件数%d、キュー登録異常終了件数%d', + (string) $parkVacancy->park_name, + (string) $parkVacancy->ptype_subject, + (string) $parkVacancy->psection_subject, + (int) $parkVacancy->vacant_count, + $reserveIdText, + $batchCommentText, + $mailSuccessCount, + $mailErrorCount, + $queueSuccessCount, + $queueErrorCount + ); + + DB::table('operator_que') + ->whereIn('que_id', $queuedIds) + ->update([ + 'que_status_comment' => mb_strimwidth($queueStatusComment, 0, 255, ''), + 'work_instructions' => '空き待ち者への連絡をお願いします。', + 'updated_at' => now(), + ]); + } + return [ - 'notification_success_count' => $notificationSuccessCount, - 'operator_queue_count' => $operatorQueueCount, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'processed_reserve_ids' => $processedReserveIds, + 'batch_comment' => implode('、', $batchComments), 'errors' => $errors, - 'queue_items' => $queueItems // 後でキュー作成用 ]; } catch (Exception $e) { @@ -578,8 +584,8 @@ class ShjFiveService } /** - * reserve.sent_date及びvalid_flag更新 - * + * SQL-3: reserve.sent_date及びvalid_flag更新 + * * 仕様書準拠:メール送信成功時にreserve.sent_dateとvalid_flag=0を同時更新 * 重複通知を防ぎ、処理済みマークを設定 * @@ -613,74 +619,51 @@ class ShjFiveService } /** - * オペレーターキューに追加 - * - * 仕様書に基づくキュー登録: + * SQL-4: オペレーターキューに追加 + * + * SHJ-5-1準拠のキュー登録: * - que_comment: 空文字列 - * - que_status_comment: 仕様書完全準拠形式(統計情報含む) + * - que_status_comment: 初期登録は空文字列(フロー仕様) + * - work_instructions: 初期登録は空文字列(フロー仕様) * - operator_id: 9999999固定 * * @param object $waitingUser 空き待ち者情報 * @param object $parkVacancy 駐輪場空き情報 - * @param string $batchComment 内部変数.バッチコメント - * @param int $mailSuccessCount メール正常終了件数 - * @param int $queueSuccessCount キュー登録正常終了件数 - * @param int $mailErrorCount メール異常終了件数 - * @param int $queueErrorCount キュー登録異常終了件数 * @return array 追加結果 */ - private function addToOperatorQueue(object $waitingUser, object $parkVacancy, string $batchComment, int $mailSuccessCount, int $queueSuccessCount, int $mailErrorCount, int $queueErrorCount): array + private function addToOperatorQueue(object $waitingUser, object $parkVacancy): array { try { - // 仕様書完全準拠:駐輪場名/駐輪分類名/車種区分名/空き台数…/対象予約ID…/内部変数.バッチコメント/内部変数.メール正常終了件数…メール異常終了件数…キュー登録正常終了件数…キュー登録異常終了件数… - $statusComment = sprintf( - '%s/%s/%s/空き台数:%d台/対象予約ID:%d/%s/メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', - $parkVacancy->park_name ?? '', // JOB1から取得 - $parkVacancy->ptype_subject ?? '', // 駐輪分類名(JOB1から取得) - $parkVacancy->psection_subject ?? '', // 車種区分名(JOB1から取得) - $parkVacancy->vacant_count ?? 0, - $waitingUser->reserve_id ?? 0, - $batchComment, // 内部変数.バッチコメント - $mailSuccessCount, // 内部変数.メール正常終了件数 - $mailErrorCount, - $queueSuccessCount, - $queueErrorCount - ); - - OperatorQue::create([ + $queue = OperatorQue::create([ 'que_class' => 4, // 予約告知通知 'user_id' => $waitingUser->user_id, 'contract_id' => null, - 'park_id' => $waitingUser->park_id, + 'park_id' => $parkVacancy->park_id, 'que_comment' => '', // 仕様書:空文字列 'que_status' => 1, // キュー発生 - 'que_status_comment' => $statusComment, // 仕様書:完全準拠形式 - 'work_instructions' => '空き待ち者への連絡をお願いします。', + 'que_status_comment' => '', // フロー仕様: 空文字列 + 'work_instructions' => '', // フロー仕様: 空文字列 'operator_id' => 9999999, // 仕様書:固定値9999999 ]); Log::info('オペレーターキュー追加成功', [ 'user_id' => $waitingUser->user_id, - 'park_id' => $waitingUser->park_id, + 'park_id' => $parkVacancy->park_id, 'reserve_id' => $waitingUser->reserve_id, 'que_class' => 4, 'operator_id' => 9999999, - 'batch_comment' => $batchComment, - 'mail_success_count' => $mailSuccessCount, - 'mail_error_count' => $mailErrorCount, - 'queue_success_count' => $queueSuccessCount, - 'queue_error_count' => $queueErrorCount, - 'status_comment' => $statusComment ]); - return ['success' => true]; + return [ + 'success' => true, + 'que_id' => (int) $queue->que_id, + ]; } catch (Exception $e) { Log::error('オペレーターキュー追加エラー', [ 'user_id' => $waitingUser->user_id, - 'park_id' => $waitingUser->park_id, + 'park_id' => $parkVacancy->park_id, 'reserve_id' => $waitingUser->reserve_id ?? null, - 'batch_comment' => $batchComment, 'error' => $e->getMessage() ]); @@ -690,4 +673,50 @@ class ShjFiveService ]; } } + + /** + * 【JOB4】SHJ-8バッチ処理ログを作成 + * + * @param string $status success|error + * @param string $statusComment ステータスコメント + * @return void + */ + private function createBatchProcessLog(string $status, string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + // SHJ-8の制約(255文字以内)に合わせる + $statusComment = mb_strimwidth($statusComment, 0, 255, ''); + if ($statusComment === '') { + $statusComment = 'SHJ-5処理実行'; + } + + $result = $this->shjEightService->execute( + $deviceId, + 'SHJ-5', + 'SHJ-5 駐輪場空きチェック', + $status, + $statusComment, + $today, + $today + ); + + if (($result['result'] ?? 1) !== 0) { + Log::error('SHJ-5 JOB4バッチログ作成失敗', [ + 'status' => $status, + 'status_comment' => $statusComment, + 'result' => $result, + ]); + } + } catch (Exception $e) { + Log::error('SHJ-5 JOB4バッチログ作成例外', [ + 'status' => $status, + 'status_comment' => $statusComment, + 'error' => $e->getMessage(), + ]); + } + } } diff --git a/app/Services/ShjFourBService.php b/app/Services/ShjFourBService.php index e9fea8a..933e674 100644 --- a/app/Services/ShjFourBService.php +++ b/app/Services/ShjFourBService.php @@ -9,11 +9,13 @@ use App\Models\PriceA; use App\Models\BatJobLog; use App\Models\Device; use App\Models\User; +use App\Services\ShjFourCService; use App\Services\ShjThirteenService; use App\Services\ShjEightService; use App\Services\ShjMailSendService; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; use Carbon\Carbon; /** @@ -45,6 +47,13 @@ class ShjFourBService */ protected $shjEightService; + /** + * SHJ-4C 車室割り当てサービス + * + * @var ShjFourCService + */ + protected $shjFourCService; + /** * SHJ-7 メール送信サービス * @@ -57,13 +66,16 @@ class ShjFourBService * * @param ShjEightService $shjEightService * @param ShjMailSendService $mailSendService + * @param ShjFourCService $shjFourCService */ public function __construct( ShjEightService $shjEightService, - ShjMailSendService $mailSendService + ShjMailSendService $mailSendService, + ShjFourCService $shjFourCService ) { $this->shjEightService = $shjEightService; $this->mailSendService = $mailSendService; + $this->shjFourCService = $shjFourCService; } /** @@ -99,7 +111,7 @@ class ShjFourBService if (!$contractResult['found']) { // 対象レコードなしの場合 $result = $this->handleNoTargetRecord($settlement, $contractResult); - $this->createBatchLog($settlement, null, null, true, $result['message']); + $this->createBatchLog($settlement, null, null, true, null, null, null, $result['message']); return $result; } @@ -107,7 +119,7 @@ class ShjFourBService // 登録済みの場合 $result = $this->handleAlreadyProcessed($settlement, $contractResult); $contract = $contractResult['contract']; - $this->createBatchLog($settlement, $contract, null, true, $result['message']); + $this->createBatchLog($settlement, $contract, null, true, null, null, null, $result['message']); return $result; } @@ -117,20 +129,36 @@ class ShjFourBService $statusResult = $this->judgeReceiptStatus($settlement, $contract); if (!$statusResult['valid']) { - // 授受状態が異常な場合 + // 授受済みの場合(仕様: JOB5メール送信 → JOB6バッチログ → 終了) $result = $this->handleInvalidStatus($settlement, $contract, $statusResult); - $this->createBatchLog($settlement, $contract, null, true, $result['message']); + // 【JOB5】メール送信(授受済みでも送信) + $mailResult = $this->sendUserNotificationMail($settlement, $contract); + $mailCommentSuffix = $mailResult['batch_comment_suffix'] ?? null; + // 【JOB6】バッチログ(授受済みメッセージを直接statusCommentとして渡す) + $this->createBatchLog( + $settlement, $contract, null, true, null, $mailCommentSuffix, + null, $result['message'] + ); return $result; } - // 【判断2】金額チェック + // 【JOB3-0】SHJ-4C 車室割り当て(新規のみ) + // 仕様:定期契約継続フラグ(update_flag) = 2(新規)の場合に呼び出す + // ※異常があったとしても、処理は継続する + $shjFourCResult = null; + if ((int) $contract->update_flag === 2) { + $shjFourCResult = $this->triggerShjFourC($contract); + } + + // 【JOB3-1】契約更新処理実行(SHJ-4C結果を含む) + // 仕様フロー順: JOB3-0 → JOB3-1/3-4 → JOB3-STEP1(金額チェック) + $updateResult = $this->executeContractUpdate($settlement, $contract, $shjFourCResult); + + // 【判断2】金額チェック(仕様のJOB3-STEP1) $amountResult = $this->judgeAmountComparison($settlement, $contract); - - // 【処理3】契約更新処理実行 - $updateResult = $this->executeContractUpdate($settlement, $contract, $amountResult); - - // 副作用処理実行 - $sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult); + + // 副作用処理実行(SHJ-4C結果をSHJ-13に渡す) + $sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult, $shjFourCResult); $result = [ 'success' => true, @@ -147,7 +175,8 @@ class ShjFourBService // 【処理6】バッチ処理ログ作成(SHJ-8呼び出し) $mailCommentSuffix = $sideEffectResult['user_mail']['batch_comment_suffix'] ?? null; - $this->createBatchLog($settlement, $contract, $amountResult, true, null, $mailCommentSuffix); + $photoDeletionResult = $sideEffectResult['photo_deletion'] ?? null; + $this->createBatchLog($settlement, $contract, $amountResult, true, null, $mailCommentSuffix, $photoDeletionResult); return $result; @@ -173,6 +202,96 @@ class ShjFourBService } } + /** + * ウェルネットPUSHデータ処理入口 + * + * 仕様順序: JOB1(SQL-1 契約検索) → JOB2(SQL-2 重複チェック → SQL-3 INSERT) → 既存処理 + * 既存の processSettlementTransaction(id) は再処理・後方互換の入口として残す + * + * @param array $wellnetData ウェルネットからの生データ + * @return array 処理結果 + */ + public function processWellnetPush(array $wellnetData): array + { + $contractPaymentNumber = $wellnetData['contract_payment_number']; + + Log::info('SHJ-4B ウェルネットPUSHデータ処理開始', [ + 'contract_payment_number' => $contractPaymentNumber, + ]); + + // 【JOB1】SQL-1: 定期契約マスタの対象レコード取得(judgeTargetContractと同一条件) + $contract = DB::table('regular_contract as T1') + ->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', $contractPaymentNumber) + ->where('T1.contract_cancel_flag', '!=', 1) + ->whereNotNull('T1.contract_flag') + ->select('T1.contract_id') + ->first(); + + // JOB1-STEP1: 対象レコード判定 + if (!$contract) { + Log::warning('SHJ-4B JOB1: 受付番号不正(対象契約なし)', [ + 'contract_payment_number' => $contractPaymentNumber, + ]); + // 仕様: JOB6 バッチログ → 終了(settlement_transaction未作成) + $statusComment = "受付番号が不正です。(受付番号:{$contractPaymentNumber})"; + $this->createWellnetPushBatchLog($statusComment); + return [ + 'success' => true, + 'result' => 'no_target', + 'message' => $statusComment, + ]; + } + + // 【JOB2】SQL-2: 重複チェック + // 補足: 受付番号は contract_payment_number(rno)で照合する + $existCount = DB::table('settlement_transaction') + ->where('contract_payment_number', $contractPaymentNumber) + ->count(); + + if ($existCount >= 1) { + Log::info('SHJ-4B SQL-2: 既に登録済み', [ + 'contract_payment_number' => $contractPaymentNumber, + ]); + return [ + 'success' => true, + 'result' => 'already_registered', + 'message' => '決済トランザクションは既に登録済みです', + ]; + } + + // 【SQL-3】決済トランザクション挿入(.定期契約ID を使用) + $settlementId = DB::table('settlement_transaction')->insertGetId([ + 'contract_id' => $contract->contract_id, + 'status' => $wellnetData['status'] ?? null, + 'pay_code' => $wellnetData['pay_code'] ?? null, + 'contract_payment_number' => $contractPaymentNumber, + 'corp_code' => $wellnetData['corp_code'] ?? null, + 'mms_date' => $wellnetData['mms_date'] ?? null, + 'cvs_code' => $wellnetData['cvs_code'] ?? null, + 'shop_code' => $wellnetData['shop_code'] ?? null, + 'pay_date' => $wellnetData['pay_date'] ?? null, + 'settlement_amount' => $wellnetData['settlement_amount'] ?? null, + 'stamp_flag' => $wellnetData['stamp_flag'] ?? null, + 'md5_string' => $wellnetData['md5_string'] ?? null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Log::info('SHJ-4B SQL-3: 決済トランザクション登録', [ + 'settlement_transaction_id' => $settlementId, + 'contract_payment_number' => $contractPaymentNumber, + 'contract_id' => $contract->contract_id, + ]); + + // JOB2-STEP2以降: 既存フローへ + return $this->processSettlementTransaction($settlementId); + } + /** * 決済トランザクション取得 * @@ -212,7 +331,7 @@ class ShjFourBService 'contract_payment_number' => $settlement->contract_payment_number, ]); - // 文档要求のSQL構造に基づく対象レコード取得 + // 仕様書要件のSQL構造に基づく対象レコード取得 // regular_contract T1 inner join park T2 inner join price_a T4 $contractQuery = DB::table('regular_contract as T1') ->select([ @@ -234,13 +353,17 @@ class ShjFourBService 'T1.contract_created_at', 'T1.contract_cancel_flag', 'T2.park_name', + 'T2.immediate_use_permit', + 'T2.update_grace_period_start_date', + 'T2.update_grace_period_end_date', + 'T2.parking_start_grace_period', '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'); // 文档要求の第二条件追加 + ->on('T1.park_id', '=', 'T4.park_id'); // 仕様書要件の第2条件 }) ->where('T1.contract_payment_number', $settlement->contract_payment_number) ->where('T1.contract_cancel_flag', '!=', 1) // 解約されていない @@ -257,7 +380,7 @@ class ShjFourBService 'found' => false, 'contract' => null, 'reason' => '対象レコードなし', - 'message' => "契約番号に一致する有効な定期契約が見つかりません: {$settlement->contract_payment_number}", + 'message' => "受付番号が不正です。(受付番号:{$settlement->contract_payment_number})", ]; } @@ -366,49 +489,36 @@ class ShjFourBService */ 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: 授受状態異常', [ + // 仕様JOB2-STEP2: 授受フラグ = 0 の場合、処理続行 + if ((int) $contract->contract_flag === 0) { + Log::info('SHJ-4B 判断1: 授受フラグ=0 処理続行', [ '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' => '決済トランザクションまたは契約の状態が更新処理に適していません', + 'valid' => true, + 'reason' => '授受フラグ未処理', + 'message' => '授受フラグチェックに合格しました', ]; } - - Log::info('SHJ-4B 判断1: 授受状態正常', [ + + // その他の場合(授受済み) + $message = sprintf( + '支払いステータスチェック:授受済みです。(定期契約ID:%s)', + $contract->contract_id + ); + + Log::warning('SHJ-4B 判断1: 授受済み', [ 'settlement_transaction_id' => $settlement->settlement_transaction_id, 'contract_id' => $contract->contract_id, - 'status_checks' => $statusChecks, + 'contract_flag' => $contract->contract_flag, ]); - + return [ - 'valid' => true, - 'reason' => '授受状態正常', - 'status_checks' => $statusChecks, - 'message' => '授受状態チェックに合格しました', + 'valid' => false, + 'reason' => '授受済み', + 'message' => $message, ]; } @@ -421,7 +531,7 @@ class ShjFourBService */ private function judgeAmountComparison(SettlementTransaction $settlement, $contract): array { - // 文档要求:請求額=授受額の厳密比較 + // 仕様書要件: 請求額=授受額の厳密比較 $billingAmount = (int) $contract->billing_amount; // 整数として比較 $settlementAmount = (int) $settlement->settlement_amount; // 整数として比較 @@ -486,49 +596,45 @@ class ShjFourBService } /** - * パターンA/B判断 - * - * 月を跨らない(パターンA)vs 月を跨る(パターンB)の判定 + * 【SQL-4】シール印刷可能日を計算(仕様パターンA/B準拠) + * + * パターン判定: + * - 即利用許可=1 → 入金日の年月日 + * - 更新期間開始日 <= 更新期間終了日(パターンA): + * - A-1: 駐輪開始猶予期間 > 本日の日 → 入金日の年月 + 猶予期間日 + * - A-2: それ以外 → 入金日の年月日 + * - 更新期間開始日 > 更新期間終了日(パターンB)→ 入金日の年月日 * - * @param object $contract * @param SettlementTransaction $settlement - * @return array + * @param object $contract + * @return string Y-m-d形式の日付 */ - private function judgeContractPattern($contract, SettlementTransaction $settlement): array + private function calculatePrintableDate(SettlementTransaction $settlement, $contract): string { $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(月を跨る)'; - } + // 契約後即利用許可 = 1 + if ((int) ($contract->immediate_use_permit ?? 0) === 1) { + return $payDate->format('Y-m-d'); } - 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, - ]); + $startDate = (int) ($contract->update_grace_period_start_date ?? 0); + $endDate = (int) ($contract->update_grace_period_end_date ?? 0); + $gracePeriod = (int) ($contract->parking_start_grace_period ?? 0); + $todayDay = (int) now()->format('d'); - return [ - 'pattern' => $isPatternB ? 'B' : 'A', - 'is_pattern_b' => $isPatternB, - 'reason' => $patternReason, - 'pay_date' => $payDate, - 'contract_start' => $contractStart, - 'contract_end' => $contractEnd, - ]; + // パターンA: 更新期間開始日 <= 更新期間終了日 + if ($startDate <= $endDate) { + if ($gracePeriod > $todayDay) { + // A-1: 入金日の年月 + 猶予期間日 + return $payDate->format('Y-m') . '-' . str_pad($gracePeriod, 2, '0', STR_PAD_LEFT); + } + // A-2: 入金日の年月日 + return $payDate->format('Y-m-d'); + } + + // パターンB: 全サブケース同一 → 入金日の年月日 + return $payDate->format('Y-m-d'); } /** @@ -560,96 +666,86 @@ class ShjFourBService } /** - * 【処理3】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新 + * 【JOB3-1】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新 * * @param SettlementTransaction $settlement * @param object $contract - * @param array $amountResult + * @param array|null $shjFourCResult SHJ-4C車室割り当て結果(新規のみ) * @return array */ private function executeContractUpdate( - SettlementTransaction $settlement, - $contract, - array $amountResult + SettlementTransaction $settlement, + $contract, + ?array $shjFourCResult = null ): array { $updateData = []; $updated = false; - + try { - // パターンA/B判定 - $pattern = $this->judgeContractPattern($contract, $settlement); - - DB::transaction(function() use ($settlement, $contract, $amountResult, $pattern, &$updateData, &$updated) { - // 基本更新項目 + DB::transaction(function() use ($settlement, $contract, $shjFourCResult, &$updateData, &$updated) { + // 【SQL-4】基本更新項目(仕様準拠) $updateData = [ 'contract_payment_day' => Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'), - 'contract_updated_at' => now(), + 'contract_money' => $settlement->settlement_amount, + 'contact_shop_code' => $settlement->shop_code, + 'contract_cvs_class' => $settlement->cvs_code, + 'contract_flag' => 1, + 'settlement_transaction_id' => $settlement->settlement_transaction_id, + 'contract_permission' => 1, + 'printable_date' => $this->calculatePrintableDate($settlement, $contract), + 'contract_payment_number' => $settlement->contract_payment_number, + '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; + + // 【新規】SHJ-4Cの結果を反映(仕様SQL-4準拠) + if ((int) $contract->update_flag === 2 && $shjFourCResult + && ($shjFourCResult['success'] ?? false) + && !empty($shjFourCResult['zone_id']) && !empty($shjFourCResult['pplace_no'])) { + $updateData['zone_id'] = $shjFourCResult['zone_id']; + $updateData['pplace_no'] = $shjFourCResult['pplace_no']; } - // パターンBの場合の特殊処理 - if ($pattern['is_pattern_b']) { - // 契約期間の延長処理等 - if ($pattern['contract_end']) { - $newEndDate = $pattern['contract_end']->addMonth(); - $updateData['contract_periode'] = $newEndDate->format('Y-m-d'); - } - } - - // 【定期契約マスタ更新】 + // 【SQL-4】定期契約マスタ更新 $affectedRows = DB::table('regular_contract') ->where('contract_id', $contract->contract_id) ->update($updateData); - + $updated = $affectedRows > 0; - // 【定期予約マスタ更新】(reserve_idが設定されている場合) + // 【SQL-5】契約元の定期契約マスタを更新(旧契約の更新済フラグ) + if (!empty($contract->old_contract_id)) { + DB::table('regular_contract') + ->where('contract_id', $contract->old_contract_id) + ->update(['contract_renewal' => 1, 'updated_at' => now()]); + + Log::info('SHJ-4B SQL-5 旧契約更新済フラグ更新', [ + 'old_contract_id' => $contract->old_contract_id, + 'contract_id' => $contract->contract_id, + ]); + } + + // 【SQL-8】定期予約マスタ更新(reserve_idがnull以外の場合) 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') + DB::table('reserve') ->where('reserve_id', $contract->reserve_id) - ->update($reserveUpdateData); + ->update([ + 'contract_id' => $contract->contract_id, + 'contract_created_at' => now(), + 'valid_flag' => 0, + 'updated_at' => now(), + ]); 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'], ]); }); @@ -672,35 +768,37 @@ class ShjFourBService /** * 副作用処理実行 - * + * * 決済授受および写真削除、新規連動等の処理 * * @param SettlementTransaction $settlement * @param object $contract * @param array $amountResult * @param array $updateResult + * @param array|null $shjFourCResult SHJ-4C車室割り当て結果(新規のみ) * @return array */ private function executeSideEffects( SettlementTransaction $settlement, $contract, array $amountResult, - array $updateResult + array $updateResult, + ?array $shjFourCResult = null ): 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); - } + // 【JOB3-2】写真削除処理(SQL-4/SQL-5の後、金額比較結果に関わらず実行) + $sideEffects['photo_deletion'] = $this->executePhotoDeletion($contract); + + // 【JOB3-3】写真削除バッチログ(独立SHJ-8呼出し) + $this->createPhotoDeletionBatchLog($sideEffects['photo_deletion']); + + // 【JOB3-STEP1】SHJ-13契約台数追加(新規 かつ shortage以外) + // 仕様:shortage → SHJ-13呼出なし、match/excess → SHJ-13呼出 + if ((int) $contract->update_flag === 2 + && $amountResult['comparison'] !== self::AMOUNT_SHORTAGE) { + $sideEffects['shj13_trigger'] = $this->triggerShjThirteen($contract, $shjFourCResult); } // 【処理4】異常時のオペレーターキュー登録処理 @@ -708,10 +806,8 @@ class ShjFourBService $sideEffects['operator_queue'] = $this->registerToOperatorQueue($settlement, $contract, $amountResult); } - // 【処理5】利用者メール送信処理 - if ($updateResult['updated']) { - $sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult); - } + // 【JOB5】利用者メール送信処理(仕様: フロー到達時は一律送信) + $sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult); Log::info('SHJ-4B 副作用処理完了', [ 'settlement_transaction_id' => $settlement->settlement_transaction_id, @@ -778,20 +874,15 @@ class ShjFourBService 'settlement_transaction_id' => $settlement->settlement_transaction_id, 'contract_id' => $contract->contract_id, 'reason' => $statusResult['reason'], - 'failed_checks' => $statusResult['failed_checks'], ]); - - // TODO: オペレーターキューへの登録や管理者通知 - + return [ - 'success' => false, + 'success' => true, // 授受済みは仕様上エラーではなく正常分岐 'settlement_transaction_id' => $settlement->settlement_transaction_id, 'contract_id' => $contract->contract_id, - 'result' => 'invalid_status', + 'result' => 'already_received', 'reason' => $statusResult['reason'], - 'failed_checks' => $statusResult['failed_checks'], 'message' => $statusResult['message'], - 'action_required' => 'オペレーターによる手動処理が必要です', ]; } @@ -803,47 +894,214 @@ class ShjFourBService */ private function executePhotoDeletion($contract): array { - // TODO: 実際の写真削除ロジックを実装 - // 現在はプレースホルダー - - Log::info('SHJ-4B 写真削除処理実行', [ - 'contract_id' => $contract->contract_id, - 'user_id' => $contract->user_id, + // 【SQL-6】利用者の写真ファイル名取得 + $userData = DB::table('regular_contract as T1') + ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') + ->where('T1.contract_payment_number', $contract->contract_payment_number) + ->where('T1.contract_flag', 1) + ->select('T2.user_id', 'T2.user_seq', 'T2.user_name', 'T2.photo_filename1', 'T2.photo_filename2') + ->first(); + + if (!$userData) { + Log::info('SHJ-4B 写真削除: 対象利用者なし', [ + 'contract_id' => $contract->contract_id, + ]); + return ['executed' => false, 'message' => '対象利用者なし']; + } + + $deletedFiles = []; + + // ファイル削除 + foreach (['photo_filename1', 'photo_filename2'] as $field) { + if (!empty($userData->$field)) { + $path = 'photo/' . $userData->$field; + if (Storage::disk('public')->exists($path)) { + Storage::disk('public')->delete($path); + $deletedFiles[] = $userData->$field; + } + } + } + + // 【SQL-7】利用者の写真ファイル名をNULLに更新 + $userUpdateQuery = DB::table('user'); + if (!empty($userData->user_id)) { + // 仕様書上の利用者ID(user.user_id)で更新 + $userUpdateQuery->where('user_id', $userData->user_id); + } else { + // user_id未設定データの後方互換 + $userUpdateQuery->where('user_seq', $userData->user_seq); + } + $userUpdateQuery->update([ + 'photo_filename1' => null, + 'photo_filename2' => null, + 'updated_at' => now(), ]); - + + Log::info('SHJ-4B 写真削除処理完了', [ + 'user_seq' => $userData->user_seq, + 'deleted_files' => $deletedFiles, + ]); + return [ 'executed' => true, - 'method' => 'placeholder', - 'message' => '写真削除処理は実装予定です', + 'user_id' => $userData->user_id, + 'user_seq' => $userData->user_seq, + 'user_name' => $userData->user_name, + 'photo_filename1' => $userData->photo_filename1, + 'photo_filename2' => $userData->photo_filename2, + 'deleted_files' => $deletedFiles, ]; } + /** + * ウェルネットPUSH処理用バッチログ作成(settlement_transaction未作成時) + * + * processWellnetPush内でJOB1不合格(受付番号不正)の場合に使用 + * + * @param string $statusComment ステータスコメント + * @return void + */ + private function createWellnetPushBatchLog(string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + $this->shjEightService->execute( + $deviceId, + 'SHJ-4B', + 'SHJ-4支払いステータスチェック', + 'success', + $statusComment, + $today, + $today + ); + } catch (\Exception $e) { + Log::error('SHJ-4B ウェルネットPUSHバッチログ作成エラー', [ + 'error' => $e->getMessage(), + ]); + } + } + + /** + * 【JOB3-3】写真削除バッチログ作成(独立SHJ-8呼出し) + * + * @param array $photoDeletionResult executePhotoDeletion()の戻り値 + * @return void + */ + private function createPhotoDeletionBatchLog(array $photoDeletionResult): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + if ($photoDeletionResult['executed'] ?? false) { + $status = 'success'; + $userId = $photoDeletionResult['user_id'] ?? ''; + $userName = $photoDeletionResult['user_name'] ?? ''; + $file1 = $photoDeletionResult['photo_filename1'] ?? ''; + $file2 = $photoDeletionResult['photo_filename2'] ?? ''; + $statusComment = "利用者ID:{$userId}、利用者名:{$userName}、ファイル名:{$file1},{$file2}"; + } else { + $status = 'error'; + $statusComment = $photoDeletionResult['message'] ?? '写真削除対象なし'; + } + + $this->shjEightService->execute( + $deviceId, + 'SHJ-4B', + 'SHJ-4B本人確認写真削除', + $status, + $statusComment, + $today, + $today + ); + } catch (\Exception $e) { + Log::error('SHJ-4B JOB3-3 写真削除バッチログ作成エラー', [ + 'error' => $e->getMessage(), + ]); + } + } + + /** + * 【JOB3-0】SHJ-4C 車室割り当て処理(新規のみ) + * + * 仕様:定期契約継続フラグ = 2(新規)の場合に呼び出す + * ※異常があったとしても、処理は継続する + * + * @param object $contract 契約データ + * @return array|null 処理結果(異常時はnull) + */ + private function triggerShjFourC($contract): ?array + { + Log::info('SHJ-4B SHJ-4C車室割り当て処理実行', [ + 'contract_id' => $contract->contract_id, + 'park_id' => $contract->park_id, + 'ptype_id' => $contract->ptype_id, + 'psection_id' => $contract->psection_id, + ]); + + try { + // SHJ-4Cサービス実行 + $result = $this->shjFourCService->executeRoomAllocation( + (int) $contract->park_id, + (int) $contract->ptype_id, + (int) $contract->psection_id + ); + + Log::info('SHJ-4B SHJ-4C車室割り当て処理完了', [ + 'contract_id' => $contract->contract_id, + 'result' => $result, + ]); + + return $result; + + } catch (\Throwable $e) { + // ※異常があったとしても、処理は継続する + Log::error('SHJ-4B SHJ-4C車室割り当て処理エラー', [ + 'contract_id' => $contract->contract_id, + 'error' => $e->getMessage(), + ]); + + return null; + } + } + /** * SHJ-13実行処理(新規のみ) - * + * * ShjThirteenServiceを使用した契約台数追加処理 + * 仕様:パラメーター4 ゾーンID = 【SHJ-4C.ゾーンID】 * * @param object $contract + * @param array|null $shjFourCResult SHJ-4C車室割り当て結果 * @return array */ - private function triggerShjThirteen($contract): array + private function triggerShjThirteen($contract, ?array $shjFourCResult = null): array { + // 仕様準拠:ゾーンID = SHJ-4C.ゾーンID + $zoneId = ($shjFourCResult && !empty($shjFourCResult['zone_id'])) + ? $shjFourCResult['zone_id'] + : $contract->zone_id; + Log::info('SHJ-4B SHJ-13実行処理', [ 'contract_id' => $contract->contract_id, 'park_id' => $contract->park_id, 'psection_id' => $contract->psection_id, 'ptype_id' => $contract->ptype_id, - 'zone_id' => $contract->zone_id, + 'zone_id' => $zoneId, ]); try { - // 契約データ準備 + // 契約データ準備(仕様:パラメーター4 ゾーンID = SHJ-4C.ゾーンID) $contractData = [ 'contract_id' => $contract->contract_id, 'park_id' => $contract->park_id, 'psection_id' => $contract->psection_id, 'ptype_id' => $contract->ptype_id, - 'zone_id' => $contract->zone_id, + 'zone_id' => $zoneId, ]; // ShjThirteenService実行 @@ -888,7 +1146,7 @@ class ShjFourBService * @param array $amountResult * @return array 処理結果 ['success' => bool, 'mail_status' => string, 'batch_comment_suffix' => string] */ - private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, array $amountResult): array + private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, ?array $amountResult = null): array { try { // 【処理5】利用者マスタよりメールアドレス、予備メールアドレスを取得する @@ -918,7 +1176,7 @@ class ShjFourBService 'user_name' => $user->user_name, 'settlement_transaction_id' => $settlement->settlement_transaction_id, 'mail_address' => $mailAddress, - 'amount_comparison' => $amountResult['comparison'], + 'amount_comparison' => $amountResult['comparison'] ?? 'already_received', ]); // 共通処理「SHJ-7メール送信」を呼び出し @@ -990,19 +1248,33 @@ class ShjFourBService $contract, array $amountResult ): array { - // TODO: OperatorQue モデルを使用したキューへの登録処理を実装 + // 【SQL-9】キュー種別: shortage=8(支払い催促), excess=6(返金処理) + $queClass = $amountResult['comparison'] === self::AMOUNT_SHORTAGE ? 8 : 6; - Log::info('SHJ-4B オペレーターキュー登録処理実行', [ - 'settlement_transaction_id' => $settlement->settlement_transaction_id, + $queId = DB::table('operator_que')->insertGetId([ + 'que_class' => $queClass, + 'user_id' => $contract->user_id, + 'contract_id' => $contract->contract_id, + 'park_id' => $contract->park_id, + 'que_comment' => '収納請求金額照合エラー', + 'que_status' => 1, + 'que_status_comment' => '', + 'work_instructions' => '', + 'operator_id' => 'SHJ-4', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Log::info('SHJ-4B オペレーターキュー登録完了', [ + 'que_id' => $queId, + 'que_class' => $queClass, 'contract_id' => $contract->contract_id, - 'amount_comparison' => $amountResult['comparison'], - 'difference' => $amountResult['difference'], ]); return [ 'registered' => true, - 'method' => 'placeholder', - 'message' => 'オペレーターキュー登録処理は実装予定です', + 'que_id' => $queId, + 'que_class' => $queClass, ]; } @@ -1016,7 +1288,8 @@ class ShjFourBService * @param array|null $amountResult * @param bool $isSuccess * @param string|null $errorMessage - * @param string|null $mailCommentSuffix メール送信結果コメント(例: "/メール正常終了件数:1" or "/メール異常終了件数:1、{error_info}") + * @param string|null $mailCommentSuffix メール送信結果コメント + * @param array|null $photoDeletionResult 写真削除結果(SQL-6情報含む) * @return void */ private function createBatchLog( @@ -1025,7 +1298,9 @@ class ShjFourBService ?array $amountResult = null, bool $isSuccess = true, ?string $errorMessage = null, - ?string $mailCommentSuffix = null + ?string $mailCommentSuffix = null, + ?array $photoDeletionResult = null, + ?string $statusCommentOverride = null ): void { try { $device = Device::orderBy('device_id')->first(); @@ -1033,30 +1308,35 @@ class ShjFourBService $today = now()->format('Y/m/d'); // ステータスコメント生成(内部変数.バッチコメント) - if ($errorMessage) { + if ($statusCommentOverride) { + // 直接指定(授受済み等) + $statusComment = $statusCommentOverride; + } elseif ($errorMessage) { // エラー時 $statusComment = "支払いステータスチェック:エラー(決済トランザクションID:{$settlement->settlement_transaction_id}) - {$errorMessage}"; } elseif ($amountResult) { // 正常処理時 - $contractId = $contract ? $contract->contract_id : 'N/A'; - switch ($amountResult['comparison']) { case self::AMOUNT_MATCH: - $statusComment = "支払いステータスチェック:OK(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、金額一致)"; + // 仕様: OK + 利用者情報 + 写真ファイル名 + $userId = $photoDeletionResult['user_id'] ?? ''; + $userName = $photoDeletionResult['user_name'] ?? ''; + $file1 = $photoDeletionResult['photo_filename1'] ?? ''; + $file2 = $photoDeletionResult['photo_filename2'] ?? ''; + $statusComment = "支払いステータスチェック:OK、(決済トランザクションID:{$settlement->settlement_transaction_id}、利用者ID:{$userId}、利用者名:{$userName}、ファイル名:{$file1},{$file2})"; break; case self::AMOUNT_SHORTAGE: - $statusComment = "支払いステータスチェック:請求金額より授受金額が少ないです(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、差額:{$amountResult['difference']}円)"; + $statusComment = "支払いステータスチェック:請求金額より授受金額が少ないです。(決済トランザクションID:{$settlement->settlement_transaction_id})"; break; case self::AMOUNT_EXCESS: - $statusComment = "支払いステータスチェック:請求金額より授受金額が多いです(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId}、差額:{$amountResult['difference']}円)"; + $statusComment = "支払いステータスチェック:請求金額より授受金額が多いです。(決済トランザクションID:{$settlement->settlement_transaction_id})"; break; default: - $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId})"; + $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id})"; } } else { // その他のケース(対象なし、登録済み等) - $contractId = $contract ? $contract->contract_id : 'N/A'; - $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id}、契約ID:{$contractId})"; + $statusComment = "支払いステータスチェック:処理完了(決済トランザクションID:{$settlement->settlement_transaction_id})"; } // メール送信結果をバッチコメントに追加 @@ -1064,12 +1344,15 @@ class ShjFourBService $statusComment .= $mailCommentSuffix; } - // SHJ-8サービス呼び出し + // 重複判定用マーカー追加(checkAlreadyProcessed/ShjFourBCheckCommand互換) + $statusComment .= " settlement_transaction_id:{$settlement->settlement_transaction_id}"; + + // SHJ-8サービス呼び出し(仕様JOB6: success/error動的判定) $this->shjEightService->execute( $deviceId, 'SHJ-4B', 'SHJ-4支払いステータスチェック', - 'success', + $isSuccess ? 'success' : 'error', $statusComment, $today, $today diff --git a/app/Services/ShjFourCService.php b/app/Services/ShjFourCService.php index c3fed34..8f9762b 100644 --- a/app/Services/ShjFourCService.php +++ b/app/Services/ShjFourCService.php @@ -86,19 +86,24 @@ class ShjFourCService if (empty($zoneInfo)) { $message = '対象のゾーン情報が見つかりません'; - $status = 'error'; - $statusComment = sprintf('エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)', - $message, $parkId, $ptypeId, $psectionId); + // 式様準拠:ステータスは常にsuccess + $status = 'success'; + // 式様準拠:異常情報フォーマット + $statusComment = sprintf( + '車室割り当てNG:車室割り当てできません。/%d/%d/%d', + $parkId, $ptypeId, $psectionId + ); // バッチログ作成 $this->createBatchLog($status, $statusComment); // JOB3: ゾーンID, 車室番号, 異常情報を返却 return [ - 'success' => false, + 'success' => true, + 'message' => $message, 'zone_id' => null, 'pplace_no' => null, - 'error_info' => $message + 'error_info' => $statusComment ]; } @@ -106,12 +111,13 @@ class ShjFourCService $allocationResult = $this->performAllocationJudgment($zoneInfo, $parkId, $ptypeId, $psectionId); if (!$allocationResult['can_allocate']) { - // 割当NGの場合、対象事室番号を設定 - $this->setTargetRoomNumber($allocationResult['target_room_number']); - - $status = 'warning'; - $statusComment = sprintf('割当NG: %s (park_id:%d, ptype_id:%d, psection_id:%d)', - $allocationResult['reason'], $parkId, $ptypeId, $psectionId); + // 式様準拠:ステータスは常にsuccess + $status = 'success'; + // 式様準拠:異常情報フォーマット + $statusComment = sprintf( + '車室割り当てNG:車室割り当てできません。/%d/%d/%d', + $parkId, $ptypeId, $psectionId + ); // バッチログ作成 $this->createBatchLog($status, $statusComment); @@ -119,15 +125,19 @@ class ShjFourCService // JOB3: ゾーンID, 車室番号, 異常情報を返却(割当NG = 空き車室なし) return [ 'success' => true, + 'message' => $allocationResult['reason'], 'zone_id' => null, 'pplace_no' => null, - 'error_info' => $allocationResult['reason'] + 'error_info' => $statusComment ]; } // 【処理2】バッチログ作成 - $statusComment = sprintf('室割当処理完了 (park_id:%d, ptype_id:%d, psection_id:%d, zone_id:%d, pplace_no:%d)', - $parkId, $ptypeId, $psectionId, $allocationResult['zone_id'], $allocationResult['pplace_no']); + // 式様準拠:割り当て情報フォーマット + $statusComment = sprintf( + '車室割り当てOK:ゾーンID:%d/車室番号:%d', + $allocationResult['zone_id'], $allocationResult['pplace_no'] + ); $this->createBatchLog($status, $statusComment); @@ -147,15 +157,19 @@ class ShjFourCService ]; } catch (\Exception $e) { - $errorMessage = 'SHJ-4C 室割当処理でエラーが発生: ' . $e->getMessage(); - $status = 'error'; - $statusComment = sprintf('例外エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)', - $e->getMessage(), $parkId, $ptypeId, $psectionId); + $errorMessage = 'SHJ-4C 車室割り当て処理でエラーが発生: ' . $e->getMessage(); + // 式様準拠:ステータスは常にsuccess + $status = 'success'; + // 式様準拠:異常情報フォーマット + $statusComment = sprintf( + '車室割り当てNG:車室割り当てできません。/%d/%d/%d', + $parkId, $ptypeId, $psectionId + ); // バッチログ作成 $this->createBatchLog($status, $statusComment); - Log::error('SHJ-4C 室割当処理エラー', [ + Log::error('SHJ-4C 車室割り当て処理エラー', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); @@ -163,6 +177,7 @@ class ShjFourCService // JOB3: ゾーンID, 車室番号, 異常情報を返却 return [ 'success' => false, + 'message' => $errorMessage, 'zone_id' => null, 'pplace_no' => null, 'error_info' => $errorMessage @@ -185,10 +200,15 @@ class ShjFourCService $today = now()->format('Y/m/d'); + // SHJ-8の制約(≤255文字)に準拠して切り詰め + if (mb_strlen($statusComment) > 255) { + $statusComment = mb_substr($statusComment, 0, 252) . '...'; + } + Log::info('SHJ-8バッチ処理ログ作成', [ 'device_id' => $deviceId, 'process_name' => 'SHJ-4C', - 'job_name' => 'SHJ-4C室割当', + 'job_name' => 'SHJ-4C車室割り当て', 'status' => $status, 'status_comment' => $statusComment ]); @@ -197,7 +217,7 @@ class ShjFourCService $this->shjEightService->execute( $deviceId, 'SHJ-4C', - 'SHJ-4C室割当', + 'SHJ-4C車室割り当て', $status, $statusComment, $today, @@ -342,7 +362,7 @@ class ShjFourCService return [ 'can_allocate' => false, 'target_room_number' => $targetRoomNumber, - 'reason' => "車室割り当てNG: " . $targetRoomNumber + 'reason' => sprintf('車室割り当てNG:車室割り当てできません。/%s', $targetRoomNumber) ]; } catch (\Exception $e) { @@ -408,7 +428,8 @@ class ShjFourCService */ private function generateTargetRoomNumber(int $parkId, int $ptypeId, int $psectionId): string { - return sprintf('%d_%d_%d', $parkId, $ptypeId, $psectionId); + // 式様準拠:スラッシュ区切り + return sprintf('%d/%d/%d', $parkId, $ptypeId, $psectionId); } /** diff --git a/app/Services/ShjMailSendService.php b/app/Services/ShjMailSendService.php index 8da047f..3f38721 100644 --- a/app/Services/ShjMailSendService.php +++ b/app/Services/ShjMailSendService.php @@ -100,7 +100,7 @@ class ShjMailSendService } // 【処理3】メールを送信する - $mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo); + $mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo, $additionalParams); if (!$mailSendResult['success']) { $errorInfo = $mailSendResult['error_info']; @@ -295,7 +295,12 @@ class ShjMailSendService * @param MailTemplate $templateInfo テンプレート情報 * @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string] */ - private function sendMail(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array + private function sendMail( + string $mailAddress, + string $backupMailAddress, + MailTemplate $templateInfo, + array $additionalParams = [] + ): array { try { // 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス) @@ -305,6 +310,11 @@ class ShjMailSendService $subject = $templateInfo->getSubject() ?? ''; $message = $templateInfo->getText() ?? ''; + // SHJ-5等で渡された追加パラメータをテンプレートへ反映 + if (!empty($additionalParams)) { + [$subject, $message] = $this->replaceTemplateParams($subject, $message, $additionalParams); + } + // 追加ヘッダ設定(BCC、From等) $headers = $this->buildMailHeaders($templateInfo); @@ -363,6 +373,58 @@ class ShjMailSendService } } + /** + * テンプレート文字列のプレースホルダ置換 + * + * 例: + * - %reserve_id% + * - %expiry% + * - {reserve_id} + * - {expiry} + * + * @param string $subject 件名 + * @param string $message 本文 + * @param array $params 置換パラメータ + * @return array [subject, message] + */ + private function replaceTemplateParams(string $subject, string $message, array $params): array + { + $replaced = false; + + foreach ($params as $key => $value) { + $token1 = '%' . $key . '%'; + $token2 = '{' . $key . '}'; + $token3 = '{{' . $key . '}}'; + + $replace = (string) $value; + + $newSubject = str_replace([$token1, $token2, $token3], $replace, $subject); + $newMessage = str_replace([$token1, $token2, $token3], $replace, $message); + + if ($newSubject !== $subject || $newMessage !== $message) { + $replaced = true; + } + + $subject = $newSubject; + $message = $newMessage; + } + + // プレースホルダがテンプレート内に無い場合は末尾へクエリ形式で補完 + if (!$replaced && !empty($params)) { + $pairs = []; + foreach ($params as $key => $value) { + $pairs[] = $key . '=' . $value; + } + $queryString = implode('&', $pairs); + + if ($queryString !== '') { + $message = rtrim($message) . "\n" . $queryString; + } + } + + return [$subject, $message]; + } + /** * メールヘッダを構築 * @@ -663,4 +725,4 @@ class ShjMailSendService ]; } } -} \ No newline at end of file +} diff --git a/database/migrations/2026_02_13_000001_add_pplace_columns_to_zone_table.php b/database/migrations/2026_02_13_000001_add_pplace_columns_to_zone_table.php new file mode 100644 index 0000000..b6a0e6d --- /dev/null +++ b/database/migrations/2026_02_13_000001_add_pplace_columns_to_zone_table.php @@ -0,0 +1,38 @@ +unsignedInteger('zone_pplace_start') + ->default(1) + ->after('zone_sort') + ->comment('車室番号開始'); + // 車室番号終了(NULLの場合は開始番号+許容台数-1) + $table->unsignedInteger('zone_pplace_end') + ->nullable() + ->after('zone_pplace_start') + ->comment('車室番号終了'); + }); + } + + /** + * カラム削除 + */ + public function down(): void + { + Schema::table('zone', function (Blueprint $table) { + $table->dropColumn(['zone_pplace_start', 'zone_pplace_end']); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index d6c30bf..5ccc0b7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -70,11 +70,13 @@ Route::middleware(['api.key'])->group(function () { | API 3 - 決済結果通知(Callback) |-------------------------------------------------------------------------- | -| POST /api/newwipe/callback - Wellnet入金通知受信 +| POST /api/newwipe/callback - Wellnet入金通知受信 +| POST /api/newwipe/callback/shj4a - SHJ-4A ウェルネット収納情報受付 | | 認証: IP白名単のみ(API Key不要) | */ Route::middleware(['wellnet.ip'])->group(function () { Route::match(['get', 'post'], 'newwipe/callback', [PaymentCallbackController::class, 'receive']); + Route::match(['get', 'post'], 'newwipe/callback/shj4a', [PaymentCallbackController::class, 'receiveShj4a']); }); diff --git a/routes/console.php b/routes/console.php index a6cc0f4..bdc5a05 100644 --- a/routes/console.php +++ b/routes/console.php @@ -10,3 +10,6 @@ Artisan::command('inspire', function () { // 支払期限切れチェック(15分毎) Schedule::command('payment:expire')->everyFifteenMinutes(); + +// SHJ-5 駐輪場空きチェック(毎月20日 11:00) +Schedule::command('shj:5')->monthlyOn(20, '11:00');