From 9441a34f6f344473e203acf7e139e06af0f67e4c Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Oct 2025 20:32:09 +0900 Subject: [PATCH] =?UTF-8?q?SHJ-9/SHJ-10:=20=E4=BF=AE=E5=A4=8D=E5=AE=9A?= =?UTF-8?q?=E6=9C=9F=E5=A5=91=E7=B4=84=E9=9B=86=E8=A8=88=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=81=AE=E7=B5=B1=E5=90=88=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定期契約データを psectionusertypemonths で統合 - 新規/更新 減免/通常 を1レコードに集約 - Operator Queue に park_id と operator_id を正確に設定 - SQL に contract_money の SUM を追加 --- app/Console/Commands/ShjNineCommand.php | 36 +- app/Models/EarningsSummary.php | 18 +- app/Models/HardwareCheckLog.php | 6 +- app/Models/JurisdictionParking.php | 97 ++ app/Models/Manager.php | 228 +++++ app/Models/PrintJobLog.php | 6 +- app/Models/Setting.php | 85 ++ app/Services/ShjNineService.php | 914 ++++++++++++------ app/Services/ShjSixService.php | 1170 +++++++++++++++++------ app/Services/ShjTenService.php | 899 +++++++++++------ 10 files changed, 2545 insertions(+), 914 deletions(-) create mode 100644 app/Models/JurisdictionParking.php create mode 100644 app/Models/Manager.php create mode 100644 app/Models/Setting.php diff --git a/app/Console/Commands/ShjNineCommand.php b/app/Console/Commands/ShjNineCommand.php index 11222e9..d95871c 100644 --- a/app/Console/Commands/ShjNineCommand.php +++ b/app/Console/Commands/ShjNineCommand.php @@ -9,7 +9,7 @@ use App\Services\ShjNineService; /** * SHJ-9 売上集計処理コマンド * - * 駐輪場の売上データを日次・月次・年次で集計する処理を実行する + * 駐輪場の売上データを日次で集計する処理を実行する * バックグラウンドで実行される定期バッチ処理 */ class ShjNineCommand extends Command @@ -18,19 +18,19 @@ class ShjNineCommand extends Command * コンソールコマンドの名前とシグネチャ * * 引数: - * - type: 集計種別 (daily/monthly/yearly) (必須) + * - type: 集計種別 (daily のみ) (必須) * - target_date: 集計対象日 (オプション、YYYY-MM-DD形式) * * @var string */ - protected $signature = 'shj:9 {type : 集計種別(daily/monthly/yearly)} {target_date? : 集計対象日(YYYY-MM-DD)}'; + protected $signature = 'shj:9 {type : 集計種別(daily)} {target_date? : 集計対象日(YYYY-MM-DD)}'; /** * コンソールコマンドの説明 * * @var string */ - protected $description = 'SHJ-9 売上集計処理 - 日次/月次/年次売上データ集計を実行'; + protected $description = 'SHJ-9 売上集計処理 - 日次売上データ集計を実行'; /** * SHJ-9サービスクラス @@ -138,10 +138,9 @@ class ShjNineCommand extends Command */ private function validateParameters(string $type, ?string $targetDate): bool { - // 集計種別チェック - $allowedTypes = ['daily', 'monthly', 'yearly']; - if (!in_array($type, $allowedTypes)) { - $this->error('集計種別は daily, monthly, yearly のいずれかを指定してください。'); + // 集計種別チェック(SHJ-9 は日次のみ対応) + if ($type !== 'daily') { + $this->error('SHJ-9 は日次集計(daily)のみ対応しています。月次/年次は SHJ-10 を使用してください。'); return false; } @@ -157,7 +156,7 @@ class ShjNineCommand extends Command /** * 集計対象日を決定 * - * @param string $type 集計種別 + * @param string $type 集計種別(daily 固定) * @param string|null $targetDate 指定日 * @return string 集計対象日 */ @@ -167,23 +166,8 @@ class ShjNineCommand extends Command return $targetDate; } - // パラメータ指定がない場合のデフォルト設定 - switch ($type) { - case 'daily': - // 日次:昨日(本日の1日前) - return now()->subDay()->format('Y-m-d'); - - case 'monthly': - // 月次:前月の最終日 - return now()->subMonth()->endOfMonth()->format('Y-m-d'); - - case 'yearly': - // 年次:前年の最終日 - return now()->subYear()->endOfYear()->format('Y-m-d'); - - default: - return now()->subDay()->format('Y-m-d'); - } + // パラメータ指定がない場合は昨日(本日の1日前) + return now()->subDay()->format('Y-m-d'); } /** diff --git a/app/Models/EarningsSummary.php b/app/Models/EarningsSummary.php index c23cda5..5e0c636 100644 --- a/app/Models/EarningsSummary.php +++ b/app/Models/EarningsSummary.php @@ -48,8 +48,8 @@ class EarningsSummary extends Model 'regular_update_amount', // 更新金額 'regular_update_reduction_count', // 更新成免件数 'regular_update_reduction_amount', // 更新成免金額 - 'turnsum_count', // 残金件数 - 'turnsum', // 残金 + 'lumpsum_count', // 一時金件数 + 'lumpsum', // 一時金 'refunds', // 解時返戻金 'other_income', // 分別収入 'other_spending', // 分別支出 @@ -79,8 +79,8 @@ class EarningsSummary extends Model 'regular_update_amount' => 'decimal:2', 'regular_update_reduction_count' => 'integer', 'regular_update_reduction_amount' => 'decimal:2', - 'turnsum_count' => 'integer', - 'turnsum' => 'decimal:2', + 'lumpsum_count' => 'integer', + 'lumpsum' => 'decimal:2', 'refunds' => 'decimal:2', 'other_income' => 'decimal:2', 'other_spending' => 'decimal:2', @@ -175,8 +175,8 @@ class EarningsSummary extends Model SUM(regular_new_amount) as total_new_amount, SUM(regular_update_count) as total_update_count, SUM(regular_update_amount) as total_update_amount, - SUM(turnsum_count) as total_turnsum_count, - SUM(turnsum) as total_turnsum, + SUM(lumpsum_count) as total_lumpsum_count, + SUM(lumpsum) as total_lumpsum, SUM(refunds) as total_refunds, SUM(reissue_count) as total_reissue_count, SUM(reissue_amount) as total_reissue_amount @@ -249,8 +249,8 @@ class EarningsSummary extends Model 'regular_update_amount' => 0.00, 'regular_update_reduction_count' => 0, 'regular_update_reduction_amount' => 0.00, - 'turnsum_count' => 0, - 'turnsum' => 0.00, + 'lumpsum_count' => 0, + 'lumpsum' => 0.00, 'refunds' => 0.00, 'other_income' => 0.00, 'other_spending' => 0.00, @@ -273,7 +273,7 @@ class EarningsSummary extends Model { return $this->regular_new_amount + $this->regular_update_amount + - $this->turnsum + + $this->lumpsum + $this->reissue_amount + $this->other_income - $this->other_spending - diff --git a/app/Models/HardwareCheckLog.php b/app/Models/HardwareCheckLog.php index f9cff6f..63d1d31 100644 --- a/app/Models/HardwareCheckLog.php +++ b/app/Models/HardwareCheckLog.php @@ -24,7 +24,7 @@ class HardwareCheckLog extends Model * * @var string */ - protected $primaryKey = 'log_id'; + protected $primaryKey = 'hardware_check_log_id'; /** * 一括代入可能な属性 @@ -46,7 +46,7 @@ class HardwareCheckLog extends Model * @var array */ protected $casts = [ - 'log_id' => 'integer', + 'hardware_check_log_id' => 'integer', 'device_id' => 'integer', 'status' => 'integer', 'operator_id' => 'integer', @@ -220,7 +220,7 @@ class HardwareCheckLog extends Model { return sprintf( 'HardwareCheckLog[ID:%d, Device:%d, Status:%s, Time:%s]', - $this->log_id, + $this->hardware_check_log_id, $this->device_id, $this->getStatusNameAttribute(), $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' diff --git a/app/Models/JurisdictionParking.php b/app/Models/JurisdictionParking.php new file mode 100644 index 0000000..0d67a37 --- /dev/null +++ b/app/Models/JurisdictionParking.php @@ -0,0 +1,97 @@ + 'integer', + 'ope_id' => 'integer', + 'park_id' => 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * オペレータとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function ope() + { + return $this->belongsTo(Ope::class, 'ope_id', 'ope_id'); + } + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * 指定駐輪場を管轄するオペレータIDリストを取得 + * + * @param int $parkId 駐輪場ID + * @return array オペレータIDの配列 + */ + public static function getOperatorIdsByPark(int $parkId): array + { + return self::where('park_id', $parkId) + ->pluck('ope_id') + ->toArray(); + } +} + diff --git a/app/Models/Manager.php b/app/Models/Manager.php new file mode 100644 index 0000000..1253ac8 --- /dev/null +++ b/app/Models/Manager.php @@ -0,0 +1,228 @@ + 'integer', + 'manager_parkid' => 'integer', + 'manager_device1' => 'integer', + 'manager_device2' => 'integer', + 'manager_alert1' => 'boolean', + 'manager_alert2' => 'boolean', + 'manager_quit_flag' => 'boolean', + 'operator_id' => 'integer', + 'manager_quitday' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 所属駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'manager_parkid', 'park_id'); + } + + /** + * 登録オペレータとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function operator() + { + return $this->belongsTo(Ope::class, 'operator_id', 'ope_id'); + } + + /** + * アクティブな管理者のみを取得するスコープ + * (退職フラグ = 0) + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('manager_quit_flag', 0); + } + + /** + * メールアドレスが設定されている管理者のみを取得するスコープ + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeHasEmail($query) + { + return $query->whereNotNull('manager_mail') + ->where('manager_mail', '!=', ''); + } + + /** + * 指定駐輪場の管理者を取得するスコープ + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $parkId 駐輪場ID + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeByPark($query, int $parkId) + { + return $query->where('manager_parkid', $parkId); + } + + /** + * メール送信対象の駐輪場管理者を取得 + * + * 条件: + * - 退職フラグ = 0(在職中) + * - メールアドレスが設定されている + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getMailTargetManagers() + { + return self::active() + ->hasEmail() + ->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) + ->get() + ->map(function ($manager) { + return [ + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid + ]; + }); + } + + /** + * 指定駐輪場のメール送信対象管理者を取得 + * + * @param int $parkId 駐輪場ID + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getMailTargetManagersByPark(int $parkId) + { + return self::active() + ->hasEmail() + ->byPark($parkId) + ->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) + ->get() + ->map(function ($manager) { + return [ + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid + ]; + }); + } + + /** + * 退職しているかどうかを判定 + * + * @return bool 退職しているかどうか + */ + public function isQuit(): bool + { + return (bool) $this->manager_quit_flag; + } + + /** + * アクティブ(在職中)かどうかを判定 + * + * @return bool 在職中かどうか + */ + public function isActive(): bool + { + return !$this->isQuit(); + } + + /** + * メールアドレスが設定されているかどうかを判定 + * + * @return bool メールアドレスが設定されているかどうか + */ + public function hasEmail(): bool + { + return !empty($this->manager_mail); + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'Manager[ID:%d, Name:%s, Park:%d, Email:%s]', + $this->manager_id, + $this->manager_name, + $this->manager_parkid, + $this->manager_mail ?? 'N/A' + ); + } +} + diff --git a/app/Models/PrintJobLog.php b/app/Models/PrintJobLog.php index d5194b1..4097168 100644 --- a/app/Models/PrintJobLog.php +++ b/app/Models/PrintJobLog.php @@ -24,7 +24,7 @@ class PrintJobLog extends Model * * @var string */ - protected $primaryKey = 'log_id'; + protected $primaryKey = 'job_log_id'; /** * 一括代入可能な属性 @@ -49,7 +49,7 @@ class PrintJobLog extends Model * @var array */ protected $casts = [ - 'log_id' => 'integer', + 'job_log_id' => 'integer', 'park_id' => 'integer', 'user_id' => 'integer', 'contract_id' => 'integer', @@ -242,7 +242,7 @@ class PrintJobLog extends Model { return sprintf( 'PrintJobLog[ID:%d, Process:%s, ErrorCode:%d, Time:%s]', - $this->log_id, + $this->job_log_id, $this->process_name, $this->error_code, $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..e4054e4 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,85 @@ + 'integer', + 'printable_alert_flag' => 'boolean', + 'printable_number' => 'integer', + 'printable_alert_number' => 'integer', + 'printer_keep_alive' => 'integer', + 'operator_id' => 'integer', + 'auto_change_date' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 設定情報を取得(通常はID=1の単一レコード) + * + * @return Setting|null + */ + public static function getSettings(): ?Setting + { + return self::first(); + } +} + diff --git a/app/Services/ShjNineService.php b/app/Services/ShjNineService.php index f5e0957..3b54a86 100644 --- a/app/Services/ShjNineService.php +++ b/app/Services/ShjNineService.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\Park; -use App\Models\RegularContract; use App\Models\EarningsSummary; use App\Models\Psection; use App\Models\Batch\BatchLog; @@ -15,83 +14,32 @@ use Carbon\Carbon; /** * SHJ-9 売上集計処理サービス * - * 日次・月次・年次の売上集計処理を実行するビジネスロジック - * バッチ処理「SHJ-9売上集計」の核となる処理を担当 + * 日次の売上集計処理を実行するビジネスロジック + * バッチ処理「SHJ-9売上集計(日次)」の核となる処理を担当 */ class ShjNineService { /** - * Park モデル - * - * @var Park + * バッチ実行者の固定オペレータID */ - protected $parkModel; + const BATCH_OPERATOR_ID = 9999999; /** - * RegularContract モデル - * - * @var RegularContract + * 日次集計の summary_type 値 */ - protected $contractModel; - - /** - * EarningsSummary モデル - * - * @var EarningsSummary - */ - protected $earningsSummaryModel; - - /** - * Psection モデル - * - * @var Psection - */ - protected $psectionModel; - - /** - * BatchLog モデル - * - * @var BatchLog - */ - protected $batchLogModel; - - /** - * OperatorQue モデル - * - * @var OperatorQue - */ - protected $operatorQueModel; + const SUMMARY_TYPE_DAILY = 3; /** * コンストラクタ - * - * @param Park $parkModel - * @param RegularContract $contractModel - * @param EarningsSummary $earningsSummaryModel - * @param Psection $psectionModel - * @param BatchLog $batchLogModel - * @param OperatorQue $operatorQueModel */ - public function __construct( - Park $parkModel, - RegularContract $contractModel, - EarningsSummary $earningsSummaryModel, - Psection $psectionModel, - BatchLog $batchLogModel, - OperatorQue $operatorQueModel - ) { - $this->parkModel = $parkModel; - $this->contractModel = $contractModel; - $this->earningsSummaryModel = $earningsSummaryModel; - $this->psectionModel = $psectionModel; - $this->batchLogModel = $batchLogModel; - $this->operatorQueModel = $operatorQueModel; + public function __construct() + { } /** - * SHJ-9 売上集計処理メイン実行 + * SHJ-9 売上集計処理メイン実行(日次のみ) * - * 処理フロー: + * 処理フロー (todo/SHJ-9/SHJ-9.txt): * 【処理1】集計対象を設定する * 【処理2】駐輪場マスタを取得する * 【判断1】取得件数判定 @@ -100,36 +48,64 @@ class ShjNineService * 【処理4】売上集計結果を削除→登録する * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する * - * @param string $type 集計種別(daily/monthly/yearly) - * @param string $aggregationDate 集計対象日 + * @param string $type 集計種別(daily 固定) + * @param string $aggregationDate 集計対象日(YYYY-MM-DD) * @return array 処理結果 */ public function executeEarningsAggregation(string $type, string $aggregationDate): array { $batchLogId = null; + $batchLog = null; + $statusComments = []; // 内部変数.ステータスコメント + $dataIntegrityIssues = []; // 内部変数.情報不備 try { // 【処理1】集計対象を設定する - $aggregationTarget = $this->setAggregationTarget($type, $aggregationDate); + // パラメーター検証(日付形式チェック) + if (!$this->isValidDateFormat($aggregationDate)) { + // 日付形式エラー時は【処理5】へ(warning扱い) + $statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)"; + + $batchLog = BatchLog::createBatchLog( + 'shj9', + BatchLog::STATUS_WARNING, + ['type' => $type, 'aggregation_date' => $aggregationDate], + $statusComment + ); + $batchLogId = $batchLog->id; + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($statusComment, $batchLogId); + + // SHJ-8 バッチ処理ログ作成 + $this->createShjBatchLog([ + 'job_name' => 'SHJ-9売上集計(日次)', + 'status' => 'success', + 'status_comment' => $statusComment + ]); + + return [ + 'success' => true, // 仕様上はwarningで成功扱い + 'message' => $statusComment, + 'batch_log_id' => $batchLogId + ]; + } + + $targetDate = Carbon::parse($aggregationDate)->format('Y-m-d'); // バッチ処理開始ログ作成 $batchLog = BatchLog::createBatchLog( 'shj9', BatchLog::STATUS_START, - [ - 'type' => $type, - 'aggregation_date' => $aggregationDate, - 'aggregation_target' => $aggregationTarget - ], - "SHJ-9 売上集計処理開始 ({$type})" + ['type' => $type, 'target_date' => $targetDate], + "SHJ-9 売上集計処理開始(日次)- 対象日: {$targetDate}" ); $batchLogId = $batchLog->id; Log::info('SHJ-9 売上集計処理開始', [ 'batch_log_id' => $batchLogId, 'type' => $type, - 'aggregation_date' => $aggregationDate, - 'aggregation_target' => $aggregationTarget + 'target_date' => $targetDate ]); // 【処理2】駐輪場マスタを取得する @@ -137,22 +113,30 @@ class ShjNineService // 【判断1】取得件数判定 if (empty($parkInfo)) { - $message = '売上集計(' . $this->getTypeLabel($type) . '):駐輪場マスタが存在していません。'; + $statusComment = '売上集計(日次):駐輪場マスタが存在していません。'; + $statusComments[] = $statusComment; // バッチログ更新 $batchLog->update([ 'status' => BatchLog::STATUS_WARNING, 'end_time' => now(), - 'message' => $message, - 'success_count' => 1 // 処理は成功したが対象なし + 'message' => $statusComment, + 'success_count' => 1 ]); - // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($message, $batchLogId); + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($statusComment, $batchLogId); + + // SHJ-8 バッチ処理ログ作成 + $this->createShjBatchLog([ + 'job_name' => 'SHJ-9売上集計(日次)', + 'status' => 'success', + 'status_comment' => $statusComment + ]); return [ 'success' => true, - 'message' => $message, + 'message' => $statusComment, 'processed_parks' => 0, 'summary_records' => 0, 'batch_log_id' => $batchLogId @@ -164,54 +148,61 @@ class ShjNineService $processedParks = 0; foreach ($parkInfo as $park) { - $parkSummaryRecords = $this->processEarningsForPark($park, $aggregationTarget, $type); + $result = $this->processEarningsForPark($park, $targetDate, $batchLogId); - if ($parkSummaryRecords > 0) { $processedParks++; - $summaryRecords += $parkSummaryRecords; + $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']; } } - // 【判断2】取得判定 - if ($summaryRecords === 0) { - $message = '対象なしの結果を設定する(契約)'; - - // バッチログ更新 - $batchLog->update([ - 'status' => BatchLog::STATUS_WARNING, - 'end_time' => now(), - 'message' => $message, - 'success_count' => 1 - ]); - - // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($message, $batchLogId); - - return [ - 'success' => true, - 'message' => $message, - 'processed_parks' => $processedParks, - 'summary_records' => 0, - 'batch_log_id' => $batchLogId - ]; + // 最終ステータスコメント生成 + $finalStatusComment = "SHJ-9 売上集計処理正常完了(日次)- 対象日: {$targetDate}, 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + if (!empty($statusComments)) { + $finalStatusComment .= "\n" . implode("\n", $statusComments); + } + if (!empty($dataIntegrityIssues)) { + $finalStatusComment .= "\n" . implode("\n", $dataIntegrityIssues); } // バッチ処理完了ログ更新 - $completionMessage = "SHJ-9 売上集計処理正常完了 ({$type}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; $batchLog->update([ 'status' => BatchLog::STATUS_SUCCESS, 'end_time' => now(), - 'message' => $completionMessage, + 'message' => $finalStatusComment, 'success_count' => 1 ]); // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($completionMessage, $batchLogId); + // ※ 駐輪場単位で既に作成済み(processEarningsForPark内で情報不備検出時に実施) + if (!empty($dataIntegrityIssues)) { + Log::warning('SHJ-9 情報不備検出', [ + 'batch_log_id' => $batchLogId, + 'issues' => $dataIntegrityIssues + ]); + } + + // SHJ-8 バッチ処理ログ作成 + $this->createShjBatchLog([ + 'job_name' => 'SHJ-9売上集計(日次)', + 'status' => 'success', + 'status_comment' => $finalStatusComment + ]); Log::info('SHJ-9 売上集計処理完了', [ 'batch_log_id' => $batchLogId, 'processed_parks' => $processedParks, - 'summary_records' => $summaryRecords + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), + 'no_data_parks' => count($statusComments) ]); return [ @@ -219,6 +210,7 @@ class ShjNineService 'message' => 'SHJ-9 売上集計処理が正常に完了しました', 'processed_parks' => $processedParks, 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), 'batch_log_id' => $batchLogId ]; @@ -235,6 +227,17 @@ class ShjNineService ]); } + // SHJ-8 バッチ処理ログ作成(エラー時も作成) + try { + $this->createShjBatchLog([ + 'job_name' => 'SHJ-9売上集計(日次)', + 'status' => 'error', + 'status_comment' => $errorMessage + ]); + } catch (\Exception $shjException) { + Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); + } + Log::error('SHJ-9 売上集計処理エラー', [ 'batch_log_id' => $batchLogId, 'exception' => $e->getMessage(), @@ -251,54 +254,23 @@ class ShjNineService } /** - * 【処理1】集計対象を設定する - * - * @param string $type 集計種別 - * @param string $aggregationDate 集計対象日 - * @return array 集計対象情報 + * 日付形式の検証 + * + * @param string $date 日付文字列 + * @return bool 有効な日付形式かどうか */ - private function setAggregationTarget(string $type, string $aggregationDate): array + private function isValidDateFormat(string $date): bool { - $date = Carbon::parse($aggregationDate); - - switch ($type) { - case 'daily': - return [ - 'type' => 'daily', - 'start_date' => $date->format('Y-m-d'), - 'end_date' => $date->format('Y-m-d'), - 'summary_type' => '日次' - ]; - - case 'monthly': - return [ - 'type' => 'monthly', - 'start_date' => $date->startOfMonth()->format('Y-m-d'), - 'end_date' => $date->endOfMonth()->format('Y-m-d'), - 'summary_type' => '月次' - ]; - - case 'yearly': - return [ - 'type' => 'yearly', - 'start_date' => $date->startOfYear()->format('Y-m-d'), - 'end_date' => $date->endOfYear()->format('Y-m-d'), - 'summary_type' => '年次' - ]; - - default: - throw new \InvalidArgumentException("不正な集計種別: {$type}"); + try { + $parsed = Carbon::parse($date); + return true; + } catch (\Exception $e) { + return false; } } /** * 【処理2】駐輪場マスタを取得する - * - * 仕様書のSQLクエリに基づく駐輪場情報取得 - * SELECT 駐輪場ID, 駐輪場名 - * FROM 駐輪場マスタ - * WHERE 閉設フラグ <> 1 - * ORDER BY 駐輪場ふりがな * * @return array 駐輪場情報 */ @@ -331,41 +303,77 @@ class ShjNineService * 駐輪場毎の売上集計処理 * * @param object $park 駐輪場情報 - * @param array $aggregationTarget 集計対象 - * @param string $type 集計種別 - * @return int 作成された集計レコード数 + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @param int $batchLogId バッチログID + * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] */ - private function processEarningsForPark($park, array $aggregationTarget, string $type): int + private function processEarningsForPark($park, string $targetDate, int $batchLogId): array { try { - // 【処理4】既存の売上集計結果を削除 - $this->deleteExistingSummary($park->park_id, $aggregationTarget); + // 0. 情報不備チェック + $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate); - // 【処理3】車種区分毎に算出する - $psections = $this->getPsectionInformation(); + // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263) + if ($dataIntegrityIssue !== '情報不備:なし') { + $this->createOperatorQueue($dataIntegrityIssue, $batchLogId, $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; - foreach ($psections as $psection) { - $earningsData = $this->calculateEarningsForPsection( - $park->park_id, - $psection->psection_id, - $aggregationTarget - ); - - if ($this->hasEarningsData($earningsData)) { - // 売上集計結果を登録 - $this->createEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $type); + // ① 定期契約データがある場合:同じ組合せ(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 + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue ]); - return $summaryRecords; + return [ + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => null + ]; } catch (\Exception $e) { Log::error('駐輪場売上集計エラー', [ @@ -378,218 +386,514 @@ class ShjNineService } /** - * 車種区分情報取得 + * 0. 情報不備チェック * - * @return array 車種区分情報 + * 仕様 todo/SHJ-9/SHJ-9.txt:44-68 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return string 情報不備メッセージ(仕様フォーマット:"情報不備:xxx" or "情報不備:なし") */ - private function getPsectionInformation(): array + private function checkDataIntegrity(int $parkId, string $targetDate): string { - return DB::table('psection') - ->select(['psection_id', 'psection_subject']) - ->get() + $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); } /** - * 【処理3】車種区分毎に売上を算出する + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) * - * 4つの項目を計算: - * ①売上・件数 - * ②一時金売上 - * ③解約返戻金 - * ④再発行金額・件数 + * 仕様 todo/SHJ-9/SHJ-9.txt:70-95 + * SQL定義:減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、 + * 授受金額の合計と件数を算出する * * @param int $parkId 駐輪場ID - * @param int $psectionId 車種区分ID - * @param array $aggregationTarget 集計対象 - * @return array 売上データ + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array */ - private function calculateEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + private function calculateRegularEarnings(int $parkId, string $targetDate): array { - $startDate = $aggregationTarget['start_date']; - $endDate = $aggregationTarget['end_date']; + $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(); - // ①売上・件数(billing_amount) - $salesData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as sales_count'), - DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('contract_flag', 1) - ->whereBetween('contract_payment_day', [$startDate, $endDate]) - ->whereNull('contract_cancelday') - ->first(); - - // ②一時金売上(contract_money) - $temporaryData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as temporary_count'), - DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('contract_flag', 1) - ->whereBetween('contract_payment_day', [$startDate, $endDate]) - ->whereNotNull('contract_money') - ->where('contract_money', '>', 0) - ->first(); - - // ③解約返戻金(refunds) - $refundData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as refund_count'), - DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->whereBetween('contract_cancelday', [$startDate, $endDate]) - ->whereNotNull('refunds') - ->where('refunds', '>', 0) - ->first(); - - // ④再発行金額・件数(seal_reissue_request) - $reissueData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as reissue_count'), - DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('seal_reissue_request', 1) - ->whereBetween('updated_at', [$startDate, $endDate]) - ->first(); - - return [ - 'sales_count' => $salesData->sales_count ?? 0, - 'sales_amount' => $salesData->sales_amount ?? 0, - 'temporary_count' => $temporaryData->temporary_count ?? 0, - 'temporary_amount' => $temporaryData->temporary_amount ?? 0, - 'refund_count' => $refundData->refund_count ?? 0, - 'refund_amount' => $refundData->refund_amount ?? 0, - 'reissue_count' => $reissueData->reissue_count ?? 0, - 'reissue_amount' => $reissueData->reissue_amount ?? 0 - ]; + return $results->toArray(); } /** - * 売上データの存在チェック + * ② 一時金データ取得(車種毎) * - * @param array $earningsData 売上データ - * @return bool データが存在するかどうか + * 仕様 todo/SHJ-9/SHJ-9.txt:114-125 + * + * @param int $parkId 駐輪場ID + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @return array */ - private function hasEarningsData(array $earningsData): bool + private function calculateLumpsumEarnings(int $parkId, string $targetDate): array { - return $earningsData['sales_count'] > 0 || - $earningsData['temporary_count'] > 0 || - $earningsData['refund_count'] > 0 || - $earningsData['reissue_count'] > 0; + $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 array $aggregationTarget 集計対象 + * @param string $targetDate 集計対象日(YYYY-MM-DD) * @return void */ - private function deleteExistingSummary(int $parkId, array $aggregationTarget): void + private function deleteExistingSummary(int $parkId, string $targetDate): void { + // 仕様書どおり、同一キーの組み合わせで削除 + // 日次の場合、集計開始日・終了日はNULL、売上日付で判定 DB::table('earnings_summary') ->where('park_id', $parkId) - ->where('summary_start_date', $aggregationTarget['start_date']) - ->where('summary_end_date', $aggregationTarget['end_date']) + ->where('summary_type', 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 $psection 車種区分情報 - * @param array $aggregationTarget 集計対象 - * @param array $earningsData 売上データ - * @param string $type 集計種別 + * @param object $data 売上データ + * @param string $targetDate 集計対象日(YYYY-MM-DD) + * @param string $dataType データ種別(regular or other) * @return void */ - private function createEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, string $type): void + private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void { - DB::table('earnings_summary')->insert([ + $insertData = [ 'park_id' => $park->park_id, - 'summary_type' => $aggregationTarget['summary_type'], - 'summary_start_date' => $aggregationTarget['start_date'], - 'summary_end_date' => $aggregationTarget['end_date'], - 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 - 'psection_id' => $psection->psection_id, - 'usertype_subject1' => $psection->psection_subject, - 'regular_new_count' => $earningsData['sales_count'], - 'regular_new_amount' => $earningsData['sales_amount'], - 'turnsum' => $earningsData['temporary_amount'], - 'turnsum_count' => $earningsData['temporary_count'], - 'refunds' => $earningsData['refund_amount'], - 'reissue_count' => $earningsData['reissue_count'], - 'reissue_amount' => $earningsData['reissue_amount'], - 'summary_note' => "SHJ-9 {$type} 売上集計結果", + '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' => 0 // システム処理 + '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】オペレータキュー作成 + * 【処理5】オペレータキュー作成(駐輪場単位・情報不備がある場合のみ) * - * @param string $message メッセージ + * 仕様 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 $batchLogId バッチログID + * @param int|null $parkId 駐輪場ID(パラメータエラー時はnull) * @return void */ - private function createOperatorQueue(string $message, int $batchLogId): void + private function createOperatorQueue(string $message, int $batchLogId, ?int $parkId = null): void { try { DB::table('operator_que')->insert([ - 'que_class' => 9, // SHJ-9用のクラス + 'que_class' => 14, // 集計対象エラー 'user_id' => null, 'contract_id' => null, - 'park_id' => null, - 'que_comment' => $message, - 'que_status' => 1, // 完了 - 'que_status_comment' => 'バッチ処理完了', - 'work_instructions' => "SHJ-9売上集計処理 BatchLogID: {$batchLogId}", + '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('オペレータキュー作成完了', [ 'batch_log_id' => $batchLogId, - 'message' => $message + 'park_id' => $parkId, + 'que_class' => 14, + 'que_status' => 1, + 'operator_id' => self::BATCH_OPERATOR_ID, + 'work_instructions' => $message ]); } catch (\Exception $e) { Log::error('オペレータキュー作成エラー', [ 'batch_log_id' => $batchLogId, + 'park_id' => $parkId, 'error' => $e->getMessage() ]); } } /** - * 集計種別のラベル取得 + * SHJ-8 バッチ処理ログ作成 * - * @param string $type 集計種別 - * @return string ラベル + * 仕様 todo/SHJ-9/SHJ-9.txt:289-300 + * 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す + * + * @param array $statistics 処理統計情報 + * @return void */ - private function getTypeLabel(string $type): string + private function createShjBatchLog(array $statistics): void { - switch ($type) { - case 'daily': - return '日次'; - case 'monthly': - return '月次'; - case 'yearly': - return '年次'; - default: - return $type; + try { + // SHJ-8 パラメータ設定 + $deviceId = 9999999; // バッチ処理用固定デバイスID + $processName = 'SHJ-9'; + $jobName = $statistics['job_name']; + $status = $statistics['status']; + $statusComment = $statistics['status_comment'] ?? ''; + + $createdDate = now()->format('Y/m/d'); + $updatedDate = now()->format('Y/m/d'); + + Log::info('SHJ-8 バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // 共通処理 SHJ-8 バッチ処理ログ作成を呼び出し + // BatchLog システムを使用してバッチ処理の実行ログを記録 + BatchLog::createBatchLog( + $processName, + $status, + [ + 'device_id' => $deviceId, + 'job_name' => $jobName, + 'status_comment' => $statusComment, + 'statistics' => $statistics, + 'shj8_params' => [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ] + ], + $statusComment + ); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'statistics' => $statistics + ]); + + // SHJ-8でエラーが発生してもメイン処理は継続 + // エラーログのみ出力 } } } diff --git a/app/Services/ShjSixService.php b/app/Services/ShjSixService.php index 9ec63af..f1f7dd6 100644 --- a/app/Services/ShjSixService.php +++ b/app/Services/ShjSixService.php @@ -7,8 +7,10 @@ use App\Models\HardwareCheckLog; use App\Models\PrintJobLog; use App\Models\Batch\BatchLog; use App\Models\OperatorQue; -use App\Models\User; -use App\Models\Park; +use App\Models\Ope; +use App\Models\Manager; +use App\Models\Setting; +use App\Models\JurisdictionParking; use App\Services\ShjMailSendService; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\DB; @@ -22,41 +24,6 @@ use Carbon\Carbon; */ class ShjSixService { - /** - * Device モデル - * - * @var Device - */ - protected $deviceModel; - - /** - * HardwareCheckLog モデル - * - * @var HardwareCheckLog - */ - protected $hardwareCheckLogModel; - - /** - * PrintJobLog モデル - * - * @var PrintJobLog - */ - protected $printJobLogModel; - - /** - * BatchLog モデル - * - * @var BatchLog - */ - protected $batchLogModel; - - /** - * OperatorQue モデル - * - * @var OperatorQue - */ - protected $operatorQueModel; - /** * ShjMailSendService * @@ -69,38 +36,47 @@ class ShjSixService * * @var string */ - const FIXED_EMAIL_ADDRESS = 'system-alert@so-manager.com'; + const FIXED_EMAIL_ADDRESS = 'test@dev-so-manager.com'; /** - * プリンタログ監視期間(分) + * 監視期間(分) * * @var int */ - const PRINTER_LOG_MONITOR_MINUTES = 15; + const MONITOR_PERIOD_MINUTES = 15; + + /** + * システム監視警報用メールテンプレートID + * + * @var int + */ + const SYSTEM_ALERT_MAIL_TEMPLATE_ID = 202; + + /** + * キュー種別定数 + */ + const QUE_CLASS_SERVER_ERROR = 101; // サーバーエラー + const QUE_CLASS_PRINTER_ERROR = 102; // プリンタエラー + const QUE_CLASS_SCANNER_ERROR = 103; // スキャナエラー + const QUE_CLASS_PAPER_WARNING = 104; // プリンタ用紙残少警報 + + /** + * デバイス種別:サーバー + */ + const DEVICE_TYPE_SERVER = 'サーバー'; + + /** + * デバイス稼働状態:稼働中 + */ + const DEVICE_WORK_ACTIVE = '1'; /** * コンストラクタ * - * @param Device $deviceModel - * @param HardwareCheckLog $hardwareCheckLogModel - * @param PrintJobLog $printJobLogModel - * @param BatchLog $batchLogModel - * @param OperatorQue $operatorQueModel * @param ShjMailSendService $mailSendService */ - public function __construct( - Device $deviceModel, - HardwareCheckLog $hardwareCheckLogModel, - PrintJobLog $printJobLogModel, - BatchLog $batchLogModel, - OperatorQue $operatorQueModel, - ShjMailSendService $mailSendService - ) { - $this->deviceModel = $deviceModel; - $this->hardwareCheckLogModel = $hardwareCheckLogModel; - $this->printJobLogModel = $printJobLogModel; - $this->batchLogModel = $batchLogModel; - $this->operatorQueModel = $operatorQueModel; + public function __construct(ShjMailSendService $mailSendService) + { $this->mailSendService = $mailSendService; } @@ -112,7 +88,6 @@ class ShjSixService * 【処理2】デバイス管理マスタを取得する * 【処理3】デバイス毎のハードウェア状態を取得する * 【処理4】プリンタ制御プログラムログを取得する - * 【判断3】エラーログ有無 * 【処理5】バッチ処理ログを作成する * ※ 異常時は共通A処理を実行 * @@ -123,6 +98,12 @@ class ShjSixService $batchLogId = null; $warnings = []; $errorDetails = []; + $alertCount = 0; + $totalMailSuccessCount = 0; + $totalMailErrorCount = 0; + $totalQueueSuccessCount = 0; // キュー登録正常終了件数(仕様書準拠) + $totalQueueErrorCount = 0; // キュー登録異常終了件数(仕様書準拠) + $accumulatedBatchComment = ''; // 累積バッチコメント try { // バッチ処理開始ログ作成 @@ -139,72 +120,173 @@ class ShjSixService ]); // 【処理1】サーバ死活監視(DBアクセス) - $dbAccessResult = $this->checkDatabaseAccess(); + $dbAccessResult = $this->checkDatabaseAccessWithSettings(); + if (!$dbAccessResult['success']) { - // DB接続NGの場合は共通A処理実行 - $this->executeCommonProcessA($batchLogId, 'DB接続エラー: ' . $dbAccessResult['message']); + // DB接続NGの場合は共通A処理実行(キュー種別:101、DB登録可否:0) + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + 'DB接続エラー: ' . $dbAccessResult['message'], + self::QUE_CLASS_SERVER_ERROR, + null, // park_id + null, // user_id + null, // contract_id + 0, // DB登録可否=0(登録不可) + 'サーバ死活監視:DB接続異常検出' + ); + + // メール送信統計を集計 + $totalMailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $commonAResult['mail_error_count'] ?? 0; + $accumulatedBatchComment = $commonAResult['updated_batch_comment'] ?? ''; return [ 'success' => false, 'message' => 'データベース接続エラーが発生しました', 'error_details' => [$dbAccessResult['message']], - 'batch_log_id' => $batchLogId + 'batch_log_id' => $batchLogId, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount ]; } + // DB登録可否フラグを取得 + $dbRegisterFlag = $dbAccessResult['db_register_flag']; + // 【処理2】デバイス管理マスタを取得する - $devices = $this->getDeviceManagementData(); - Log::info('デバイス管理マスタ取得完了', [ + $devices = $this->getServerDevices(); + Log::info('サーバーデバイス取得完了', [ 'device_count' => count($devices) ]); // 【処理3】デバイス毎のハードウェア状態を取得する - $hardwareStatusResult = $this->getHardwareStatus($devices); - if (!$hardwareStatusResult['success']) { - // ハードウェア状態取得できなかった場合は共通A処理実行 - $this->executeCommonProcessA($batchLogId, 'ハードウェア状態取得エラー: ' . $hardwareStatusResult['message']); - $warnings[] = 'ハードウェア状態の一部で異常を検出しました'; - $errorDetails[] = $hardwareStatusResult['message']; + foreach ($devices as $device) { + $hardwareResult = $this->checkDeviceHardwareStatus($device, $batchLogId, $dbRegisterFlag); + if ($hardwareResult['has_alert']) { + $alertCount++; + $warnings[] = "デバイスID {$device->device_id}: ハードウェア異常検出"; + + // 仕様書準拠:メール送信統計とキュー登録統計を集計 + $totalMailSuccessCount += $hardwareResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $hardwareResult['mail_error_count'] ?? 0; + $totalQueueSuccessCount += $hardwareResult['queue_success_count'] ?? 0; + $totalQueueErrorCount += $hardwareResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($hardwareResult['updated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $hardwareResult['updated_batch_comment']; + } + } } // 【処理4】プリンタ制御プログラムログを取得する - $printerLogResult = $this->getPrinterControlLogs(); - - // 【判断3】エラーログ有無 - if ($printerLogResult['has_errors']) { - // エラーログ有の場合は共通A処理実行 - $this->executeCommonProcessA($batchLogId, 'プリンタエラーログ検出: ' . $printerLogResult['error_summary']); - $warnings[] = 'プリンタ制御でエラーが検出されました'; - $errorDetails[] = $printerLogResult['error_summary']; + $printerResult = $this->checkPrinterErrorLogs($batchLogId, $dbRegisterFlag); + if ($printerResult['error_count'] > 0) { + $alertCount += $printerResult['error_count']; + $warnings[] = "プリンタエラー {$printerResult['error_count']}件検出"; + + // 仕様書準拠:メール送信統計とキュー登録統計を集計 + $totalMailSuccessCount += $printerResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $printerResult['mail_error_count'] ?? 0; + $totalQueueSuccessCount += $printerResult['queue_success_count'] ?? 0; + $totalQueueErrorCount += $printerResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($printerResult['accumulated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $printerResult['accumulated_batch_comment']; + } } - // 【処理5】バッチ処理ログを作成する - $monitoringSummary = $this->createMonitoringSummary($devices, $hardwareStatusResult, $printerLogResult); + // 【処理5】バッチ処理ログを作成する - SHJ-8呼び出し + // 仕様書準拠:キュー登録件数も含める + $statusComment = sprintf( + 'デバイス数: %d, アラート件数: %d, メール正常: %d件, メール異常: %d件, キュー登録正常: %d件, キュー登録異常: %d件', + count($devices), + $alertCount, + $totalMailSuccessCount, + $totalMailErrorCount, + $totalQueueSuccessCount, + $totalQueueErrorCount + ); + + // バッチコメントが累積されている場合は追加 + if (!empty($accumulatedBatchComment)) { + $statusComment .= ' | ' . $accumulatedBatchComment; + } $status = empty($warnings) ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_WARNING; $message = empty($warnings) ? 'SHJ-6 サーバ死活監視処理正常完了' : 'SHJ-6 サーバ死活監視処理完了(警告あり)'; + // 仕様書準拠:処理2で取得したデバイスIDを連結 + $deviceIds = array_map(function($device) { + return $device->device_id; + }, $devices); + $concatenatedDeviceIds = !empty($deviceIds) ? implode(',', $deviceIds) : ''; + + // SHJ-8 バッチ処理ログ作成(仕様書準拠) + // device_id = 処理2で取得したデバイスID(複数なら連結) + // process_name = 処理4のプロセス名(プリンタログから取得、なければ'SHJ-6') + // job_name = "SHJ-6サーバ死活監視" 固定 + // status = 常に "success" + $shj8Result = $this->createShjBatchLog([ + 'device_id' => $concatenatedDeviceIds, // 処理2のデバイスID(連結) + 'process_name' => $printerResult['process_name'] ?? 'SHJ-6', // 処理4のプロセス名 + 'job_name' => 'SHJ-6サーバ死活監視', + 'status' => 'success', // 仕様書:常にsuccess + 'status_comment' => $statusComment, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount, + 'queue_success_count' => $totalQueueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $totalQueueErrorCount, // 仕様書準拠 + 'device_count' => count($devices), + 'alert_count' => $alertCount + ]); + + // 仕様書準拠:SHJ-8の戻り値確認(処理結果 = 0以外なら異常) + if (!$shj8Result['success']) { + $shj8Error = $shj8Result['error'] ?? 'Unknown error'; + Log::warning('SHJ-8 バッチ処理ログ作成で異常が発生しました', [ + 'error' => $shj8Error + ]); + + // 仕様書準拠:異常時はバッチコメントへ反映 + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + sprintf('SHJ-8異常: %s', $shj8Error); + + // statusCommentも更新 + $statusComment .= sprintf(' | SHJ-8異常: %s', $shj8Error); + } + + // 既存のバッチログも更新 $batchLog->update([ - 'status' => $status, + 'status' => empty($warnings) ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_WARNING, 'end_time' => now(), 'message' => $message, - 'success_count' => 1 + 'success_count' => $totalMailSuccessCount, + 'error_count' => $totalMailErrorCount ]); Log::info('SHJ-6 サーバ死活監視処理完了', [ 'batch_log_id' => $batchLogId, - 'monitoring_summary' => $monitoringSummary, - 'warnings' => $warnings + 'status_comment' => $statusComment, + 'warnings' => $warnings, + 'alert_count' => $alertCount, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount ]); return [ 'success' => true, 'message' => 'SHJ-6 サーバ死活監視処理が完了しました', - 'monitoring_summary' => $monitoringSummary, + 'status_comment' => $statusComment, 'warnings' => $warnings, - 'error_details' => $errorDetails, + 'alert_count' => $alertCount, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount, 'batch_log_id' => $batchLogId ]; @@ -217,12 +299,25 @@ class ShjSixService 'end_time' => now(), 'message' => $errorMessage, 'error_details' => $e->getMessage(), - 'error_count' => 1 + 'success_count' => $totalMailSuccessCount, + 'error_count' => $totalMailErrorCount + 1 ]); } - // 例外発生時も共通A処理実行 - $this->executeCommonProcessA($batchLogId, $errorMessage); + // 例外発生時も共通A処理実行(キュー種別:101、DB登録可否:0) + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $errorMessage, + self::QUE_CLASS_SERVER_ERROR, + null, + null, + null, + 0, + 'サーバ死活監視:システムエラー発生' + ); + + $totalMailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $totalMailErrorCount += $commonAResult['mail_error_count'] ?? 0; Log::error('SHJ-6 サーバ死活監視処理エラー', [ 'batch_log_id' => $batchLogId, @@ -234,34 +329,40 @@ class ShjSixService 'success' => false, 'message' => $errorMessage, 'error_details' => [$e->getMessage()], - 'batch_log_id' => $batchLogId + 'batch_log_id' => $batchLogId, + 'mail_success_count' => $totalMailSuccessCount, + 'mail_error_count' => $totalMailErrorCount ]; } } /** - * 【処理1】サーバ死活監視(DBアクセス) + * 【処理1】サーバ死活監視(DBアクセス)- 設定マスタを使用 * * @return array アクセス結果 */ - private function checkDatabaseAccess(): array + private function checkDatabaseAccessWithSettings(): array { try { - // 設定マスタテーブルへの簡単なクエリでDB接続確認 - $result = DB::select('SELECT 1 as test'); + // 設定マスタを取得してDB接続確認 + $setting = Setting::getSettings(); - if (empty($result)) { + if (!$setting) { return [ 'success' => false, - 'message' => 'データベースクエリの結果が空です' + 'message' => '設定マスタが取得できませんでした', + 'db_register_flag' => 0 ]; } - Log::info('データベース接続確認成功'); + Log::info('データベース接続確認成功(設定マスタ取得)', [ + 'setting_id' => $setting->setting_id + ]); return [ 'success' => true, - 'message' => 'データベース接続正常' + 'message' => 'データベース接続正常', + 'db_register_flag' => 1 // DB登録可能 ]; } catch (\Exception $e) { @@ -271,17 +372,24 @@ class ShjSixService return [ 'success' => false, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'db_register_flag' => 0 // DB登録不可 ]; } } /** - * 【処理2】デバイス管理マスタを取得する + * 【処理2】サーバーデバイスを取得する * - * @return array デバイス情報 + * 条件: + * - device_type = 'サーバー' + * - device_work = '1' (稼働中) + * - device_workstart <= 現在日付 + * - device_replace IS NULL + * + * @return \Illuminate\Support\Collection デバイス情報 */ - private function getDeviceManagementData(): array + private function getServerDevices() { try { $devices = DB::table('device') @@ -293,23 +401,22 @@ class ShjSixService 'device_identifier', 'device_work', 'device_workstart', - 'device_replace', - 'device_remarks', - 'operator_id' + 'device_remarks' ]) + ->where('device_type', self::DEVICE_TYPE_SERVER) + ->where('device_work', self::DEVICE_WORK_ACTIVE) ->where('device_workstart', '<=', now()) ->whereNull('device_replace') - ->get() - ->toArray(); + ->get(); - Log::info('デバイス管理マスタ取得完了', [ - 'device_count' => count($devices) + Log::info('サーバーデバイス取得完了', [ + 'device_count' => $devices->count() ]); return $devices; } catch (\Exception $e) { - Log::error('デバイス管理マスタ取得エラー', [ + Log::error('サーバーデバイス取得エラー', [ 'error' => $e->getMessage() ]); @@ -318,302 +425,596 @@ class ShjSixService } /** - * 【処理3】デバイス毎のハードウェア状態を取得する + * 【処理3】デバイスのハードウェア状態をチェック * - * @param array $devices デバイス一覧 - * @return array ハードウェア状態結果 + * 直近15分のハードウェアチェックログを確認し、異常時は共通A処理を実行 + * + * @param object $device デバイス情報 + * @param int|null $batchLogId バッチログID + * @param int $dbRegisterFlag DB登録可否 + * @return array チェック結果(メール送信統計含む) */ - private function getHardwareStatus(array $devices): array + private function checkDeviceHardwareStatus($device, ?int $batchLogId, int $dbRegisterFlag): array { try { - $normalDevices = 0; - $abnormalDevices = 0; - $abnormalDetails = []; + // 直近15分のログを取得(最新1件) + $fifteenMinutesAgo = Carbon::now()->subMinutes(self::MONITOR_PERIOD_MINUTES); - foreach ($devices as $device) { - $latestStatus = HardwareCheckLog::getLatestStatusByDevice($device->device_id); + $latestLog = DB::table('hardware_check_log') + ->where('device_id', $device->device_id) + ->where('created_at', '>=', $fifteenMinutesAgo) + ->where('created_at', '<=', now()) + ->orderBy('created_at', 'desc') + ->first(); + + // ログが取得できない、または異常状態の場合 + if (!$latestLog) { + // ログが存在しない = 異常 + $alertMessage = sprintf( + 'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 理由=直近15分のログが存在しません', + $device->device_id, + $device->device_subject ?? 'N/A' + ); - if (!$latestStatus) { - $abnormalDevices++; - $abnormalDetails[] = "デバイスID {$device->device_id}: ハードウェア状態ログが存在しません"; - continue; - } + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + self::QUE_CLASS_SERVER_ERROR, + $device->park_id, + null, // user_id + null, // contract_id + $dbRegisterFlag, + 'ハードウェア監視:ログ未検出' + ); - if ($latestStatus->isNormal()) { - $normalDevices++; - } else { - $abnormalDevices++; - $abnormalDetails[] = "デバイスID {$device->device_id}: {$latestStatus->getStatusNameAttribute()} - {$latestStatus->status_comment}"; - } + return [ + 'has_alert' => true, + 'reason' => 'ログなし', + 'mail_success_count' => $commonAResult['mail_success_count'] ?? 0, + 'mail_error_count' => $commonAResult['mail_error_count'] ?? 0, + 'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠 + 'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠 + 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' + ]; } - Log::info('ハードウェア状態取得完了', [ - 'total_devices' => count($devices), - 'normal_devices' => $normalDevices, - 'abnormal_devices' => $abnormalDevices - ]); + // status != 1(正常)の場合は異常 + if ($latestLog->status != HardwareCheckLog::STATUS_NORMAL) { + $statusName = HardwareCheckLog::getStatusName($latestLog->status); + $alertMessage = sprintf( + 'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 状態=%s, コメント=%s', + $device->device_id, + $device->device_subject ?? 'N/A', + $statusName, + $latestLog->status_comment ?? 'N/A' + ); + + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + self::QUE_CLASS_SERVER_ERROR, + $device->park_id, + null, + null, + $dbRegisterFlag, + 'ハードウェア監視:異常状態検出' + ); + return [ + 'has_alert' => true, + 'reason' => '異常状態', + 'mail_success_count' => $commonAResult['mail_success_count'] ?? 0, + 'mail_error_count' => $commonAResult['mail_error_count'] ?? 0, + 'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠 + 'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠 + 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' + ]; + } + + // 正常 return [ - 'success' => $abnormalDevices === 0, - 'total_devices' => count($devices), - 'normal_devices' => $normalDevices, - 'abnormal_devices' => $abnormalDevices, - 'abnormal_details' => $abnormalDetails, - 'message' => $abnormalDevices > 0 ? - "{$abnormalDevices}台のデバイスで異常を検出" : - '全デバイス正常' + 'has_alert' => false, + 'reason' => '正常', + 'mail_success_count' => 0, + 'mail_error_count' => 0 ]; } catch (\Exception $e) { - Log::error('ハードウェア状態取得エラー', [ + Log::error('ハードウェア状態チェックエラー', [ + 'device_id' => $device->device_id, 'error' => $e->getMessage() ]); return [ - 'success' => false, - 'message' => $e->getMessage(), - 'abnormal_details' => ["ハードウェア状態取得中にエラーが発生: " . $e->getMessage()] + 'has_alert' => false, + 'reason' => 'エラー', + 'mail_success_count' => 0, + 'mail_error_count' => 0 ]; } } /** - * 【処理4】プリンタ制御プログラムログを取得する + * 【処理4】プリンタエラーログをチェック * - * @return array プリンタログ結果 + * 直近15分のプリンタエラーログ(status >= 100)を確認し、 + * エラーログごとに共通A処理を実行 + * + * 仕様書準拠:ステータス >= 100 を条件とする + * + * @param int|null $batchLogId バッチログID + * @param int $dbRegisterFlag DB登録可否 + * @return array チェック結果 */ - private function getPrinterControlLogs(): array + private function checkPrinterErrorLogs(?int $batchLogId, int $dbRegisterFlag): array { try { - // 過去15分間のエラーログを取得 - $errorLogs = PrintJobLog::getRecentErrorLogs(); + // 直近15分のエラーログを取得 + $fifteenMinutesAgo = Carbon::now()->subMinutes(self::MONITOR_PERIOD_MINUTES); - $hasErrors = $errorLogs->count() > 0; - $errorSummary = ''; - $errorDetails = []; + // 仕様書: ステータス >= 100 + // status は varchar型のため CAST で数値変換して比較 + $errorLogs = DB::table('print_job_log') + ->where('created_at', '>=', $fifteenMinutesAgo) + ->where('created_at', '<=', now()) + ->whereRaw('CAST(status AS SIGNED) >= 100') + ->orderBy('created_at', 'desc') + ->get(); - if ($hasErrors) { - $errorsByCode = $errorLogs->groupBy('error_code'); - $errorSummaryParts = []; - - foreach ($errorsByCode as $errorCode => $logs) { - $count = $logs->count(); - $errorSummaryParts[] = "エラーコード{$errorCode}: {$count}件"; - - foreach ($logs as $log) { - $errorDetails[] = sprintf( - "[%s] %s - エラーコード: %d, %s", - $log->created_at->format('Y-m-d H:i:s'), - $log->process_name, - $log->error_code, - $log->status_comment - ); - } + Log::info('プリンタエラーログ取得完了', [ + 'error_count' => $errorLogs->count(), + 'period_minutes' => self::MONITOR_PERIOD_MINUTES + ]); + + // メール送信統計とキュー登録統計(仕様書準拠) + $mailSuccessCount = 0; + $mailErrorCount = 0; + $queueSuccessCount = 0; + $queueErrorCount = 0; + $accumulatedBatchComment = ''; + + // エラーログごとに共通A処理を実行 + foreach ($errorLogs as $log) { + // 仕様書準拠:statusの値に応じてキュー種別を判定 + // - status 200番台 → 102(プリンタエラー) + // - status 300番台 → 103(スキャナエラー) + // - status 400番台 → 104(プリンタ用紙残少警報) + $statusValue = (int)$log->status; + if ($statusValue >= 400 && $statusValue < 500) { + $queClass = self::QUE_CLASS_PAPER_WARNING; // 104 正しい定数名 + $errorType = 'プリンタ用紙残少警報'; + } elseif ($statusValue >= 300 && $statusValue < 400) { + $queClass = self::QUE_CLASS_SCANNER_ERROR; // 103 + $errorType = 'スキャナエラー'; + } else { + $queClass = self::QUE_CLASS_PRINTER_ERROR; // 102(デフォルト) + $errorType = 'プリンタエラー'; } - $errorSummary = implode(', ', $errorSummaryParts); + $alertMessage = sprintf( + '%s: ステータス=%s, プロセス=%s, ジョブ=%s, エラーコード=%d, コメント=%s', + $errorType, + $log->status ?? 'N/A', + $log->process_name ?? 'N/A', + $log->job_name ?? 'N/A', + $log->error_code ?? 0, + $log->status_comment ?? 'N/A' + ); + + $commonAResult = $this->executeCommonProcessA( + $batchLogId, + $alertMessage, + $queClass, // ステータス値に応じて102/103/104 + $log->park_id, + $log->user_id, + $log->contract_id, + $dbRegisterFlag, + sprintf('プリンタ制御:%s検出', $errorType) + ); + + // 仕様書準拠:メール送信結果とキュー登録結果を集計 + $mailSuccessCount += $commonAResult['mail_success_count'] ?? 0; + $mailErrorCount += $commonAResult['mail_error_count'] ?? 0; + $queueSuccessCount += $commonAResult['queue_success_count'] ?? 0; + $queueErrorCount += $commonAResult['queue_error_count'] ?? 0; + + // 仕様書準拠:バッチコメントを累積 + if (!empty($commonAResult['updated_batch_comment'])) { + $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . + $commonAResult['updated_batch_comment']; + } } - Log::info('プリンタ制御プログラムログ取得完了', [ - 'monitoring_period_minutes' => self::PRINTER_LOG_MONITOR_MINUTES, - 'error_logs_count' => $errorLogs->count(), - 'has_errors' => $hasErrors - ]); + // 仕様書準拠:処理4のプロセス名を返却(複数件を連結) + $processNames = []; + foreach ($errorLogs as $log) { + if (!empty($log->process_name)) { + $processNames[] = $log->process_name; + } + } + // 重複を除去して連結 + $processNames = array_unique($processNames); + $concatenatedProcessNames = !empty($processNames) ? implode(',', $processNames) : null; return [ 'success' => true, - 'has_errors' => $hasErrors, 'error_count' => $errorLogs->count(), - 'error_summary' => $errorSummary, - 'error_details' => $errorDetails, - 'monitoring_period' => self::PRINTER_LOG_MONITOR_MINUTES . '分間' + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $queueErrorCount, // 仕様書準拠 + 'accumulated_batch_comment' => $accumulatedBatchComment, + 'process_name' => $concatenatedProcessNames // 処理4のプロセス名(複数件連結) ]; } catch (\Exception $e) { - Log::error('プリンタ制御プログラムログ取得エラー', [ + Log::error('プリンタエラーログチェックエラー', [ 'error' => $e->getMessage() ]); return [ 'success' => false, - 'has_errors' => true, - 'error_summary' => 'ログ取得エラー: ' . $e->getMessage(), - 'error_details' => ["プリンタログ取得中にエラーが発生: " . $e->getMessage()] + 'error_count' => 0 ]; } } - /** - * 監視結果サマリーを作成 - * - * @param array $devices デバイス一覧 - * @param array $hardwareResult ハードウェア結果 - * @param array $printerResult プリンタ結果 - * @return string 監視サマリー - */ - private function createMonitoringSummary(array $devices, array $hardwareResult, array $printerResult): string - { - $summary = [ - 'デバイス数: ' . count($devices), - 'ハードウェア正常: ' . ($hardwareResult['normal_devices'] ?? 0) . '台', - 'ハードウェア異常: ' . ($hardwareResult['abnormal_devices'] ?? 0) . '台', - 'プリンタエラー: ' . ($printerResult['error_count'] ?? 0) . '件' - ]; - - return implode(', ', $summary); - } - /** * 共通A処理:監視結果を反映 * + * 仕様書準拠: + * - SHJ-7 メール送信結果を集計(正常終了件数/異常終了件数) + * - 異常時はバッチコメントに SHJ-7 異常情報を追記 + * - 更新されたバッチコメントでオペレータキュー登録 + * * @param int|null $batchLogId バッチログID * @param string $alertMessage アラートメッセージ - * @return void + * @param int $queClass 対象キュー種別ID(101〜104) + * @param int|null $parkId 駐輪場ID + * @param int|null $userId 利用者ID + * @param int|null $contractId 定期契約ID + * @param int $dbRegisterFlag DB登録可否(0=不可、1=可) + * @param string|null $batchComment バッチコメント + * @return array 処理結果(メール送信統計含む) */ - private function executeCommonProcessA(?int $batchLogId, string $alertMessage): void - { + private function executeCommonProcessA( + ?int $batchLogId, + string $alertMessage, + int $queClass, + ?int $parkId, + ?int $userId, + ?int $contractId, + int $dbRegisterFlag, + ?string $batchComment + ): array { + // メール送信結果統計 + $mailSuccessCount = 0; + $mailErrorCount = 0; + $mailErrorDetails = []; + + // キュー登録結果統計(仕様書準拠) + $queueSuccessCount = 0; + $queueErrorCount = 0; + $registeredQueId = null; // 登録したキューID(後で更新するため) + try { Log::info('共通A処理開始', [ 'batch_log_id' => $batchLogId, - 'alert_message' => $alertMessage + 'alert_message' => $alertMessage, + 'que_class' => $queClass, + 'park_id' => $parkId, + 'db_register_flag' => $dbRegisterFlag ]); // 【共通判断1】DB反映可否判定 - $canReflectToDb = $this->canReflectToDatabase(); - - if ($canReflectToDb) { - // 【共通処理1】オペレータキューを登録する - $this->registerOperatorQueue($alertMessage, $batchLogId); + if ($dbRegisterFlag === 1) { + // DB登録可能な場合 + + // 仕様書準拠:バッチコメント = 処理1.定期契約ID + 元のバッチコメント + $updatedBatchComment = sprintf( + '定期契約ID: %s / %s', + $contractId ?? 'なし', + $batchComment ?? '' + ); + + // 【共通処理1】オペレータキューを登録する(仕様書順序準拠) + // 注:SHJ-7異常情報はメール送信後に追記 + $queueResult = $this->registerOperatorQueue( + $alertMessage, + $batchLogId, + $queClass, + $parkId, + $userId, + $contractId, + $updatedBatchComment + ); + + // 仕様書準拠:キュー登録正常終了件数/異常終了件数を更新 + if ($queueResult['success']) { + $queueSuccessCount++; + $registeredQueId = $queueResult['que_id']; // キューIDを保存 + } else { + $queueErrorCount++; + $updatedBatchComment .= sprintf( + ' | キュー登録異常: %s', + $queueResult['error'] ?? 'Unknown error' + ); + } // 【共通処理2】メール送信対象オペレータを取得する - $operators = $this->getMailTargetOperators(); + $operators = $this->getMailTargetOperators($queClass, $parkId); // 【共通判断2】送信対象有無 if (!empty($operators)) { foreach ($operators as $operator) { - $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); + $result = $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); + + // SHJ-7 メール送信結果を集計 + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + 'オペレータ[%s]へのメール送信失敗: %s', + $operator['email'], + $result['error'] ?? 'Unknown error' + ); + } } } // 【共通処理3】駐輪場管理者を取得する - $parkManagers = $this->getParkManagers(); + $parkManagers = $this->getParkManagers($parkId); // 【共通判断3】送信対象有無 if (!empty($parkManagers)) { foreach ($parkManagers as $manager) { - $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); + $result = $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); + + // SHJ-7 メール送信結果を集計 + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + '駐輪場管理者[%s]へのメール送信失敗: %s', + $manager['email'], + $result['error'] ?? 'Unknown error' + ); + } } } + // 仕様書準拠:メール異常時にSHJ-7異常情報を追記 + if ($mailErrorCount > 0) { + $updatedBatchComment .= sprintf( + ' | SHJ-7メール異常: %d件 (%s)', + $mailErrorCount, + implode('; ', $mailErrorDetails) + ); + } + + // 仕様書準拠:キュー登録後、SHJ-7異常情報を含めてキューコメントを更新 + // キューコメント = 内部変数.バッチコメント(定期契約ID + SHJ-7異常情報) + if ($registeredQueId && $mailErrorCount > 0) { + try { + OperatorQue::where('que_id', $registeredQueId)->update([ // 主キーはque_id + 'que_comment' => $updatedBatchComment, // 仕様書:キューコメントに追記 + 'updated_at' => now() + ]); + + Log::info('オペレータキューコメント更新完了(SHJ-7異常情報追記)', [ + 'que_id' => $registeredQueId, + 'updated_comment' => $updatedBatchComment + ]); + } catch (\Exception $e) { + Log::error('オペレータキューコメント更新エラー', [ + 'que_id' => $registeredQueId, + 'error' => $e->getMessage() + ]); + } + } + } else { - // DB反映NGの場合は固定メールアドレスに送信 - $this->sendAlertMail(self::FIXED_EMAIL_ADDRESS, $alertMessage, 'システム管理者'); + // DB反映NGの場合は固定メールアドレスに緊急メール送信 + // 仕様書準拠:テンプレート不使用、件名固定、本文なし + $result = $this->sendEmergencyMail( + self::FIXED_EMAIL_ADDRESS, + $alertMessage + ); + + if ($result['success']) { + $mailSuccessCount++; + } else { + $mailErrorCount++; + $mailErrorDetails[] = sprintf( + '固定アドレス[%s]への緊急メール送信失敗: %s', + self::FIXED_EMAIL_ADDRESS, + $result['error'] ?? 'Unknown error' + ); + } + + $updatedBatchComment = $batchComment ?? ''; + if ($mailErrorCount > 0) { + $updatedBatchComment .= sprintf( + ' | 緊急メール異常: %d件 (%s)', + $mailErrorCount, + implode('; ', $mailErrorDetails) + ); + } } Log::info('共通A処理完了', [ - 'batch_log_id' => $batchLogId + 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'updated_batch_comment' => $updatedBatchComment ?? $batchComment ]); + return [ + 'success' => true, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'mail_error_details' => $mailErrorDetails, + 'queue_success_count' => $queueSuccessCount, // 仕様書準拠 + 'queue_error_count' => $queueErrorCount, // 仕様書準拠 + 'updated_batch_comment' => $updatedBatchComment ?? $batchComment + ]; + } catch (\Exception $e) { Log::error('共通A処理エラー', [ 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, 'error' => $e->getMessage() ]); - } - } - - /** - * DB反映可否判定 - * - * @return bool 反映可能かどうか - */ - private function canReflectToDatabase(): bool - { - try { - // 簡単なINSERTテストでDB反映可否を確認 - DB::beginTransaction(); - $testId = DB::table('operator_que')->insertGetId([ - 'que_class' => 6, - 'que_comment' => 'DB反映テスト', - 'que_status' => 0, - 'created_at' => now(), - 'updated_at' => now() - ]); - // テストレコードを削除 - DB::table('operator_que')->where('que_id', $testId)->delete(); - DB::commit(); + $updatedBatchComment = ($batchComment ?? '') . ' | 共通A処理エラー: ' . $e->getMessage(); - return true; - - } catch (\Exception $e) { - DB::rollBack(); - Log::warning('DB反映不可', ['error' => $e->getMessage()]); - return false; + return [ + 'success' => false, + 'mail_success_count' => $mailSuccessCount, + 'mail_error_count' => $mailErrorCount, + 'mail_error_details' => $mailErrorDetails, + 'queue_success_count' => $queueSuccessCount, + 'queue_error_count' => $queueErrorCount, + 'updated_batch_comment' => $updatedBatchComment, + 'error' => $e->getMessage() + ]; } } /** * オペレータキューを登録 * + * 仕様書準拠: + * - 定期契約IDをバッチコメントに含める + * - 登録の成否とque_idを返却(後で更新するため) + * * @param string $alertMessage アラートメッセージ * @param int|null $batchLogId バッチログID - * @return void + * @param int $queClass 対象キュー種別ID(101〜104) + * @param int|null $parkId 駐輪場ID + * @param int|null $userId 利用者ID + * @param int|null $contractId 定期契約ID + * @param string|null $batchComment バッチコメント(SHJ-7異常情報は後で追記) + * @return array 登録結果 ['success' => bool, 'error' => string|null, 'que_id' => int|null] */ - private function registerOperatorQueue(string $alertMessage, ?int $batchLogId): void - { + private function registerOperatorQueue( + string $alertMessage, + ?int $batchLogId, + int $queClass, + ?int $parkId, + ?int $userId, + ?int $contractId, + ?string $batchComment + ): array { try { - OperatorQue::create([ - 'que_class' => 6, // SHJ-6用のクラス - 'user_id' => null, - 'contract_id' => null, - 'park_id' => null, - 'que_comment' => $alertMessage, - 'que_status' => 0, // 待機中 - 'que_status_comment' => 'システム監視アラート', - 'work_instructions' => "SHJ-6監視処理 BatchLogID: {$batchLogId}", + // 仕様書準拠(todo/SHJ-6/SHJ-6.txt:170-182): + // - キューコメント = 内部変数.バッチコメント + // - キューステータスコメント = "" (空) + // - 業務指示コメント = "" (空) + + $operatorQue = OperatorQue::create([ + 'que_class' => $queClass, + 'user_id' => $userId, + 'contract_id' => $contractId, + 'park_id' => $parkId, + 'que_comment' => $batchComment ?? '', // 仕様書:内部変数.バッチコメント(SHJ-7異常情報は後で追記) + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', // 仕様書:空文字列 + 'work_instructions' => '', // 仕様書:空文字列 + 'operator_id' => 9999999, // 仕様書準拠:固定値 'created_at' => now(), 'updated_at' => now() ]); Log::info('オペレータキュー登録完了', [ 'batch_log_id' => $batchLogId, - 'alert_message' => $alertMessage + 'que_class' => $queClass, + 'que_id' => $operatorQue->que_id, // 主キーはque_id + 'park_id' => $parkId, + 'user_id' => $userId, + 'contract_id' => $contractId ]); + return [ + 'success' => true, + 'error' => null, + 'que_id' => $operatorQue->que_id // 仕様書準拠:主キーque_idを返却 + ]; + } catch (\Exception $e) { Log::error('オペレータキュー登録エラー', [ 'batch_log_id' => $batchLogId, + 'que_class' => $queClass, 'error' => $e->getMessage() ]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'que_id' => null + ]; } } /** * メール送信対象オペレータを取得 * + * 管轄駐輪場マスタをJOINして、指定駐輪場を管轄しているオペレータのみ取得 + * + * キュー種別IDに応じた送信フラグをチェック: + * - 101(サーバーエラー) → ope_sendalart_que10 + * - 102(プリンタエラー) → ope_sendalart_que11 + * - 103(スキャナエラー) → ope_sendalart_que12 + * - 104(プリンタ用紙残少) → ope_sendalart_que13 + * + * @param int $queClass 対象キュー種別ID + * @param int|null $parkId 駐輪場ID(nullの場合は全オペレータ) * @return array オペレータ一覧 */ - private function getMailTargetOperators(): array + private function getMailTargetOperators(int $queClass, ?int $parkId): array { try { - // user_typeがオペレータのユーザーを取得(仮の条件) - $operators = DB::table('users') - ->select(['user_id', 'email', 'name']) - ->where('user_type', 'operator') // 実際のテーブル構造に合わせて調整 - ->whereNotNull('email') - ->where('email', '!=', '') - ->get() - ->map(function ($user) { + // キュー種別IDに対応する送信フラグカラム名を決定 + $alertFlagColumn = $this->getOperatorAlertFlagColumn($queClass); + + if (empty($alertFlagColumn)) { + Log::warning('不正なキュー種別ID', ['que_class' => $queClass]); + return []; + } + + // 管轄駐輪場マスタをJOINしてオペレータを取得 + $query = DB::table('ope as T1') + ->select(['T1.ope_id', 'T1.ope_name', 'T1.ope_mail']) + ->where('T1.' . $alertFlagColumn, 1) // 該当アラート送信フラグが有効 + ->where('T1.ope_quit_flag', 0) // 退職していない + ->whereNotNull('T1.ope_mail') + ->where('T1.ope_mail', '!=', ''); + + // 駐輪場IDが指定されている場合は管轄駐輪場マスタでフィルタ + if ($parkId !== null) { + $query->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id') + ->where('T2.park_id', $parkId); + } + + $operators = $query->get() + ->map(function ($ope) { return [ - 'user_id' => $user->user_id, - 'email' => $user->email, - 'name' => $user->name ?? 'オペレータ' + 'ope_id' => $ope->ope_id, + 'name' => $ope->ope_name, + 'email' => $ope->ope_mail ]; }) ->toArray(); Log::info('メール送信対象オペレータ取得完了', [ + 'que_class' => $queClass, + 'park_id' => $parkId, + 'alert_flag_column' => $alertFlagColumn, 'operator_count' => count($operators) ]); @@ -621,6 +1022,8 @@ class ShjSixService } catch (\Exception $e) { Log::error('メール送信対象オペレータ取得エラー', [ + 'que_class' => $queClass, + 'park_id' => $parkId, 'error' => $e->getMessage() ]); @@ -628,31 +1031,59 @@ class ShjSixService } } + /** + * キュー種別IDに対応するオペレータ送信フラグカラム名を取得 + * + * @param int $queClass キュー種別ID + * @return string|null カラム名 + */ + private function getOperatorAlertFlagColumn(int $queClass): ?string + { + $mapping = [ + 101 => 'ope_sendalart_que10', // サーバーエラー + 102 => 'ope_sendalart_que11', // プリンタエラー + 103 => 'ope_sendalart_que12', // スキャナエラー + 104 => 'ope_sendalart_que13', // プリンタ用紙残少警報 + ]; + + return $mapping[$queClass] ?? null; + } + /** * 駐輪場管理者を取得 * + * 駐輪場管理者マスタから指定駐輪場の管理者を取得: + * - 退職フラグ = 0(在職中) + * - メールアドレスが設定されている + * - 所属駐輪場ID = 指定駐輪場ID + * + * @param int|null $parkId 駐輪場ID(nullの場合は全管理者) * @return array 駐輪場管理者一覧 */ - private function getParkManagers(): array + private function getParkManagers(?int $parkId): array { try { - // 駐輪場管理者を取得(仮の条件) - $managers = DB::table('users') - ->select(['user_id', 'email', 'name']) - ->where('user_type', 'park_manager') // 実際のテーブル構造に合わせて調整 - ->whereNotNull('email') - ->where('email', '!=', '') + $query = Manager::active()->hasEmail(); + + // 駐輪場IDが指定されている場合はフィルタ + if ($parkId !== null) { + $query->where('manager_parkid', $parkId); + } + + $managers = $query->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) ->get() - ->map(function ($user) { + ->map(function ($manager) { return [ - 'user_id' => $user->user_id, - 'email' => $user->email, - 'name' => $user->name ?? '駐輪場管理者' + 'manager_id' => $manager->manager_id, + 'name' => $manager->manager_name, + 'email' => $manager->manager_mail, + 'park_id' => $manager->manager_parkid ]; }) ->toArray(); Log::info('駐輪場管理者取得完了', [ + 'park_id' => $parkId, 'manager_count' => count($managers) ]); @@ -660,6 +1091,7 @@ class ShjSixService } catch (\Exception $e) { Log::error('駐輪場管理者取得エラー', [ + 'park_id' => $parkId, 'error' => $e->getMessage() ]); @@ -668,21 +1100,25 @@ class ShjSixService } /** - * アラートメールを送信 + * アラートメールを送信(SHJ-7) + * + * 仕様書準拠: + * - SHJ-7 メール送信サービスを呼び出し + * - 戻り値(成功/失敗、異常情報)を返却 * * @param string $email メールアドレス * @param string $alertMessage アラートメッセージ * @param string $recipientType 受信者タイプ - * @return void + * @return array 送信結果 ['success' => bool, 'error' => string|null] */ - private function sendAlertMail(string $email, string $alertMessage, string $recipientType): void + private function sendAlertMail(string $email, string $alertMessage, string $recipientType): array { try { - // SHJメール送信機能を使用(メールテンプレートID=1を使用、実際の値に調整) + // SHJ-7 メール送信機能を使用(メールテンプレートID=202を使用) $result = $this->mailSendService->executeMailSend( $email, '', // 予備メールアドレスは空 - 1 // システムアラート用メールテンプレートID + self::SYSTEM_ALERT_MAIL_TEMPLATE_ID ); if ($result['success']) { @@ -691,12 +1127,22 @@ class ShjSixService 'recipient_type' => $recipientType, 'alert_message' => $alertMessage ]); + + return [ + 'success' => true, + 'error' => null + ]; } else { Log::error('アラートメール送信失敗', [ 'email' => $email, 'recipient_type' => $recipientType, 'error' => $result['message'] ]); + + return [ + 'success' => false, + 'error' => $result['message'] ?? 'メール送信失敗' + ]; } } catch (\Exception $e) { @@ -705,6 +1151,144 @@ class ShjSixService 'recipient_type' => $recipientType, 'error' => $e->getMessage() ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + /** + * DB反映不可時の緊急メール送信 + * + * 仕様書準拠: + * - テンプレート不使用 + * - 件名: 「So-Manager:死活監視DBアタッチエラー(SHJ-6)」固定 + * - 本文: なし(空) + * - 固定アドレスに直接送信 + * + * @param string $email 送信先メールアドレス + * @param string $alertMessage アラートメッセージ(ログ用) + * @return array 送信結果 ['success' => bool, 'error' => string|null] + */ + private function sendEmergencyMail(string $email, string $alertMessage): array + { + try { + // 仕様書準拠:件名固定、本文なし + $subject = 'So-Manager:死活監視DBアタッチエラー(SHJ-6)'; + $body = ''; // 本文なし + + // Laravelの Mail facade を使用して直接送信 + \Illuminate\Support\Facades\Mail::raw($body, function ($message) use ($email, $subject) { + $message->to($email) + ->subject($subject); + }); + + Log::info('緊急メール送信成功', [ + 'email' => $email, + 'subject' => $subject, + 'alert_message' => $alertMessage + ]); + + return [ + 'success' => true, + 'error' => null + ]; + + } catch (\Exception $e) { + Log::error('緊急メール送信エラー', [ + 'email' => $email, + 'alert_message' => $alertMessage, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + /** + * 【処理5】SHJ-8 バッチ処理ログ作成 + * + * 仕様書に基づくSHJ-8共通処理呼び出し + * BatchLogシステムを使用してバッチ処理の実行ログを記録 + * + * @param array $statistics 処理統計情報 + * @return array 処理結果 ['success' => bool, 'error' => string|null] + */ + private function createShjBatchLog(array $statistics): array + { + try { + // 仕様書準拠のSHJ-8パラメータ設定 + // device_id: 処理2で取得したデバイスID(複数なら連結文字列) + // process_name: 処理4のプロセス名 + $deviceId = $statistics['device_id']; // 連結されたデバイスID文字列 + $processName = $statistics['process_name'] ?? 'SHJ-6'; + $jobName = $statistics['job_name']; // "SHJ-6サーバ死活監視" 固定 + $status = $statistics['status']; // 常に "success" + $statusComment = $statistics['status_comment'] ?? ''; + + $createdDate = now()->format('Y/m/d'); + $updatedDate = now()->format('Y/m/d'); + + Log::info('SHJ-8 バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ]); + + // 共通処理 SHJ-8 バッチ処理ログ作成を呼び出し + // BatchLog::createBatchLog を使用して統一的にログを記録 + $batchLog = BatchLog::createBatchLog( + $processName, + $status, + [ + 'device_id' => $deviceId, + 'job_name' => $jobName, + 'status_comment' => $statusComment, + 'statistics' => $statistics, + 'shj8_params' => [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ] + ], + $statusComment + ); + + Log::info('SHJ-8 バッチ処理ログ作成完了', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'batch_log_id' => $batchLog->id + ]); + + return [ + 'success' => true, + 'error' => null + ]; + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'statistics' => $statistics + ]); + + // 仕様書準拠:SHJ-8でエラーが発生してもメイン処理は継続 + // エラー情報を返却 + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; } } } diff --git a/app/Services/ShjTenService.php b/app/Services/ShjTenService.php index f1a59cd..ce994b1 100644 --- a/app/Services/ShjTenService.php +++ b/app/Services/ShjTenService.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\Park; -use App\Models\RegularContract; use App\Models\EarningsSummary; use App\Models\Psection; use App\Models\Batch\BatchLog; @@ -16,52 +15,10 @@ use Carbon\Carbon; * SHJ-10 売上集計処理サービス * * 財政年度ベースの年次・月次売上集計処理を実行するビジネスロジック - * バッチ処理「SHJ-10売上集計」の核となる処理を担当 + * バッチ処理「SHJ-10売上集計(年次・月次)」の核となる処理を担当 */ class ShjTenService { - /** - * Park モデル - * - * @var Park - */ - protected $parkModel; - - /** - * RegularContract モデル - * - * @var RegularContract - */ - protected $contractModel; - - /** - * EarningsSummary モデル - * - * @var EarningsSummary - */ - protected $earningsSummaryModel; - - /** - * Psection モデル - * - * @var Psection - */ - protected $psectionModel; - - /** - * BatchLog モデル - * - * @var BatchLog - */ - protected $batchLogModel; - - /** - * OperatorQue モデル - * - * @var OperatorQue - */ - protected $operatorQueModel; - /** * 財政年度開始月 * @@ -70,42 +27,38 @@ class ShjTenService const FISCAL_START_MONTH = 4; /** - * コンストラクタ - * - * @param Park $parkModel - * @param RegularContract $contractModel - * @param EarningsSummary $earningsSummaryModel - * @param Psection $psectionModel - * @param BatchLog $batchLogModel - * @param OperatorQue $operatorQueModel + * バッチ実行者の固定オペレータID */ - public function __construct( - Park $parkModel, - RegularContract $contractModel, - EarningsSummary $earningsSummaryModel, - Psection $psectionModel, - BatchLog $batchLogModel, - OperatorQue $operatorQueModel - ) { - $this->parkModel = $parkModel; - $this->contractModel = $contractModel; - $this->earningsSummaryModel = $earningsSummaryModel; - $this->psectionModel = $psectionModel; - $this->batchLogModel = $batchLogModel; - $this->operatorQueModel = $operatorQueModel; + const BATCH_OPERATOR_ID = 9999999; + + /** + * 年次集計の summary_type 値 + */ + const SUMMARY_TYPE_YEARLY = 1; + + /** + * 月次集計の summary_type 値 + */ + const SUMMARY_TYPE_MONTHLY = 2; + + /** + * コンストラクタ + */ + public function __construct() + { } /** * SHJ-10 財政年度売上集計処理メイン実行 * - * 処理フロー: + * 処理フロー (todo/SHJ-10/SHJ-10.txt): * 【処理1】集計対象を設定する * 【処理2】駐輪場マスタを取得する * 【判断1】取得件数判定 * 【処理3】車種区分毎に算出する * 【判断2】取得判定 * 【処理4】売上集計結果を削除→登録する - * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する + * 【処理5】バッチ処理ログを作成し、情報不備がある場合のみオペレータキューを作成する * * @param string $type 集計種別(yearly/monthly) * @param string $target 集計対象 @@ -115,6 +68,9 @@ class ShjTenService public function executeFiscalEarningsAggregation(string $type, string $target, array $fiscalPeriod): array { $batchLogId = null; + $batchLog = null; + $statusComments = []; // 内部変数.ステータスコメント + $dataIntegrityIssues = []; // 内部変数.情報不備 try { // 【処理1】集計対象を設定する(財政年度ベース) @@ -148,22 +104,30 @@ class ShjTenService // 【判断1】取得件数判定 if (empty($parkInfo)) { $typeLabel = $this->getTypeLabel($type); - $message = "売上集計({$typeLabel}):駐輪場マスタが存在していません。"; + $statusComment = "売上集計{$typeLabel}:駐輪場マスタが存在していません。"; + $statusComments[] = $statusComment; // バッチログ更新 $batchLog->update([ 'status' => BatchLog::STATUS_WARNING, 'end_time' => now(), - 'message' => $message, - 'success_count' => 1 // 処理は成功したが対象なし + 'message' => $statusComment, + 'success_count' => 1 ]); // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($message, $batchLogId); + $this->createOperatorQueue($statusComment, $batchLogId); + + // SHJ-8 バッチ処理ログ作成 + $this->createShjBatchLog([ + 'job_name' => 'SHJ-10売上集計(年次・月次)', + 'status' => 'success', + 'status_comment' => $statusComment + ]); return [ 'success' => true, - 'message' => $message, + 'message' => $statusComment, 'processed_parks' => 0, 'summary_records' => 0, 'batch_log_id' => $batchLogId @@ -175,54 +139,61 @@ class ShjTenService $processedParks = 0; foreach ($parkInfo as $park) { - $parkSummaryRecords = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod); + $result = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod, $batchLogId); - if ($parkSummaryRecords > 0) { - $processedParks++; - $summaryRecords += $parkSummaryRecords; + $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']; } } - // 【判断2】取得判定 - if ($summaryRecords === 0) { - $message = '対象なしの結果を設定する(契約)'; - - // バッチログ更新 - $batchLog->update([ - 'status' => BatchLog::STATUS_WARNING, - 'end_time' => now(), - 'message' => $message, - 'success_count' => 1 - ]); - - // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($message, $batchLogId); - - return [ - 'success' => true, - 'message' => $message, - 'processed_parks' => $processedParks, - 'summary_records' => 0, - 'batch_log_id' => $batchLogId - ]; + // 最終ステータスコメント生成 + $finalStatusComment = "SHJ-10 売上集計処理正常完了 ({$type}: {$fiscalPeriod['target_label']}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + if (!empty($statusComments)) { + $finalStatusComment .= "\n" . implode("\n", $statusComments); } - + if (!empty($dataIntegrityIssues)) { + $finalStatusComment .= "\n" . implode("\n", $dataIntegrityIssues); + } + // バッチ処理完了ログ更新 - $completionMessage = "SHJ-10 売上集計処理正常完了 ({$type}: {$fiscalPeriod['target_label']}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; $batchLog->update([ 'status' => BatchLog::STATUS_SUCCESS, 'end_time' => now(), - 'message' => $completionMessage, + 'message' => $finalStatusComment, 'success_count' => 1 ]); // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($completionMessage, $batchLogId); + // ※ 駐輪場単位で既に作成済み(processFiscalEarningsForPark内で情報不備検出時に実施) + if (!empty($dataIntegrityIssues)) { + Log::warning('SHJ-10 情報不備検出', [ + 'batch_log_id' => $batchLogId, + 'issues' => $dataIntegrityIssues + ]); + } + + // SHJ-8 バッチ処理ログ作成 + $this->createShjBatchLog([ + 'job_name' => 'SHJ-10売上集計(年次・月次)', + 'status' => 'success', + 'status_comment' => $finalStatusComment + ]); Log::info('SHJ-10 売上集計処理完了', [ 'batch_log_id' => $batchLogId, 'processed_parks' => $processedParks, - 'summary_records' => $summaryRecords + 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), + 'no_data_parks' => count($statusComments) ]); return [ @@ -230,6 +201,7 @@ class ShjTenService 'message' => 'SHJ-10 売上集計処理が正常に完了しました', 'processed_parks' => $processedParks, 'summary_records' => $summaryRecords, + 'data_integrity_issues' => count($dataIntegrityIssues), 'batch_log_id' => $batchLogId ]; @@ -246,6 +218,17 @@ class ShjTenService ]); } + // SHJ-8 バッチ処理ログ作成(エラー時も作成) + try { + $this->createShjBatchLog([ + 'job_name' => 'SHJ-10売上集計(年次・月次)', + 'status' => 'error', + 'status_comment' => $errorMessage + ]); + } catch (\Exception $shjException) { + Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); + } + Log::error('SHJ-10 売上集計処理エラー', [ 'batch_log_id' => $batchLogId, 'exception' => $e->getMessage(), @@ -281,12 +264,6 @@ class ShjTenService /** * 【処理2】駐輪場マスタを取得する - * - * 仕様書のSQLクエリに基づく駐輪場情報取得 - * SELECT 駐輪場ID, 駐輪場名 - * FROM 駐輪場マスタ - * WHERE 閉設フラグ <> 1 - * ORDER BY 駐輪場ふりがな * * @return array 駐輪場情報 */ @@ -321,43 +298,83 @@ class ShjTenService * @param object $park 駐輪場情報 * @param array $aggregationTarget 集計対象 * @param array $fiscalPeriod 財政期間情報 - * @return int 作成された集計レコード数 + * @param int $batchLogId バッチログID + * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] */ - private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): int + private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod, int $batchLogId): array { try { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // 0. 情報不備チェック + $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $startDate, $endDate); + + // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-10/SHJ-10.txt:289-299) + if ($dataIntegrityIssue !== '情報不備:なし') { + $this->createOperatorQueue($dataIntegrityIssue, $batchLogId, $park->park_id); + } + + // ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + $regularData = $this->calculateRegularEarnings($park->park_id, $startDate, $endDate); + + // ② 一時金データ取得(車種毎) + $lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $startDate, $endDate); + + // ③ 解約返戻金データ取得(車種区分毎) + $refundData = $this->calculateRefundEarnings($park->park_id, $startDate, $endDate); + + // ④ 再発行データ取得(車種区分毎) + $reissueData = $this->calculateReissueCount($park->park_id, $startDate, $endDate); + + // 【判断2】データがいずれかあれば【処理4】へ + if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) { + // 対象データなし - 仕様 todo/SHJ-10/SHJ-10.txt:209-211 + $typeLabel = $this->getTypeLabel($aggregationTarget['type']); + $noDataMessage = "売上集計{$typeLabel}:{$startDate}~{$endDate}/駐輪場:{$park->park_name}:売上データが存在しません。"; + + return [ + 'summary_records' => 0, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => $noDataMessage + ]; + } + // 【処理4】既存の売上集計結果を削除 $this->deleteExistingFiscalSummary($park->park_id, $aggregationTarget); - // 【処理3】車種区分毎に算出する - $psections = $this->getPsectionInformation(); + // 【処理4】売上集計結果を登録 $summaryRecords = 0; - foreach ($psections as $psection) { - $earningsData = $this->calculateFiscalEarningsForPsection( - $park->park_id, - $psection->psection_id, - $aggregationTarget - ); - - if ($this->hasEarningsData($earningsData)) { - // 売上集計結果を登録 - $this->createFiscalEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $fiscalPeriod); - $summaryRecords++; - } + // ① 定期契約データがある場合:同じ組合せ(psection×usertype×months)を統合 + $mergedRegularData = $this->mergeRegularDataByGroup($regularData); + foreach ($mergedRegularData as $key => $mergedRow) { + $this->createFiscalEarningsSummary($park, $mergedRow, $aggregationTarget, $fiscalPeriod, 'regular'); + $summaryRecords++; } - Log::info('駐輪場財政年度売上集計完了', [ + // ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約) + $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); + foreach ($otherDataByPsection as $psectionId => $data) { + $this->createFiscalEarningsSummary($park, $data, $aggregationTarget, $fiscalPeriod, 'other'); + $summaryRecords++; + } + + Log::info('駐輪場売上集計完了', [ 'park_id' => $park->park_id, 'park_name' => $park->park_name, 'summary_records' => $summaryRecords, - 'fiscal_period' => $fiscalPeriod['target_label'] + 'data_integrity_issue' => $dataIntegrityIssue ]); - return $summaryRecords; + return [ + 'summary_records' => $summaryRecords, + 'data_integrity_issue' => $dataIntegrityIssue, + 'no_data_message' => null + ]; } catch (\Exception $e) { - Log::error('駐輪場財政年度売上集計エラー', [ + Log::error('駐輪場売上集計エラー', [ 'park_id' => $park->park_id, 'error' => $e->getMessage() ]); @@ -367,117 +384,315 @@ class ShjTenService } /** - * 車種区分情報取得 + * 0. 情報不備チェック(期間集計版) * - * @return array 車種区分情報 - */ - private function getPsectionInformation(): array - { - return DB::table('psection') - ->select(['psection_id', 'psection_subject']) - ->get() - ->toArray(); - } - - /** - * 【処理3】車種区分毎に財政年度売上を算出する + * 仕様 todo/SHJ-10/SHJ-10.txt:77-101 * - * 4つの項目を計算: - * ①売上・件数 - * ②一時金売上 - * ③解約返戻金 - * ④再発行金額・件数 - * * @param int $parkId 駐輪場ID - * @param int $psectionId 車種区分ID - * @param array $aggregationTarget 集計対象 - * @return array 売上データ + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return string 情報不備メッセージ(仕様フォーマット:"情報不備:xxx" or "情報不備:なし") */ - private function calculateFiscalEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + private function checkDataIntegrity(int $parkId, string $startDate, string $endDate): string { - $startDate = $aggregationTarget['start_date']; - $endDate = $aggregationTarget['end_date']; + $incompleteContracts = DB::table('regular_contract') + ->select('contract_id') + ->where('park_id', $parkId) + ->where('contract_flag', 1) + ->whereBetween(DB::raw('DATE(contract_payment_day)'), [$startDate, $endDate]) + ->where(function($query) { + $query->whereNull('update_flag') + ->orWhereNull('psection_id') + ->orWhereNull('enable_months'); + }) + ->pluck('contract_id') + ->toArray(); - // ①売上・件数(billing_amount) - $salesData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as sales_count'), - DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('contract_flag', 1) - ->whereBetween('contract_payment_day', [$startDate, $endDate]) - ->whereNull('contract_cancelday') - ->first(); - - // ②一時金売上(contract_money) - $temporaryData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as temporary_count'), - DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('contract_flag', 1) - ->whereBetween('contract_payment_day', [$startDate, $endDate]) - ->whereNotNull('contract_money') - ->where('contract_money', '>', 0) - ->first(); - - // ③解約返戻金(refunds) - $refundData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as refund_count'), - DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->whereBetween('contract_cancelday', [$startDate, $endDate]) - ->whereNotNull('refunds') - ->where('refunds', '>', 0) - ->first(); - - // ④再発行金額・件数(seal_reissue_request) - $reissueData = DB::table('regular_contract') - ->select([ - DB::raw('COUNT(*) as reissue_count'), - DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') - ]) - ->where('park_id', $parkId) - ->where('psection_id', $psectionId) - ->where('seal_reissue_request', 1) - ->whereBetween('updated_at', [$startDate, $endDate]) - ->first(); - - return [ - 'sales_count' => $salesData->sales_count ?? 0, - 'sales_amount' => $salesData->sales_amount ?? 0, - 'temporary_count' => $temporaryData->temporary_count ?? 0, - 'temporary_amount' => $temporaryData->temporary_amount ?? 0, - 'refund_count' => $refundData->refund_count ?? 0, - 'refund_amount' => $refundData->refund_amount ?? 0, - 'reissue_count' => $reissueData->reissue_count ?? 0, - 'reissue_amount' => $reissueData->reissue_amount ?? 0 - ]; + if (empty($incompleteContracts)) { + return '情報不備:なし'; + } + + // 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り + return '情報不備:' . implode(',', $incompleteContracts); } /** - * 売上データの存在チェック + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + * ※ 金額・件数は全て0固定だが、分類や月数の組み合わせごとにレコードが必要 * - * @param array $earningsData 売上データ - * @return bool データが存在するかどうか + * 仕様 todo/SHJ-10/SHJ-10.txt:104-128 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array */ - private function hasEarningsData(array $earningsData): bool + /** + * ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:104-129 + * SQL定義:減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、 + * 授受金額の合計と件数を算出する + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateRegularEarnings(int $parkId, string $startDate, string $endDate): array { - return $earningsData['sales_count'] > 0 || - $earningsData['temporary_count'] > 0 || - $earningsData['refund_count'] > 0 || - $earningsData['reissue_count'] > 0; + $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'), // 仕様:授受金額の合計 + DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様:授受件数 + ]) + ->where('T1.park_id', $parkId) + ->where('T1.contract_flag', 1) + ->whereBetween(DB::raw('DATE(T1.contract_payment_day)'), [$startDate, $endDate]) + ->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(); } /** - * 【処理4】既存の財政年度売上集計結果を削除 + * ② 一時金データ取得(車種毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:148-159 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateLumpsumEarnings(int $parkId, string $startDate, string $endDate): 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) + ->whereBetween(DB::raw('DATE(pay_date)'), [$startDate, $endDate]) + ->groupBy('type_class') + ->get(); + + return $results->toArray(); + } + + /** + * ③ 解約返戻金データ取得(車種区分毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:160-171 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateRefundEarnings(int $parkId, string $startDate, string $endDate): 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) + ->whereBetween(DB::raw('DATE(repayment_at)'), [$startDate, $endDate]) + ->groupBy('psection_id') + ->get(); + + return $results->toArray(); + } + + /** + * ④ 再発行データ取得(車種区分毎) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:172-183 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 集計開始日(YYYY-MM-DD) + * @param string $endDate 集計終了日(YYYY-MM-DD) + * @return array + */ + private function calculateReissueCount(int $parkId, string $startDate, string $endDate): 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) + ->whereBetween(DB::raw('DATE(seal_day)'), [$startDate, $endDate]) + ->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-10/SHJ-10.txt:130-147 + * + * @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-10/SHJ-10.txt:213-221 + * 仕様書どおり summary_start_date と summary_end_date は NULL で保存されているため、NULL で検索する * * @param int $parkId 駐輪場ID * @param array $aggregationTarget 集計対象 @@ -485,84 +700,218 @@ class ShjTenService */ private function deleteExistingFiscalSummary(int $parkId, array $aggregationTarget): void { + // 仕様書どおり、同一キーの組み合わせで削除 DB::table('earnings_summary') ->where('park_id', $parkId) - ->where('summary_start_date', $aggregationTarget['start_date']) - ->where('summary_end_date', $aggregationTarget['end_date']) ->where('summary_type', $aggregationTarget['summary_type']) + ->whereNull('summary_start_date') // 仕様書line 251: null + ->whereNull('summary_end_date') // 仕様書line 252: null + ->where('earnings_date', $aggregationTarget['end_date']) + // psection_id, usertype_subject1, enable_months は + // レコードごとに異なるため、ここでは指定しない ->delete(); - } - - /** - * 財政年度売上集計結果を登録 - * - * @param object $park 駐輪場情報 - * @param object $psection 車種区分情報 - * @param array $aggregationTarget 集計対象 - * @param array $earningsData 売上データ - * @param array $fiscalPeriod 財政期間情報 - * @return void - */ - private function createFiscalEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, array $fiscalPeriod): void - { - DB::table('earnings_summary')->insert([ - 'park_id' => $park->park_id, - 'summary_type' => $aggregationTarget['summary_type'], // 1:年次, 2:月次 - 'summary_start_date' => $aggregationTarget['start_date'], - 'summary_end_date' => $aggregationTarget['end_date'], - 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 - 'psection_id' => $psection->psection_id, - 'usertype_subject1' => $psection->psection_subject, - 'regular_new_count' => $earningsData['sales_count'], - 'regular_new_amount' => $earningsData['sales_amount'], - 'turnsum' => $earningsData['temporary_amount'], - 'turnsum_count' => $earningsData['temporary_count'], - 'refunds' => $earningsData['refund_amount'], - 'reissue_count' => $earningsData['reissue_count'], - 'reissue_amount' => $earningsData['reissue_amount'], - 'summary_note' => "SHJ-10 {$fiscalPeriod['target_label']} 財政年度売上集計結果", - 'created_at' => now(), - 'updated_at' => now(), - 'operator_id' => 0 // システム処理 + + Log::debug('既存の売上集計結果削除', [ + 'park_id' => $parkId, + 'summary_type' => $aggregationTarget['summary_type'], + 'earnings_date' => $aggregationTarget['end_date'] ]); } /** - * 【処理5】オペレータキュー作成 + * 売上集計結果を登録(財政年度ベース) * - * @param string $message メッセージ - * @param int $batchLogId バッチログID + * 仕様 todo/SHJ-10/SHJ-10.txt:215-284 + * + * @param object $park 駐輪場情報 + * @param object $data 売上データ + * @param array $aggregationTarget 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @param string $dataType データ種別(regular or other) * @return void */ - private function createOperatorQueue(string $message, int $batchLogId): void + private function createFiscalEarningsSummary($park, $data, array $aggregationTarget, array $fiscalPeriod, string $dataType): void + { + $insertData = [ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], // 1=年次, 2=月次 + 'summary_start_date' => null, // 仕様書line 251: null + 'summary_end_date' => null, // 仕様書line 252: null + 'earnings_date' => $aggregationTarget['end_date'], // 集計終了日 + 'psection_id' => $data->psection_id, + 'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名 + 'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数 + 'summary_note' => "SHJ-10:{$fiscalPeriod['target_label']}/{$aggregationTarget['start_date']}~{$aggregationTarget['end_date']}", // 仕様line 272 + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 275) + ]; + + if ($dataType === 'regular') { + // 定期契約データの場合:mergeRegularDataByGroup()で既に統合済み + // 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 130-147) + $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 140 + 'lumpsum' => 0, // 仕様line 141 + 'refunds' => 0, // 仕様line 142 + 'other_income' => 0, // 仕様line 143 + 'other_spending' => 0, // 仕様line 144 + 'reissue_count' => 0, // 仕様line 145 + 'reissue_amount' => 0 // 仕様line 146 + ]); + } else { + // 一時金・解約・再発行データの場合:定期フィールドは0固定 (仕様line 186-202) + $insertData = array_merge($insertData, [ + 'regular_new_count' => 0, // 仕様line 186 + 'regular_new_amount' => 0, // 仕様line 187 + 'regular_new_reduction_count' => 0, // 仕様line 188 + 'regular_new_reduction_amount' => 0, // 仕様line 189 + 'regular_update_count' => 0, // 仕様line 190 + 'regular_update_amount' => 0, // 仕様line 191 + 'regular_update_reduction_count' => 0, // 仕様line 192 + 'regular_update_reduction_amount' => 0, // 仕様line 193 + 'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 194 + 'lumpsum' => $data->lumpsum ?? 0, // 仕様line 195 + 'refunds' => $data->refunds ?? 0, // 仕様line 196 + 'other_income' => 0, // 仕様line 197 + 'other_spending' => 0, // 仕様line 198 + 'reissue_count' => $data->reissue_count ?? 0, // 仕様line 199 + 'reissue_amount' => 0 // 仕様line 200: 0固定 + ]); + } + + DB::table('earnings_summary')->insert($insertData); + + Log::debug('売上集計結果登録', [ + 'park_id' => $park->park_id, + 'psection_id' => $data->psection_id, + 'data_type' => $dataType, + 'summary_type' => $aggregationTarget['summary_type'] + ]); + } + + /** + * 【処理5】オペレータキュー作成(駐輪場単位・情報不備がある場合のみ) + * + * 仕様 todo/SHJ-10/SHJ-10.txt:289-317 + * - que_class: 14(集計対象エラー) + * - que_comment: 空(仕様書line 297) + * - que_status: 1(キュー発生) + * - que_status_comment: 空(仕様書line 299) + * - work_instructions: 情報不備メッセージ(仕様書line 300) + * - park_id: 駐輪場ID(仕様 "処理1.駐輪場ID"、パラメータエラー時はnull) + * - operator_id: 9999999(バッチ処理固定値) + * + * @param string $message 情報不備メッセージ + * @param int $batchLogId バッチログID + * @param int|null $parkId 駐輪場ID(パラメータエラー時はnull) + * @return void + */ + private function createOperatorQueue(string $message, int $batchLogId, ?int $parkId = null): void { try { DB::table('operator_que')->insert([ - 'que_class' => 10, // SHJ-10用のクラス + 'que_class' => 14, // 集計対象エラー 'user_id' => null, 'contract_id' => null, - 'park_id' => null, - 'que_comment' => $message, - 'que_status' => 1, // 完了 - 'que_status_comment' => 'バッチ処理完了', - 'work_instructions' => "SHJ-10売上集計処理 BatchLogID: {$batchLogId}", + 'park_id' => $parkId, // 仕様:処理1.駐輪場ID + 'que_comment' => '', // 仕様: 空 + 'que_status' => 1, // キュー発生 + 'que_status_comment' => '', // 仕様: 空 + 'work_instructions' => $message, // 仕様: 情報不備 + 'operator_id' => self::BATCH_OPERATOR_ID, // 9999999 'created_at' => now(), 'updated_at' => now() ]); Log::info('オペレータキュー作成完了', [ 'batch_log_id' => $batchLogId, - 'message' => $message + 'park_id' => $parkId, + 'que_class' => 14, + 'que_status' => 1, + 'operator_id' => self::BATCH_OPERATOR_ID, + 'work_instructions' => $message ]); } catch (\Exception $e) { Log::error('オペレータキュー作成エラー', [ 'batch_log_id' => $batchLogId, + 'park_id' => $parkId, 'error' => $e->getMessage() ]); } } + /** + * SHJ-8 バッチ処理ログ作成 + * + * 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す + * + * @param array $statistics 処理統計情報 + * @return void + */ + private function createShjBatchLog(array $statistics): void + { + try { + // SHJ-8 パラメータ設定 + $deviceId = 9999999; // バッチ処理用固定デバイスID + $processName = 'SHJ-10'; + $jobName = $statistics['job_name']; + $status = $statistics['status']; + $statusComment = $statistics['status_comment'] ?? ''; + + $createdDate = now()->format('Y/m/d'); + $updatedDate = now()->format('Y/m/d'); + + Log::info('SHJ-8 バッチ処理ログ作成', [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'status_comment' => $statusComment + ]); + + // 共通処理 SHJ-8 バッチ処理ログ作成を呼び出し + // BatchLog システムを使用してバッチ処理の実行ログを記録 + BatchLog::createBatchLog( + $processName, + $status, + [ + 'device_id' => $deviceId, + 'job_name' => $jobName, + 'status_comment' => $statusComment, + 'statistics' => $statistics, + 'shj8_params' => [ + 'device_id' => $deviceId, + 'process_name' => $processName, + 'job_name' => $jobName, + 'status' => $status, + 'created_date' => $createdDate, + 'updated_date' => $updatedDate + ] + ], + $statusComment + ); + + } catch (\Exception $e) { + Log::error('SHJ-8 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'statistics' => $statistics + ]); + + // SHJ-8でエラーが発生してもメイン処理は継続 + // エラーログのみ出力 + } + } + /** * 集計種別のラベル取得 * @@ -573,11 +922,11 @@ class ShjTenService { switch ($type) { case 'yearly': - return '年次'; + return '(年次)'; case 'monthly': - return '月次'; + return '(月次)'; default: - return $type; + return ''; } } }