584 lines
20 KiB
PHP
584 lines
20 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Park;
|
||
use App\Models\RegularContract;
|
||
use App\Models\EarningsSummary;
|
||
use App\Models\Psection;
|
||
use App\Models\Batch\BatchLog;
|
||
use App\Models\OperatorQue;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Carbon\Carbon;
|
||
|
||
/**
|
||
* 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;
|
||
|
||
/**
|
||
* 財政年度開始月
|
||
*
|
||
* @var int
|
||
*/
|
||
const FISCAL_START_MONTH = 4;
|
||
|
||
/**
|
||
* コンストラクタ
|
||
*
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* SHJ-10 財政年度売上集計処理メイン実行
|
||
*
|
||
* 処理フロー:
|
||
* 【処理1】集計対象を設定する
|
||
* 【処理2】駐輪場マスタを取得する
|
||
* 【判断1】取得件数判定
|
||
* 【処理3】車種区分毎に算出する
|
||
* 【判断2】取得判定
|
||
* 【処理4】売上集計結果を削除→登録する
|
||
* 【処理5】オペレータキュー作成およびバッチ処理ログを作成する
|
||
*
|
||
* @param string $type 集計種別(yearly/monthly)
|
||
* @param string $target 集計対象
|
||
* @param array $fiscalPeriod 財政期間情報
|
||
* @return array 処理結果
|
||
*/
|
||
public function executeFiscalEarningsAggregation(string $type, string $target, array $fiscalPeriod): array
|
||
{
|
||
$batchLogId = null;
|
||
|
||
try {
|
||
// 【処理1】集計対象を設定する(財政年度ベース)
|
||
$aggregationTarget = $this->setFiscalAggregationTarget($fiscalPeriod);
|
||
|
||
// バッチ処理開始ログ作成
|
||
$batchLog = BatchLog::createBatchLog(
|
||
'shj10',
|
||
BatchLog::STATUS_START,
|
||
[
|
||
'type' => $type,
|
||
'target' => $target,
|
||
'fiscal_period' => $fiscalPeriod,
|
||
'aggregation_target' => $aggregationTarget
|
||
],
|
||
"SHJ-10 売上集計処理開始 ({$type}: {$fiscalPeriod['target_label']})"
|
||
);
|
||
$batchLogId = $batchLog->id;
|
||
|
||
Log::info('SHJ-10 売上集計処理開始', [
|
||
'batch_log_id' => $batchLogId,
|
||
'type' => $type,
|
||
'target' => $target,
|
||
'fiscal_period' => $fiscalPeriod,
|
||
'aggregation_target' => $aggregationTarget
|
||
]);
|
||
|
||
// 【処理2】駐輪場マスタを取得する
|
||
$parkInfo = $this->getParkInformation();
|
||
|
||
// 【判断1】取得件数判定
|
||
if (empty($parkInfo)) {
|
||
$typeLabel = $this->getTypeLabel($type);
|
||
$message = "売上集計({$typeLabel}):駐輪場マスタが存在していません。";
|
||
|
||
// バッチログ更新
|
||
$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' => 0,
|
||
'summary_records' => 0,
|
||
'batch_log_id' => $batchLogId
|
||
];
|
||
}
|
||
|
||
// 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する
|
||
$summaryRecords = 0;
|
||
$processedParks = 0;
|
||
|
||
foreach ($parkInfo as $park) {
|
||
$parkSummaryRecords = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod);
|
||
|
||
if ($parkSummaryRecords > 0) {
|
||
$processedParks++;
|
||
$summaryRecords += $parkSummaryRecords;
|
||
}
|
||
}
|
||
|
||
// 【判断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
|
||
];
|
||
}
|
||
|
||
// バッチ処理完了ログ更新
|
||
$completionMessage = "SHJ-10 売上集計処理正常完了 ({$type}: {$fiscalPeriod['target_label']}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}";
|
||
$batchLog->update([
|
||
'status' => BatchLog::STATUS_SUCCESS,
|
||
'end_time' => now(),
|
||
'message' => $completionMessage,
|
||
'success_count' => 1
|
||
]);
|
||
|
||
// 【処理5】オペレータキュー作成
|
||
$this->createOperatorQueue($completionMessage, $batchLogId);
|
||
|
||
Log::info('SHJ-10 売上集計処理完了', [
|
||
'batch_log_id' => $batchLogId,
|
||
'processed_parks' => $processedParks,
|
||
'summary_records' => $summaryRecords
|
||
]);
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'SHJ-10 売上集計処理が正常に完了しました',
|
||
'processed_parks' => $processedParks,
|
||
'summary_records' => $summaryRecords,
|
||
'batch_log_id' => $batchLogId
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
$errorMessage = 'SHJ-10 売上集計処理でエラーが発生: ' . $e->getMessage();
|
||
|
||
if (isset($batchLog) && $batchLog) {
|
||
$batchLog->update([
|
||
'status' => BatchLog::STATUS_ERROR,
|
||
'end_time' => now(),
|
||
'message' => $errorMessage,
|
||
'error_details' => $e->getMessage(),
|
||
'error_count' => 1
|
||
]);
|
||
}
|
||
|
||
Log::error('SHJ-10 売上集計処理エラー', [
|
||
'batch_log_id' => $batchLogId,
|
||
'exception' => $e->getMessage(),
|
||
'trace' => $e->getTraceAsString()
|
||
]);
|
||
|
||
return [
|
||
'success' => false,
|
||
'message' => $errorMessage,
|
||
'details' => $e->getMessage(),
|
||
'batch_log_id' => $batchLogId
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 【処理1】財政年度集計対象を設定する
|
||
*
|
||
* @param array $fiscalPeriod 財政期間情報
|
||
* @return array 集計対象情報
|
||
*/
|
||
private function setFiscalAggregationTarget(array $fiscalPeriod): array
|
||
{
|
||
return [
|
||
'type' => $fiscalPeriod['type'],
|
||
'start_date' => $fiscalPeriod['start_date'],
|
||
'end_date' => $fiscalPeriod['end_date'],
|
||
'summary_type' => $fiscalPeriod['summary_type'],
|
||
'fiscal_year' => $fiscalPeriod['fiscal_year'],
|
||
'target_label' => $fiscalPeriod['target_label']
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 【処理2】駐輪場マスタを取得する
|
||
*
|
||
* 仕様書のSQLクエリに基づく駐輪場情報取得
|
||
* SELECT 駐輪場ID, 駐輪場名
|
||
* FROM 駐輪場マスタ
|
||
* WHERE 閉設フラグ <> 1
|
||
* ORDER BY 駐輪場ふりがな
|
||
*
|
||
* @return array 駐輪場情報
|
||
*/
|
||
private function getParkInformation(): array
|
||
{
|
||
try {
|
||
$parkInfo = DB::table('park')
|
||
->select(['park_id', 'park_name'])
|
||
->where('park_close_flag', '<>', 1)
|
||
->orderBy('park_ruby')
|
||
->get()
|
||
->toArray();
|
||
|
||
Log::info('駐輪場マスタ取得完了', [
|
||
'park_count' => count($parkInfo)
|
||
]);
|
||
|
||
return $parkInfo;
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('駐輪場マスタ取得エラー', [
|
||
'error' => $e->getMessage()
|
||
]);
|
||
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 駐輪場毎の財政年度売上集計処理
|
||
*
|
||
* @param object $park 駐輪場情報
|
||
* @param array $aggregationTarget 集計対象
|
||
* @param array $fiscalPeriod 財政期間情報
|
||
* @return int 作成された集計レコード数
|
||
*/
|
||
private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): int
|
||
{
|
||
try {
|
||
// 【処理4】既存の売上集計結果を削除
|
||
$this->deleteExistingFiscalSummary($park->park_id, $aggregationTarget);
|
||
|
||
// 【処理3】車種区分毎に算出する
|
||
$psections = $this->getPsectionInformation();
|
||
$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++;
|
||
}
|
||
}
|
||
|
||
Log::info('駐輪場財政年度売上集計完了', [
|
||
'park_id' => $park->park_id,
|
||
'park_name' => $park->park_name,
|
||
'summary_records' => $summaryRecords,
|
||
'fiscal_period' => $fiscalPeriod['target_label']
|
||
]);
|
||
|
||
return $summaryRecords;
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('駐輪場財政年度売上集計エラー', [
|
||
'park_id' => $park->park_id,
|
||
'error' => $e->getMessage()
|
||
]);
|
||
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 車種区分情報取得
|
||
*
|
||
* @return array 車種区分情報
|
||
*/
|
||
private function getPsectionInformation(): array
|
||
{
|
||
return DB::table('psection')
|
||
->select(['psection_id', 'psection_subject'])
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 【処理3】車種区分毎に財政年度売上を算出する
|
||
*
|
||
* 4つの項目を計算:
|
||
* ①売上・件数
|
||
* ②一時金売上
|
||
* ③解約返戻金
|
||
* ④再発行金額・件数
|
||
*
|
||
* @param int $parkId 駐輪場ID
|
||
* @param int $psectionId 車種区分ID
|
||
* @param array $aggregationTarget 集計対象
|
||
* @return array 売上データ
|
||
*/
|
||
private function calculateFiscalEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array
|
||
{
|
||
$startDate = $aggregationTarget['start_date'];
|
||
$endDate = $aggregationTarget['end_date'];
|
||
|
||
// ①売上・件数(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
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 売上データの存在チェック
|
||
*
|
||
* @param array $earningsData 売上データ
|
||
* @return bool データが存在するかどうか
|
||
*/
|
||
private function hasEarningsData(array $earningsData): bool
|
||
{
|
||
return $earningsData['sales_count'] > 0 ||
|
||
$earningsData['temporary_count'] > 0 ||
|
||
$earningsData['refund_count'] > 0 ||
|
||
$earningsData['reissue_count'] > 0;
|
||
}
|
||
|
||
/**
|
||
* 【処理4】既存の財政年度売上集計結果を削除
|
||
*
|
||
* @param int $parkId 駐輪場ID
|
||
* @param array $aggregationTarget 集計対象
|
||
* @return void
|
||
*/
|
||
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'])
|
||
->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 // システム処理
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 【処理5】オペレータキュー作成
|
||
*
|
||
* @param string $message メッセージ
|
||
* @param int $batchLogId バッチログID
|
||
* @return void
|
||
*/
|
||
private function createOperatorQueue(string $message, int $batchLogId): void
|
||
{
|
||
try {
|
||
DB::table('operator_que')->insert([
|
||
'que_class' => 10, // SHJ-10用のクラス
|
||
'user_id' => null,
|
||
'contract_id' => null,
|
||
'park_id' => null,
|
||
'que_comment' => $message,
|
||
'que_status' => 1, // 完了
|
||
'que_status_comment' => 'バッチ処理完了',
|
||
'work_instructions' => "SHJ-10売上集計処理 BatchLogID: {$batchLogId}",
|
||
'created_at' => now(),
|
||
'updated_at' => now()
|
||
]);
|
||
|
||
Log::info('オペレータキュー作成完了', [
|
||
'batch_log_id' => $batchLogId,
|
||
'message' => $message
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('オペレータキュー作成エラー', [
|
||
'batch_log_id' => $batchLogId,
|
||
'error' => $e->getMessage()
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 集計種別のラベル取得
|
||
*
|
||
* @param string $type 集計種別
|
||
* @return string ラベル
|
||
*/
|
||
private function getTypeLabel(string $type): string
|
||
{
|
||
switch ($type) {
|
||
case 'yearly':
|
||
return '年次';
|
||
case 'monthly':
|
||
return '月次';
|
||
default:
|
||
return $type;
|
||
}
|
||
}
|
||
}
|