$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.zone_id', 'T1.update_flag', 'T1.reserve_id', 'T1.contract_payment_number', 'T1.contract_payment_day', 'T1.contract_periods', 'T1.contract_periode', 'T1.contract_created_at', 'T1.contract_cancel_flag', 'T2.park_name', 'T4.price_month', 'T4.price' ]) ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') ->join('price_a as T4', function($join) { $join->on('T1.price_parkplaceid', '=', 'T4.price_parkplaceid') ->on('T1.park_id', '=', 'T4.park_id'); // 文档要求の第二条件追加 }) ->where('T1.contract_payment_number', $settlement->contract_payment_number) ->where('T1.contract_cancel_flag', '!=', 1) // 解約されていない ->whereNotNull('T1.contract_flag') // 状態が設定済み ->first(); if (!$contractQuery) { Log::warning('SHJ-4B 判断0: 対象レコードなし', [ 'contract_payment_number' => $settlement->contract_payment_number, 'settlement_transaction_id' => $settlement->settlement_transaction_id, ]); return [ 'found' => false, 'contract' => null, 'reason' => '対象レコードなし', 'message' => "契約番号に一致する有効な定期契約が見つかりません: {$settlement->contract_payment_number}", ]; } // 登録済み判定 $isAlreadyProcessed = $this->checkAlreadyProcessed($contractQuery, $settlement); if ($isAlreadyProcessed['processed']) { Log::info('SHJ-4B 判断0: 登録済み検出', [ 'contract_id' => $contractQuery->contract_id, 'contract_payment_number' => $settlement->contract_payment_number, 'reason' => $isAlreadyProcessed['reason'], ]); return [ 'found' => true, 'contract' => $contractQuery, 'already_processed' => true, 'reason' => '登録済み', 'message' => "この決済は既に処理済みです: " . $isAlreadyProcessed['reason'], ]; } Log::info('SHJ-4B 判断0: 対象契約取得成功', [ 'contract_id' => $contractQuery->contract_id, 'contract_payment_number' => $settlement->contract_payment_number, 'billing_amount' => $contractQuery->billing_amount, 'contract_flag' => $contractQuery->contract_flag, 'park_name' => $contractQuery->park_name, 'price_month' => $contractQuery->price_month, ]); return [ 'found' => true, 'contract' => $contractQuery, 'already_processed' => false, 'reason' => '対象契約取得成功', 'message' => "契約ID {$contractQuery->contract_id} を取得しました", ]; } /** * 登録済み判定 * * 複数の条件で既に処理済みかを判定 * * @param object $contract * @param SettlementTransaction $settlement * @return array */ private function checkAlreadyProcessed($contract, SettlementTransaction $settlement): array { // 条件1: contract_payment_dayが既に設定済みで、今回の支払日以降 if (!empty($contract->contract_payment_day)) { $existingPaymentDate = Carbon::parse($contract->contract_payment_day); $currentPaymentDate = Carbon::parse($settlement->pay_date); if ($existingPaymentDate->gte($currentPaymentDate)) { return [ 'processed' => true, 'reason' => "既に支払日 {$existingPaymentDate->format('Y-m-d')} が設定済み", ]; } } // 条件2: 同一の決済条件(contract_payment_number + pay_date + settlement_amount)が // 既に他のsettlement_transactionで処理済み $existingTransaction = SettlementTransaction::where('contract_payment_number', $settlement->contract_payment_number) ->where('pay_date', $settlement->pay_date) ->where('settlement_amount', $settlement->settlement_amount) ->where('settlement_transaction_id', '!=', $settlement->settlement_transaction_id) ->first(); if ($existingTransaction) { return [ 'processed' => true, 'reason' => "同一条件の決済トランザクション {$existingTransaction->settlement_transaction_id} が既に存在", ]; } // 条件3: 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実行処理(新規のみ) * * ShjThirteenServiceを使用した契約台数追加処理 * * @param object $contract * @return array */ private function triggerShjThirteen($contract): array { Log::info('SHJ-4B SHJ-13実行処理', [ 'contract_id' => $contract->contract_id, 'park_id' => $contract->park_id, 'psection_id' => $contract->psection_id, 'ptype_id' => $contract->ptype_id, 'zone_id' => $contract->zone_id, ]); try { // 契約データ準備 $contractData = [ 'contract_id' => $contract->contract_id, 'park_id' => $contract->park_id, 'psection_id' => $contract->psection_id, 'ptype_id' => $contract->ptype_id, 'zone_id' => $contract->zone_id, ]; // ShjThirteenService実行 $shjThirteenService = app(ShjThirteenService::class); $result = $shjThirteenService->execute($contractData); Log::info('SHJ-4B SHJ-13実行完了', [ 'contract_id' => $contract->contract_id, 'result' => $result, ]); return $result; } catch (\Throwable $e) { Log::error('SHJ-4B SHJ-13実行エラー', [ 'contract_id' => $contract->contract_id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return [ 'result' => 1, 'error_code' => $e->getCode() ?: 1999, 'error_message' => $e->getMessage(), 'stack_trace' => $e->getTraceAsString(), ]; } } /** * 利用者メール送信処理 * * @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' => 'オペレーターキュー登録処理は実装予定です', ]; } }