diff --git a/app/Console/Commands/ShjFourBCheckCommand.php b/app/Console/Commands/ShjFourBCheckCommand.php new file mode 100644 index 0000000..b19c7af --- /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(), + ]; + } +} \ No newline at end of file diff --git a/app/Console/Commands/ShjThreeCommand.php b/app/Console/Commands/ShjThreeCommand.php index 1af4430..dfb03b9 100644 --- a/app/Console/Commands/ShjThreeCommand.php +++ b/app/Console/Commands/ShjThreeCommand.php @@ -106,3 +106,4 @@ class ShjThreeCommand extends Command } } } + diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 4a3c358..0727ba7 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -77,9 +77,13 @@ class LoginController extends Controller */ protected function validateLogin(Request $request) { + // 個別未入力メッセージ(仕様1,2) $request->validate([ - 'ope_id' => 'required|string', // オペレータID(旧システムと同じ) - 'ope_pass' => 'required|string', // オペレータパスワード(旧システムと同じ) + 'ope_id' => 'required|string', + 'ope_pass' => 'required|string', + ], [ + 'ope_id.required' => 'ログインIDが未入力です。', + 'ope_pass.required' => 'パスワードが未入力です。', ]); } @@ -91,6 +95,13 @@ class LoginController extends Controller */ protected function attemptLogin(Request $request) { + // 先にIDのみでオペレータ取得して退職フラグを確認(仕様5-1) + $opeId = $request->input('ope_id'); + $operator = \App\Models\Ope::where('ope_id', $opeId)->first(); + if ($operator && (int)($operator->ope_quit_flag) === 1) { + // 退職扱いは認証失敗と同じメッセージ(仕様5-1 と 3/4 統一表示) + return false; + } return Auth::attempt($this->credentials($request), false); } @@ -118,9 +129,9 @@ class LoginController extends Controller protected function sendLoginResponse(Request $request) { $request->session()->regenerate(); - $this->clearLoginAttempts($request); - + // 仕様5: ログインIDをセッション保持 + $request->session()->put('login_ope_id', $request->input('ope_id')); return redirect()->intended($this->redirectTo); } diff --git a/app/Http/Controllers/Webhook/WellnetController.php b/app/Http/Controllers/Webhook/WellnetController.php new file mode 100644 index 0000000..628e10c --- /dev/null +++ b/app/Http/Controllers/Webhook/WellnetController.php @@ -0,0 +1,427 @@ +getContent(); + $md5Hash = md5($raw); + + // IP白名单检查(如果配置了) + if (!$this->validateClientIp($request->ip())) { + Log::warning('SHJ-4A IP白名单验证失败', [ + 'ip' => $request->ip(), + 'content_length' => strlen($raw), + ]); + return $this->errorResponse('Unauthorized IP', 403); + } + + // 事前にログ記録(サイズ上限に注意) + Log::info('SHJ-4A Wellnet PUSH received', [ + 'length' => strlen($raw), + 'content_type' => $request->header('Content-Type'), + 'ip' => $request->ip(), + 'md5_hash' => $md5Hash, + ]); + + // 共通バッチログ: start + $batch = BatchLog::createBatchLog( + 'shj4a', + BatchLog::STATUS_START, + [ + 'ip' => $request->ip(), + 'content_type' => $request->header('Content-Type'), + 'content_length' => strlen($raw), + 'md5_hash' => $md5Hash, + ], + 'SHJ-4A Wellnet PUSH start' + ); + + try { + // 【処理1】幂等性检查 - MD5重复检查 + $existingByMd5 = SettlementTransaction::where('md5_string', $md5Hash)->first(); + if ($existingByMd5) { + Log::info('SHJ-4A 幂等性: MD5重复检测', [ + 'md5_hash' => $md5Hash, + 'existing_id' => $existingByMd5->settlement_transaction_id, + ]); + + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4A 幂等性: MD5重复,直接返回成功', + 'success_count' => 0, // 幂等返回不计入成功数 + ]); + + return $this->successResponse('処理済み(幂等性)'); + } + + // 【処理2】SOAP/XML解析 + $xml = @simplexml_load_string($raw); + if (!$xml) { + throw new \RuntimeException('Invalid XML/SOAP payload'); + } + + // Body 以下の最初の要素を取得 + $nsBody = $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body ?? null; + $payloadNode = $nsBody ? current($nsBody->children()) : $xml; // SOAPでなければ素のXML想定 + + // XML -> 配列化 + $payloadArray = json_decode(json_encode($payloadNode), true) ?? []; + + // 【処理3】データ抽出と正規化 + $data = $this->extractSettlementData($payloadArray, $md5Hash); + + // 【処理4】必須フィールド検証 + $this->validateRequiredFields($data); + + // 【処理5】複合キー重复检查(contract_payment_number + pay_date + settlement_amount) + $existingByComposite = $this->findExistingByCompositeKey($data); + if ($existingByComposite) { + Log::info('SHJ-4A 幂等性: 複合キー重复検出', [ + 'contract_payment_number' => $data['contract_payment_number'], + 'pay_date' => $data['pay_date'], + 'settlement_amount' => $data['settlement_amount'], + 'existing_id' => $existingByComposite->settlement_transaction_id, + ]); + + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4A 幂等性: 複合キー重复,直接返回成功', + 'success_count' => 0, + ]); + + return $this->successResponse('処理済み(幂等性)'); + } + + // 【処理6】データベース取込と関連処理 + $settlementId = null; + DB::transaction(function() use ($data, $batch, &$settlementId) { + // 決済トランザクション登録 + $settlement = SettlementTransaction::create($data); + $settlementId = $settlement->settlement_transaction_id; + + // 契約テーブルの軽微な更新(SHJ-4Bで正式更新) + RegularContract::where('contract_payment_number', $data['contract_payment_number']) + ->update(['contract_updated_at' => now()]); + + // バッチログ成功更新 + $batch->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => 'SHJ-4A Wellnet PUSH stored successfully', + 'success_count' => 1, + 'parameters' => json_encode([ + 'settlement_transaction_id' => $settlementId, + 'contract_payment_number' => $data['contract_payment_number'], + 'settlement_amount' => $data['settlement_amount'], + ]), + ]); + + Log::info('SHJ-4A 決済トランザクション登録成功', [ + 'settlement_transaction_id' => $settlementId, + 'contract_payment_number' => $data['contract_payment_number'], + 'settlement_amount' => $data['settlement_amount'], + ]); + }); + + // 【処理7】SHJ-4B用キュージョブ投入 + try { + $jobContext = [ + 'contract_payment_number' => $data['contract_payment_number'], + 'settlement_amount' => $data['settlement_amount'], + 'pay_date' => $data['pay_date'], + 'pay_code' => $data['pay_code'], + 'triggered_by' => 'shj4a_webhook', + 'triggered_at' => $startedAt->toISOString(), + ]; + + ProcessSettlementJob::dispatch($settlementId, $jobContext); + + Log::info('SHJ-4A ProcessSettlementJob投入成功', [ + 'settlement_transaction_id' => $settlementId, + 'job_context' => $jobContext, + ]); + + } catch (\Throwable $jobError) { + // キュー投入失敗は警告レベル(メイン処理は成功済み) + Log::warning('SHJ-4A ProcessSettlementJob投入失敗', [ + 'settlement_transaction_id' => $settlementId, + 'error' => $jobError->getMessage(), + 'note' => '兜底巡検で処理される予定', + ]); + } + + return $this->successResponse(); + + } catch (\Throwable $e) { + Log::error('SHJ-4A error', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'md5_hash' => $md5Hash, + ]); + + if (isset($batch)) { + $batch->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => 'SHJ-4A failed: ' . $e->getMessage(), + 'error_details' => $e->getTraceAsString(), + 'error_count' => 1, + ]); + } + + return $this->errorResponse($e->getMessage()); + } + } + + /** + * IP白名单验证 + * + * @param string $clientIp + * @return bool + */ + private function validateClientIp(string $clientIp): bool + { + $whitelist = config('services.wellnet.ip_whitelist', ''); + + if (empty($whitelist)) { + return true; // 白名单为空时不验证 + } + + $allowedIps = array_map('trim', explode(',', $whitelist)); + + foreach ($allowedIps as $allowedIp) { + if (strpos($allowedIp, '/') !== false) { + // CIDR記法対応 + if ($this->ipInRange($clientIp, $allowedIp)) { + return true; + } + } else { + // 直接IP比較 + if ($clientIp === $allowedIp) { + return true; + } + } + } + + return false; + } + + /** + * CIDR範囲でのIP检查 + * + * @param string $ip + * @param string $range + * @return bool + */ + private function ipInRange(string $ip, string $range): bool + { + list($subnet, $bits) = explode('/', $range); + $ip = ip2long($ip); + $subnet = ip2long($subnet); + $mask = -1 << (32 - $bits); + $subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned + return ($ip & $mask) == $subnet; + } + + /** + * 決済データの抽出と正規化 + * + * @param array $payloadArray + * @param string $md5Hash + * @return array + */ + private function extractSettlementData(array $payloadArray, string $md5Hash): array + { + // inData/Result系の取り出し(キー名差異に寛容) + $first = function(array $arr, array $keys, $default = null) { + foreach ($keys as $k) { + if (isset($arr[$k])) return is_array($arr[$k]) ? $arr[$k] : (string)$arr[$k]; + } + return $default; + }; + + $flat = $payloadArray; + // よくある入れ子: { YoyakuNyukin: { inData: {...} } } / { YoyakuNyukinResponse: { YoyakuNyukinResult: {...} } } + foreach (['inData','YoyakuSyunoBarCodeResult','YoyakuNyukinResult','YoyakuSyunoETicketResult'] as $k) { + if (isset($flat[$k]) && is_array($flat[$k])) { $flat = $flat[$k]; } + } + + $data = [ + 'pay_code' => $first($flat, ['NyukinPayCode','SyunoPayCode','BcPayCode']), + 'contract_payment_number' => $first($flat, ['NyukinRecvNum','SyunoRecvNum','RecvNum','contract_payment_number']), + 'corp_code' => $first($flat, ['NyukinCorpCode','SyunoCorpCode','BcCorpCode','CorpCode']), + 'mms_date' => $first($flat, ['NyukinReferDate','SyunoMMSNo','MmsDate']), + 'cvs_code' => $first($flat, ['NyukinCvsCode','CvsCode']), + 'shop_code' => $first($flat, ['NyukinShopCode','ShopCode']), + 'pay_date' => $first($flat, ['NyukinPaidDate','PaidDate']), + 'settlement_amount' => $first($flat, ['NyukinPaidAmount','SyunoPayAmount','PaidAmount']), + 'stamp_flag' => $first($flat, ['NyukinInshiFlag','InshiFlag']), + 'status' => 'received', + 'md5_string' => $md5Hash, + ]; + + // データ正規化処理 + $data = $this->normalizeSettlementData($data); + + return $data; + } + + /** + * 決済データの正規化 + * + * @param array $data + * @return array + */ + private function normalizeSettlementData(array $data): array + { + // 金額を数値化(非負数) + if (!empty($data['settlement_amount'])) { + $amount = preg_replace('/[^\d.]/', '', $data['settlement_amount']); + $data['settlement_amount'] = max(0, (float)$amount); + } else { + $data['settlement_amount'] = null; + } + + // 支払日時の正規化 + if (!empty($data['pay_date'])) { + try { + $data['pay_date'] = Carbon::parse($data['pay_date'])->format('Y-m-d H:i:s'); + } catch (\Throwable $e) { + Log::warning('SHJ-4A 支払日時解析失敗', [ + 'original_pay_date' => $data['pay_date'], + 'error' => $e->getMessage(), + ]); + $data['pay_date'] = null; + } + } + + // 文字列フィールドのトリム + $stringFields = ['pay_code', 'contract_payment_number', 'corp_code', 'mms_date', 'cvs_code', 'shop_code', 'stamp_flag']; + foreach ($stringFields as $field) { + if (isset($data[$field])) { + $data[$field] = trim($data[$field]) ?: null; + } + } + + return $data; + } + + /** + * 必須フィールドの検証 + * + * @param array $data + * @throws \RuntimeException + */ + private function validateRequiredFields(array $data): void + { + // 必須フィールドのチェック + if (empty($data['contract_payment_number'])) { + throw new \RuntimeException('必須フィールドが不足: contract_payment_number (RecvNum)'); + } + + if (!isset($data['settlement_amount']) || $data['settlement_amount'] === null) { + throw new \RuntimeException('必須フィールドが不足: settlement_amount'); + } + + if (empty($data['pay_date'])) { + throw new \RuntimeException('必須フィールドが不足: pay_date'); + } + } + + /** + * 複合キーによる既存レコード検索 + * + * @param array $data + * @return SettlementTransaction|null + */ + private function findExistingByCompositeKey(array $data): ?SettlementTransaction + { + return SettlementTransaction::where('contract_payment_number', $data['contract_payment_number']) + ->where('pay_date', $data['pay_date']) + ->where('settlement_amount', $data['settlement_amount']) + ->first(); + } + + /** + * 成功レスポンスの生成 + * + * @param string $message + * @return \Illuminate\Http\Response + */ + private function successResponse(string $message = '正常処理'): \Illuminate\Http\Response + { + $responseFormat = config('services.wellnet.response_format', 'json'); + + if ($responseFormat === 'soap') { + return $this->soapResponse(0, $message); + } else { + return response()->json(['result' => 0, 'message' => $message]); + } + } + + /** + * エラーレスポンスの生成 + * + * @param string $message + * @param int $httpCode + * @return \Illuminate\Http\Response + */ + private function errorResponse(string $message, int $httpCode = 500): \Illuminate\Http\Response + { + $responseFormat = config('services.wellnet.response_format', 'json'); + + if ($responseFormat === 'soap') { + return $this->soapResponse(1, $message, $httpCode); + } else { + $resultCode = ($httpCode >= 500) ? 1 : 2; // サーバーエラー:1, クライアントエラー:2 + return response()->json(['result' => $resultCode, 'error' => $message], $httpCode); + } + } + + /** + * SOAP形式のレスポンス生成 + * + * @param int $resultCode + * @param string $message + * @param int $httpCode + * @return \Illuminate\Http\Response + */ + private function soapResponse(int $resultCode, string $message, int $httpCode = 200): \Illuminate\Http\Response + { + $soapEnvelope = '' + . '' + . '' + . '' + . '' . htmlspecialchars($resultCode) . '' + . '' . htmlspecialchars($message) . '' + . '' + . '' + . ''; + + return response($soapEnvelope, $httpCode) + ->header('Content-Type', 'text/xml; charset=utf-8'); + } +} + 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/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/bootstrap/app.php b/bootstrap/app.php index 7b162da..8a80778 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,7 +11,12 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware) { - // + // SHJ-4A ウェルネット決済情報受信用エンドポイントのCSRF例外設定 + // 外部システムからのPOSTリクエストのためCSRF保護を無効化 + $middleware->validateCsrfTokens(except: [ + '/shj4a', // SHJ-4A本番用エンドポイント + '/webhook/wellnet', // SHJ-4A開発・デバッグ用エンドポイント + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/config/services.php b/config/services.php index 27a3617..444d2ab 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,11 @@ return [ ], ], + 'wellnet' => [ + 'response_format' => env('WELLNET_WEBHOOK_RESPONSE', 'json'), + 'ip_whitelist' => env('WELLNET_IP_WHITELIST', ''), + 'verify_signature' => env('WELLNET_VERIFY_SIGNATURE', false), + 'signature_header' => env('WELLNET_SIGNATURE_HEADER', 'X-Wellnet-Signature'), + ], + ]; diff --git a/resources/lang/ja/auth.php b/resources/lang/ja/auth.php index 635d02d..e756f08 100644 --- a/resources/lang/ja/auth.php +++ b/resources/lang/ja/auth.php @@ -12,6 +12,6 @@ return [ | */ - 'failed' => '認証情報と一致するレコードがありません。', + 'failed' => 'ログインID・パスワードが不正です。', 'throttle' => 'ログインの試行回数が多すぎます。:seconds 秒後にお試しください。', ]; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 7e2fb3b..6de3d35 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -11,12 +11,12 @@

{{ __('ログインID、パスワードを入力して') }}
{{ __('ログインボタンをクリックしてください') }}

-
+
- +
@@ -27,9 +27,9 @@
@endif
- +
diff --git a/routes/web.php b/routes/web.php index e5edf4c..5537399 100644 --- a/routes/web.php +++ b/routes/web.php @@ -489,3 +489,17 @@ Route::middleware('auth')->group(function () { //kin end }); + +// Wellnet PUSH webhook (SHJ-4A) +// 外部からのウェルネット決済情報受信用エンドポイント + +// 本番用SHJ-4A専用エンドポイント(外部からのPUSH通知受信) +Route::post('/shj4a', [App\Http\Controllers\Webhook\WellnetController::class, 'receive']) + ->name('shj4a.webhook') + ->withoutMiddleware(['auth', 'verified']); // 認証不要、外部アクセス用 + +// 開発・デバッグ用の既存エンドポイント(内部テスト用として保持) +Route::post('/webhook/wellnet', [App\Http\Controllers\Webhook\WellnetController::class, 'receive']) + ->name('wellnet.webhook.receive') + ->withoutMiddleware(['auth', 'verified']); // 認証不要 +