shjEightService = $shjEightService; } /** * SHJ-9 売上集計処理メイン実行(日次のみ) * * 処理フロー (todo/SHJ-9/SHJ-9.txt): * 【処理1】集計対象を設定する * 【処理2】駐輪場マスタを取得する * 【判断1】取得件数判定 * 【処理3】車種区分毎に算出する * 【判断2】取得判定 * 【処理4】売上集計結果を削除→登録する * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する * * @param string $type 集計種別(daily 固定) * @param string $aggregationDate 集計対象日(YYYY-MM-DD) * @return array 処理結果 */ public function executeEarningsAggregation(string $type, string $aggregationDate): array { $statusComments = []; // 内部変数.ステータスコメント $dataIntegrityIssues = []; // 内部変数.情報不備 try { // 【処理1】集計対象を設定する // パラメーター検証(日付形式チェック) if (!$this->isValidDateFormat($aggregationDate)) { // 日付形式エラー時は【処理5】へ(warning扱い) $statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)"; // 【処理5】オペレータキュー作成 $this->createOperatorQueue($statusComment, null); // SHJ-8 バッチ処理ログ作成 $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); return [ 'success' => true, // 仕様上はwarningで成功扱い 'message' => $statusComment ]; } $targetDate = Carbon::parse($aggregationDate)->format('Y-m-d'); Log::info('SHJ-9 売上集計処理開始', [ 'type' => $type, 'target_date' => $targetDate ]); // 【処理2】駐輪場マスタを取得する $parkInfo = $this->getParkInformation(); // 【判断1】取得件数判定 if (empty($parkInfo)) { $statusComment = '売上集計(日次):駐輪場マスタが存在していません。'; $statusComments[] = $statusComment; // 【処理5】オペレータキュー作成 $this->createOperatorQueue($statusComment, null); // SHJ-8 バッチ処理ログ作成 $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); return [ 'success' => true, 'message' => $statusComment, 'processed_parks' => 0, 'summary_records' => 0 ]; } // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する $summaryRecords = 0; $processedParks = 0; foreach ($parkInfo as $park) { $result = $this->processEarningsForPark($park, $targetDate); $processedParks++; $summaryRecords += $result['summary_records']; // 対象データなしの場合のステータスコメント収集 if (!empty($result['no_data_message'])) { $statusComments[] = $result['no_data_message']; } // 情報不備を収集("なし"でない場合) if ($result['data_integrity_issue'] !== '情報不備:なし') { $dataIntegrityIssues[] = $result['data_integrity_issue']; } } // 最終ステータスコメント生成 $finalStatusComment = "売上集計(日次):対象日={$targetDate}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}"; if (!empty($dataIntegrityIssues)) { $finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues); } // 【処理5】オペレータキュー作成 // ※ 駐輪場単位で既に作成済み(processEarningsForPark内で情報不備検出時に実施) if (!empty($dataIntegrityIssues)) { Log::warning('SHJ-9 情報不備検出', [ 'issues' => $dataIntegrityIssues ]); } // SHJ-8 バッチ処理ログ作成 $this->callShjEight('SHJ-9売上集計(日次)', 'success', $finalStatusComment); Log::info('SHJ-9 売上集計処理完了', [ 'processed_parks' => $processedParks, 'summary_records' => $summaryRecords, 'data_integrity_issues' => count($dataIntegrityIssues), 'no_data_parks' => count($statusComments) ]); return [ 'success' => true, 'message' => 'SHJ-9 売上集計処理が正常に完了しました', 'processed_parks' => $processedParks, 'summary_records' => $summaryRecords, 'data_integrity_issues' => count($dataIntegrityIssues) ]; } catch (\Exception $e) { $errorMessage = '売上集計(日次):エラー発生 - ' . $e->getMessage(); // SHJ-8 バッチ処理ログ作成(エラー時も作成) try { $this->callShjEight('SHJ-9売上集計(日次)', 'error', $errorMessage); } catch (\Exception $shjException) { Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); } Log::error('SHJ-9 売上集計処理エラー', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'message' => $errorMessage, 'details' => $e->getMessage() ]; } } /** * 日付形式の検証 * * @param string $date 日付文字列 * @return bool 有効な日付形式かどうか */ private function isValidDateFormat(string $date): bool { try { $parsed = Carbon::parse($date); return true; } catch (\Exception $e) { return false; } } /** * 【処理2】駐輪場マスタを取得する * * @return array 駐輪場情報 */ private function getParkInformation(): array { try { $parkInfo = DB::table('park') ->select(['park_id', 'park_name']) ->where('park_close_flag', '<>', 1) ->orderBy('park_ruby') ->get() ->toArray(); Log::info('駐輪場マスタ取得完了', [ 'park_count' => count($parkInfo) ]); return $parkInfo; } catch (\Exception $e) { Log::error('駐輪場マスタ取得エラー', [ 'error' => $e->getMessage() ]); throw $e; } } /** * 駐輪場毎の売上集計処理 * * @param object $park 駐輪場情報 * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] */ private function processEarningsForPark($park, string $targetDate): array { try { // 0. 情報不備チェック $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate); // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263) if ($dataIntegrityIssue !== '情報不備:なし') { $this->createOperatorQueue($dataIntegrityIssue, $park->park_id); } // ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) $regularData = $this->calculateRegularEarnings($park->park_id, $targetDate); // ② 一時金データ取得(車種毎) $lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $targetDate); // ③ 解約返戻金データ取得(車種区分毎) $refundData = $this->calculateRefundEarnings($park->park_id, $targetDate); // ④ 再発行データ取得(車種区分毎) $reissueData = $this->calculateReissueCount($park->park_id, $targetDate); // 【判断2】データがいずれかあれば【処理4】へ if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) { // 対象データなし - 仕様 todo/SHJ-9/SHJ-9.txt:172-175 $noDataMessage = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}:売上データが存在しません。"; return [ 'summary_records' => 0, 'data_integrity_issue' => $dataIntegrityIssue, 'no_data_message' => $noDataMessage ]; } // 【処理4】既存の売上集計結果を削除 $this->deleteExistingSummary($park->park_id, $targetDate); // 【処理4】売上集計結果を登録 $summaryRecords = 0; // ① 定期契約データがある場合:同じ組合せ(psection×usertype×months)を統合 $mergedRegularData = $this->mergeRegularDataByGroup($regularData); foreach ($mergedRegularData as $key => $mergedRow) { $this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular'); $summaryRecords++; } // ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約) $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); foreach ($otherDataByPsection as $psectionId => $data) { $this->createEarningsSummary($park, $data, $targetDate, 'other'); $summaryRecords++; } Log::info('駐輪場売上集計完了', [ 'park_id' => $park->park_id, 'park_name' => $park->park_name, 'summary_records' => $summaryRecords, 'data_integrity_issue' => $dataIntegrityIssue ]); return [ 'summary_records' => $summaryRecords, 'data_integrity_issue' => $dataIntegrityIssue, 'no_data_message' => null ]; } catch (\Exception $e) { Log::error('駐輪場売上集計エラー', [ 'park_id' => $park->park_id, 'error' => $e->getMessage() ]); throw $e; } } /** * 0. 情報不備チェック * * 仕様 todo/SHJ-9/SHJ-9.txt:44-68 * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return string 情報不備メッセージ(仕様フォーマット:"情報不備:xxx" or "情報不備:なし") */ private function checkDataIntegrity(int $parkId, string $targetDate): string { $incompleteContracts = DB::table('regular_contract') ->select('contract_id') ->where('park_id', $parkId) ->where('contract_flag', 1) ->whereDate('contract_payment_day', '=', $targetDate) ->where(function($query) { $query->whereNull('update_flag') ->orWhereNull('psection_id') ->orWhereNull('enable_months'); }) ->pluck('contract_id') ->toArray(); if (empty($incompleteContracts)) { return '情報不備:なし'; } // 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り return '情報不備:' . implode(',', $incompleteContracts); } /** * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) * * 仕様 todo/SHJ-9/SHJ-9.txt:70-95 * SQL定義:減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、 * 授受金額の合計と件数を算出する * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return array */ private function calculateRegularEarnings(int $parkId, string $targetDate): array { $results = DB::table('regular_contract as T1') ->join('usertype as T3', 'T1.user_categoryid', '=', 'T3.user_categoryid') ->select([ DB::raw('IFNULL(T1.contract_reduction, 0) as contract_reduction'), 'T1.update_flag', 'T1.psection_id', 'T3.usertype_subject1', 'T1.enable_months', DB::raw('SUM(T1.contract_money) as total_amount'), // 仕様line 77: 授受金額の合計 DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様line 78: 授受件数 ]) ->where('T1.park_id', $parkId) ->where('T1.contract_flag', 1) ->whereDate('T1.contract_payment_day', '=', $targetDate) ->whereNotNull('T1.update_flag') ->whereNotNull('T1.psection_id') ->whereNotNull('T1.enable_months') ->groupBy([ DB::raw('IFNULL(T1.contract_reduction, 0)'), 'T1.update_flag', 'T1.psection_id', 'T3.usertype_subject1', 'T1.enable_months' ]) ->get(); return $results->toArray(); } /** * ② 一時金データ取得(車種毎) * * 仕様 todo/SHJ-9/SHJ-9.txt:114-125 * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return array */ private function calculateLumpsumEarnings(int $parkId, string $targetDate): array { $results = DB::table('lumpsum_transaction') ->select([ 'type_class as psection_id', DB::raw('COUNT(*) as lumpsum_count'), DB::raw('COALESCE(SUM(deposit_amount), 0) as lumpsum') ]) ->where('park_id', $parkId) ->whereDate('pay_date', '=', $targetDate) ->groupBy('type_class') ->get(); return $results->toArray(); } /** * ③ 解約返戻金データ取得(車種区分毎) * * 仕様 todo/SHJ-9/SHJ-9.txt:126-137 * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return array */ private function calculateRefundEarnings(int $parkId, string $targetDate): array { $results = DB::table('regular_contract') ->select([ 'psection_id', DB::raw('COALESCE(SUM(refunds), 0) as refunds') ]) ->where('park_id', $parkId) ->where('contract_cancel_flag', 1) ->whereDate('repayment_at', '=', $targetDate) ->groupBy('psection_id') ->get(); return $results->toArray(); } /** * ④ 再発行データ取得(車種区分毎) * * 仕様 todo/SHJ-9/SHJ-9.txt:138-149 * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return array */ private function calculateReissueCount(int $parkId, string $targetDate): array { $results = DB::table('seal') ->select([ 'psection_id', DB::raw('COUNT(contract_id) as reissue_count') ]) ->where('park_id', $parkId) ->where('contract_seal_issue', '>=', 2) ->whereDate('seal_day', '=', $targetDate) ->groupBy('psection_id') ->get(); return $results->toArray(); } /** * 定期契約データを組合せ毎に統合 * * SQLは contract_reduction × update_flag で分組しているため、 * 同じ psection × usertype × months の組合せで複数行が返る場合がある。 * ここで park_id + psection_id + usertype_subject1 + enable_months をキーに統合し、 * 新規/更新 × 減免/通常 の各件数・金額を1つのオブジェクトに集約する。 * * 仕様 todo/SHJ-9/SHJ-9.txt:96-113 * * @param array $regularData calculateRegularEarnings()の結果 * @return array キー:psection_id|usertype|months、値:統合されたデータオブジェクト */ private function mergeRegularDataByGroup(array $regularData): array { $merged = []; foreach ($regularData as $row) { // 統合キー:psection_id|usertype_subject1|enable_months $key = $row->psection_id . '|' . $row->usertype_subject1 . '|' . $row->enable_months; // 初回作成 if (!isset($merged[$key])) { $merged[$key] = (object)[ 'psection_id' => $row->psection_id, 'usertype_subject1' => $row->usertype_subject1, 'enable_months' => $row->enable_months, // 新規・通常 'regular_new_count' => 0, 'regular_new_amount' => 0, // 新規・減免 'regular_new_reduction_count' => 0, 'regular_new_reduction_amount' => 0, // 更新・通常 'regular_update_count' => 0, 'regular_update_amount' => 0, // 更新・減免 'regular_update_reduction_count' => 0, 'regular_update_reduction_amount' => 0 ]; } // 区分判定 $isNew = in_array($row->update_flag, [2, null]); // 新規 $isReduction = ($row->contract_reduction == 1); // 減免 $count = $row->contract_count ?? 0; $amount = $row->total_amount ?? 0; // 対応するフィールドに累加 if ($isNew && !$isReduction) { // 新規・通常 $merged[$key]->regular_new_count += $count; $merged[$key]->regular_new_amount += $amount; } elseif ($isNew && $isReduction) { // 新規・減免 $merged[$key]->regular_new_reduction_count += $count; $merged[$key]->regular_new_reduction_amount += $amount; } elseif (!$isNew && !$isReduction) { // 更新・通常 $merged[$key]->regular_update_count += $count; $merged[$key]->regular_update_amount += $amount; } elseif (!$isNew && $isReduction) { // 更新・減免 $merged[$key]->regular_update_reduction_count += $count; $merged[$key]->regular_update_reduction_amount += $amount; } } return $merged; } /** * 一時金・解約・再発行データを車種区分毎に統合 * * @param array $lumpsumData * @param array $refundData * @param array $reissueData * @return array */ private function mergeOtherEarningsData(array $lumpsumData, array $refundData, array $reissueData): array { $merged = []; // 一時金 foreach ($lumpsumData as $row) { $psectionId = $row->psection_id; if (!isset($merged[$psectionId])) { $merged[$psectionId] = (object)[ 'psection_id' => $psectionId, 'usertype_subject1' => null, 'enable_months' => 0, 'lumpsum_count' => 0, 'lumpsum' => 0, 'refunds' => 0, 'reissue_count' => 0 ]; } $merged[$psectionId]->lumpsum_count = $row->lumpsum_count; $merged[$psectionId]->lumpsum = $row->lumpsum; } // 解約返戻金 foreach ($refundData as $row) { $psectionId = $row->psection_id; if (!isset($merged[$psectionId])) { $merged[$psectionId] = (object)[ 'psection_id' => $psectionId, 'usertype_subject1' => null, 'enable_months' => 0, 'lumpsum_count' => 0, 'lumpsum' => 0, 'refunds' => 0, 'reissue_count' => 0 ]; } $merged[$psectionId]->refunds = $row->refunds; } // 再発行 foreach ($reissueData as $row) { $psectionId = $row->psection_id; if (!isset($merged[$psectionId])) { $merged[$psectionId] = (object)[ 'psection_id' => $psectionId, 'usertype_subject1' => null, 'enable_months' => 0, 'lumpsum_count' => 0, 'lumpsum' => 0, 'refunds' => 0, 'reissue_count' => 0 ]; } $merged[$psectionId]->reissue_count = $row->reissue_count; } return $merged; } /** * 【処理4】既存の売上集計結果を削除 * * 仕様書のキー:駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数 * 仕様 todo/SHJ-9/SHJ-9.txt:181-189 * * @param int $parkId 駐輪場ID * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return void */ private function deleteExistingSummary(int $parkId, string $targetDate): void { // 仕様書どおり、同一キーの組み合わせで削除 // 日次の場合、集計開始日・終了日はNULL、売上日付で判定 DB::table('earnings_summary') ->where('park_id', $parkId) ->where('summary_type', self::SUMMARY_TYPE_DAILY) ->whereNull('summary_start_date') ->whereNull('summary_end_date') ->where('earnings_date', $targetDate) // psection_id, usertype_subject1, enable_months は // レコードごとに異なるため、ここでは指定しない ->delete(); Log::debug('既存の売上集計結果削除', [ 'park_id' => $parkId, 'target_date' => $targetDate ]); } /** * 売上集計結果を登録 * * 仕様 todo/SHJ-9/SHJ-9.txt:181-247 * * @param object $park 駐輪場情報 * @param object $data 売上データ * @param string $targetDate 集計対象日(YYYY-MM-DD) * @param string $dataType データ種別(regular or other) * @return void */ private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void { $insertData = [ 'park_id' => $park->park_id, 'summary_type' => self::SUMMARY_TYPE_DAILY, // 3 = 日次 'summary_start_date' => null, // 日次は NULL (仕様line 215) 'summary_end_date' => null, // 日次は NULL (仕様line 216) 'earnings_date' => $targetDate, 'psection_id' => $data->psection_id, 'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名 'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数 'summary_note' => "SHJ-9:{$targetDate}", // 仕様line 236 'created_at' => now(), 'updated_at' => now(), 'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 239) ]; if ($dataType === 'regular') { // 定期契約データの場合:mergeRegularDataByGroup()で既に統合済み // 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 98-113) $insertData = array_merge($insertData, [ 'regular_new_count' => $data->regular_new_count ?? 0, 'regular_new_amount' => $data->regular_new_amount ?? 0, 'regular_new_reduction_count' => $data->regular_new_reduction_count ?? 0, 'regular_new_reduction_amount' => $data->regular_new_reduction_amount ?? 0, 'regular_update_count' => $data->regular_update_count ?? 0, 'regular_update_amount' => $data->regular_update_amount ?? 0, 'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0, 'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0, 'lumpsum_count' => 0, // 仕様line 106 'lumpsum' => 0, // 仕様line 107 'refunds' => 0, // 仕様line 108 'other_income' => 0, // 仕様line 109 'other_spending' => 0, // 仕様line 110 'reissue_count' => 0, // 仕様line 111 'reissue_amount' => 0 // 仕様line 112 ]); } else { // 一時金・解約・再発行データの場合:定期フィールドは0固定 (仕様line 152-167) $insertData = array_merge($insertData, [ 'regular_new_count' => 0, // 仕様line 152 'regular_new_amount' => 0, // 仕様line 153 'regular_new_reduction_count' => 0, // 仕様line 154 'regular_new_reduction_amount' => 0, // 仕様line 155 'regular_update_count' => 0, // 仕様line 156 'regular_update_amount' => 0, // 仕様line 157 'regular_update_reduction_count' => 0, // 仕様line 158 'regular_update_reduction_amount' => 0, // 仕様line 159 'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 160 'lumpsum' => $data->lumpsum ?? 0, // 仕様line 161 'refunds' => $data->refunds ?? 0, // 仕様line 162 'other_income' => 0, // 仕様line 163 'other_spending' => 0, // 仕様line 164 'reissue_count' => $data->reissue_count ?? 0, // 仕様line 165 'reissue_amount' => 0 // 仕様line 166: 0固定 ]); } DB::table('earnings_summary')->insert($insertData); Log::debug('売上集計結果登録', [ 'park_id' => $park->park_id, 'psection_id' => $data->psection_id, 'data_type' => $dataType, 'target_date' => $targetDate ]); } /** * 【処理5】オペレータキュー作成(駐輪場単位・情報不備がある場合のみ) * * 仕様 todo/SHJ-9/SHJ-9.txt:253-280 * - que_class: 14(集計対象エラー) * - que_comment: 空文字("") * - que_status: 1(キュー発生) * - que_status_comment: 空文字("") * - work_instructions: 情報不備メッセージ * - park_id: 駐輪場ID(仕様 "処理1.駐輪場ID"、パラメータエラー時はnull) * - operator_id: 9999999(バッチ処理固定値) * * @param string $message 情報不備メッセージ * @param int|null $parkId 駐輪場ID(パラメータエラー時はnull) * @return void */ private function createOperatorQueue(string $message, ?int $parkId = null): void { try { DB::table('operator_que')->insert([ 'que_class' => 14, // 集計対象エラー 'user_id' => null, 'contract_id' => null, 'park_id' => $parkId, // 仕様:処理1.駐輪場ID 'que_comment' => '', // 仕様line 260: "" 'que_status' => 1, // キュー発生 'que_status_comment' => '', // 仕様line 262: "" 'work_instructions' => $message, // 仕様line 263: 情報不備 'operator_id' => self::BATCH_OPERATOR_ID, // 9999999 'created_at' => now(), 'updated_at' => now() ]); Log::info('オペレータキュー作成完了', [ 'park_id' => $parkId, 'que_class' => 14, 'que_status' => 1, 'operator_id' => self::BATCH_OPERATOR_ID, 'work_instructions' => $message ]); } catch (\Exception $e) { Log::error('オペレータキュー作成エラー', [ 'park_id' => $parkId, 'error' => $e->getMessage() ]); } } /** * SHJ-8 バッチ処理ログ作成 * * 仕様 todo/SHJ-9/SHJ-9.txt:289-300 * 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す * * @param string $jobName ジョブ名 * @param string $status ステータス (success/error) * @param string $statusComment 業務固有のステータスコメント * @return void */ private function callShjEight(string $jobName, string $status, string $statusComment): void { try { $device = Device::orderBy('device_id')->first(); $deviceId = $device ? $device->device_id : 1; $today = now()->format('Y/m/d'); $this->shjEightService->execute( $deviceId, 'SHJ-9', $jobName, $status, $statusComment, $today, $today ); Log::info('SHJ-8 バッチ処理ログ作成完了', [ 'job_name' => $jobName, 'status' => $status ]); } catch (\Exception $e) { Log::error('SHJ-8 バッチ処理ログ作成エラー', [ 'error' => $e->getMessage(), 'job_name' => $jobName, 'status_comment' => $statusComment ]); throw $e; } } }