SHJ-4 SHJ-5 SHJ-6 変更点実装
All checks were successful
Deploy api / deploy (push) Successful in 23s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-02-13 19:25:40 +09:00
parent 8499e0a01c
commit 41814dd908
9 changed files with 998 additions and 472 deletions

View File

@ -104,10 +104,12 @@ class ShjFourCCommand extends Command
return self::SUCCESS;
} else {
$this->error('SHJ-4C 室割当処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-4C 室割当処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
// messageキーが無い場合はerror_infoを使用
$errorMessage = $result['message'] ?? $result['error_info'] ?? '不明なエラー';
$this->error('SHJ-4C 車室割り当て処理でエラーが発生しました: ' . $errorMessage);
Log::error('SHJ-4C 車室割り当て処理エラー', [
'error' => $errorMessage,
'result' => $result
]);
return self::FAILURE;

View File

@ -152,6 +152,92 @@ class PaymentCallbackController extends Controller
}
}
/**
* SHJ-4A ウェルネット収納情報受付
*
* POST /api/newwipe/callback/shj4a
* 仕様: JOB1(PUSH受信) JOB2(SHJ-4Bへ渡す) JOB3(応答返却 result=0)
*/
public function receiveShj4a(Request $request): Response
{
try {
// 疎通確認用
if ($request->isMethod('get') && $request->all() === []) {
return response('000', 200)->header('Content-Type', 'text/plain');
}
// 【JOB1】パラメータ解析
$params = $this->parseParams($request);
$retRaw = (string) ($params['ret'] ?? '');
$scdRaw = (string) ($params['scd'] ?? '');
$rnoRaw = (string) ($params['rno'] ?? '');
$kcdRaw = (string) ($params['kcd'] ?? '');
$pwdRaw = (string) ($params['pwd'] ?? '');
$ndt = (string) ($params['ndt'] ?? '');
$tcd = (string) ($params['tcd'] ?? '');
$ccd = (string) ($params['ccd'] ?? '');
$pri = trim((string) ($params['pri'] ?? ''));
$zdt = (string) ($params['zdt'] ?? $params['sdt'] ?? '');
$ifg = (string) ($params['ifg'] ?? '');
$ret = trim($retRaw);
$scd = trim($scdRaw);
$rno = trim($rnoRaw);
$kcd = trim($kcdRaw);
$pwd = trim($pwdRaw);
// 仕様準拠: JOB1->JOB2->JOB3 を維持するため、異常時もJOB2へ渡す
// 受付番号が空の場合はログを残して継続
if ($rno === '') {
\Illuminate\Support\Facades\Log::warning('SHJ-4A: 受付番号が空');
}
// MD5検証仕様準拠のため、失敗時もログを残してJOB2へ継続
$secretKey = config('wellnet.callback.md5_secret');
if (empty($secretKey)) {
if (app()->environment('production')) {
\Illuminate\Support\Facades\Log::critical('WELLNET_MD5_SECRET未設定SHJ-4A');
}
\Illuminate\Support\Facades\Log::warning('WELLNET_MD5_SECRET未設定: MD5検証スキップSHJ-4A');
} else {
$expectedHashRaw = md5("ret{$retRaw}scd{$scdRaw}rno{$rnoRaw}{$secretKey}");
$expectedHashTrim = md5("ret{$ret}scd{$scd}rno{$rno}{$secretKey}");
if ($pwd !== $expectedHashRaw && $pwd !== $expectedHashTrim) {
\Illuminate\Support\Facades\Log::warning('SHJ-4A: MD5検証失敗', [
'contract_payment_number' => $rno,
]);
}
}
// 【JOB2】SHJ-4Bへ渡す
$shjFourBService = app(\App\Services\ShjFourBService::class);
$shjFourBService->processWellnetPush([
'contract_payment_number' => $rno,
'status' => $ret,
'pay_code' => $scd,
'corp_code' => $kcd,
'mms_date' => $zdt,
'cvs_code' => $ccd,
'shop_code' => $tcd,
'pay_date' => !empty($ndt) ? $this->parseDatetime($ndt) : null,
'settlement_amount' => $pri,
'stamp_flag' => $ifg,
'md5_string' => $pwd,
]);
// 【JOB3】応答返却 result=0正常受付
return response('000', 200)->header('Content-Type', 'text/plain');
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('SHJ-4A処理エラー: ' . $e->getMessage(), [
'exception' => $e,
]);
// SHJ-4B処理失敗でもWellnetには正常応答ShjFourBCheckCommandがフォールバック
return response('000', 200)->header('Content-Type', 'text/plain');
}
}
/**
* リクエストパラメータ解析(短縮名・論理名 両方対応)
*/
@ -160,7 +246,7 @@ class PaymentCallbackController extends Controller
$params = [];
// 短縮名で取得
$shortNames = ['ret', 'scd', 'rno', 'kcd', 'sdt', 'ccd', 'tcd', 'nno', 'ndt', 'pri', 'ifg', 'pwd'];
$shortNames = ['ret', 'scd', 'rno', 'kcd', 'sdt', 'zdt', 'ccd', 'tcd', 'nno', 'ndt', 'pri', 'ifg', 'pwd'];
foreach ($shortNames as $name) {
$value = $request->input($name);
if ($value !== null) {

View File

@ -9,7 +9,7 @@ use App\Models\Device;
use Exception;
/**
* SHJ-5 空き待ち通知処理サービス
* SHJ-5 駐輪場空きチェック処理サービス
*
* 駐輪場の空き状況を確認し、空き待ち予約者への通知処理を実行する
* 仕様書に基づくバックグラウンド定期バッチ処理
@ -32,16 +32,17 @@ class ShjFiveService
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-5 メイン処理を実行
*
* 処理フロー:
* 1. 駐輪場の空き状況を取得する
* 2. 空き状況判定
* 3. 空き待ち者の情報を取得する
* 4. 取得件数判定
* 5. 空き待ち者への通知、またはオペレーターキュー追加処理
* 6. バッチ処理ログを作成する
* 処理フロー仕様SHJ-5-1準拠):
* JOB1: 駐輪場の空き状況を取得するSQL-1
* JOB1-STEP1: 空き状況判定(空き台数 > 0 JOB2、空きなし ステータスコメント設定)
* JOB2: 空き待ち者の情報を取得するSQL-2
* JOB2-STEP1: 取得件数判定(> 0 JOB3、なし ステータスコメント設定)
* JOB3: 空き待ち者への通知/オペレーターキュー追加処理
* JOB4: SHJ-8 バッチ処理ログ作成
*
* @return array 処理結果
*/
@ -49,19 +50,20 @@ class ShjFiveService
{
try {
$startTime = now();
Log::info('SHJ-5 空き待ち通知処理開始');
Log::info('SHJ-5 駐輪場空きチェック処理開始');
// 処理統計
$processedParksCount = 0;
$vacantParksCount = 0;
$totalWaitingUsers = 0;
$notificationSuccessCount = 0;
$operatorQueueCount = 0;
$mailErrors = []; // メール異常終了件数専用
$errors = []; // 全体エラー収集用
$allQueueItems = []; // 全オペレーターキュー作成用データ
// 処理統計(グローバル集計用)
$processedParksCount = 0;
$vacantParksCount = 0;
$totalWaitingUsers = 0;
$globalMailSuccessCount = 0;
$globalMailErrorCount = 0;
$globalQueueSuccessCount = 0;
$globalQueueErrorCount = 0;
$errors = [];
$job4LogCount = 0;
// 【処理1】駐輪場の空き状況を取得する
// 【JOB1】駐輪場の空き状況を取得するSQL-1
$parkVacancyList = $this->getParkVacancyStatus();
Log::info('駐輪場空き状況取得完了', [
'total_parks' => count($parkVacancyList)
@ -69,7 +71,6 @@ class ShjFiveService
// 各駐輪場に対する処理
foreach ($parkVacancyList as $parkVacancyData) {
// 配列をオブジェクトに変換
$parkVacancy = (object) $parkVacancyData;
$processedParksCount++;
@ -81,29 +82,38 @@ class ShjFiveService
'vacant_count' => $parkVacancy->vacant_count
]);
// 【判断1】空き状況判定
$statusCommentNameBase = sprintf(
'%s%s%s',
(string) $parkVacancy->park_name,
(string) $parkVacancy->ptype_subject,
(string) $parkVacancy->psection_subject
);
$statusCommentVacancyBase = $statusCommentNameBase . '/空き台数' . (int) $parkVacancy->vacant_count;
// 【JOB1-STEP1】空き状況判定
if ($parkVacancy->vacant_count < 1) {
Log::info('空きなし - 処理スキップ', [
'park_id' => $parkVacancy->park_id,
'vacant_count' => $parkVacancy->vacant_count
]);
// 仕様準拠空きなしはJOB4へステータスコメントは駐輪場/分類/車種のみ)
$this->createBatchProcessLog('success', $statusCommentNameBase);
$job4LogCount++;
Log::info('空きなし', ['park_id' => $parkVacancy->park_id]);
continue;
}
$vacantParksCount++;
// 【処理2】空き待ち者の情報を取得する
// 【JOB2】空き待ち者の情報を取得するSQL-2
$waitingUsers = $this->getWaitingUsersInfo(
$parkVacancy->park_id,
$parkVacancy->psection_id,
$parkVacancy->ptype_id
);
// 【判断2】取得件数判定
// 【JOB2-STEP1】取得件数判定
if (empty($waitingUsers)) {
Log::info('空き待ち者なし', [
'park_id' => $parkVacancy->park_id
]);
// 仕様準拠空き待ち該当者なしはJOB4へ
$this->createBatchProcessLog('success', $statusCommentVacancyBase . ':空き待ち該当者なし');
$job4LogCount++;
Log::info('空き待ち者なし', ['park_id' => $parkVacancy->park_id]);
continue;
}
@ -113,151 +123,120 @@ class ShjFiveService
'waiting_users_count' => count($waitingUsers)
]);
// 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理
// 【JOB3】空き待ち者への通知、またはオペレーターキュー追加処理
$notificationResult = $this->processWaitingUsersNotification(
$waitingUsers,
$parkVacancy
);
$notificationSuccessCount += $notificationResult['notification_success_count'];
$operatorQueueCount += $notificationResult['operator_queue_count'];
// グローバル集計
$globalMailSuccessCount += $notificationResult['mail_success_count'];
$globalMailErrorCount += $notificationResult['mail_error_count'];
$globalQueueSuccessCount += $notificationResult['queue_success_count'];
$globalQueueErrorCount += $notificationResult['queue_error_count'];
if (!empty($notificationResult['errors'])) {
$mailErrors = array_merge($mailErrors, $notificationResult['errors']);
$errors = array_merge($errors, $notificationResult['errors']);
}
// オペレーターキュー作成用データを収集
if (!empty($notificationResult['queue_items'])) {
$allQueueItems = array_merge($allQueueItems ?? [], $notificationResult['queue_items']);
}
$reserveIdText = !empty($notificationResult['processed_reserve_ids'])
? implode('、', $notificationResult['processed_reserve_ids'])
: '-';
$batchComment = (string) ($notificationResult['batch_comment'] ?? '');
// 仕様書記載の形式に合わせたステータスコメント
$statusComment = $statusCommentVacancyBase
. '対象予約ID ' . $reserveIdText
. '' . $batchComment
. ':メール正常終了件数' . $notificationResult['mail_success_count']
. '、メール異常終了件数' . $notificationResult['mail_error_count']
. '、キュー登録正常終了件数' . $notificationResult['queue_success_count']
. '、キュー登録異常終了件数' . $notificationResult['queue_error_count'];
$status = ($notificationResult['mail_error_count'] > 0
|| $notificationResult['queue_error_count'] > 0)
? 'error'
: 'success';
$this->createBatchProcessLog($status, $statusComment);
$job4LogCount++;
Log::info('駐輪場処理完了', [
'park_id' => $parkVacancy->park_id,
'mail_success_count' => $notificationResult['mail_success_count'],
'mail_error_count' => $notificationResult['mail_error_count'],
'queue_success_count' => $notificationResult['queue_success_count'],
'queue_error_count' => $notificationResult['queue_error_count'],
]);
}
// 【処理4】仕様書準拠先に呼び出し、成功時のみ内部変数を更新
$queueErrorCount = 0; // キュー登録異常終了件数(累計)
$queueSuccessCount = 0; // キュー登録正常終了件数(累計)
foreach ($allQueueItems as $queueItem) {
// 仕様書準拠:在呼叫前先計算"如果這次成功會是第幾件",確保記錄反映最新件數
$predictedSuccessCount = $queueSuccessCount + 1;
$predictedErrorCount = $queueErrorCount; // 暂时保持当前错误计数
$queueResult = $this->addToOperatorQueue(
$queueItem['waiting_user'],
$queueItem['park_vacancy'],
$queueItem['batch_comment'],
$notificationSuccessCount, // 最終メール正常終了件数
$predictedSuccessCount, // 預測成功時的件數(包含本次)
count($mailErrors), // 現在のメール異常終了件数(動態計算)
$predictedErrorCount // 現在のキュー登録異常終了件数
);
// 仕様書:根据实际结果决定是否采用预测值
if ($queueResult['success']) {
$queueSuccessCount = $predictedSuccessCount; // 采用预测的成功计数
} else {
$queueErrorCount++; // 失败时递增错误计数
// 仕様書:包含具体错误消息,满足"エラーメッセージ/スタックトレースを保持"要求
$errorDetail = $queueResult['error'] ?? 'Unknown error';
$queueErrorInfo = sprintf('キュー登録失敗:予約ID:%d - %s',
$queueItem['waiting_user']->reserve_id ?? 0,
$errorDetail
);
$errors[] = $queueErrorInfo; // 加入总错误统计(包含具体原因)
Log::error('オペレーターキュー作成失敗', [
'user_id' => $queueItem['waiting_user']->user_id,
'reserve_id' => $queueItem['waiting_user']->reserve_id ?? 0,
'error' => $errorDetail
]);
}
// JOB1対象が0件の場合もJOB4ログを残す
if ($processedParksCount === 0) {
$this->createBatchProcessLog('success', '対象駐輪場データなし');
$job4LogCount++;
}
$endTime = now();
$duration = $startTime->diffInSeconds($endTime);
Log::info('SHJ-5 空き待ち通知処理完了', [
Log::info('SHJ-5 駐輪場空きチェック処理完了', [
'duration_seconds' => $duration,
'processed_parks_count' => $processedParksCount,
'vacant_parks_count' => $vacantParksCount,
'total_waiting_users' => $totalWaitingUsers,
'notification_success_count' => $notificationSuccessCount,
'operator_queue_success_count' => $queueSuccessCount, // 仕様書:正常完了件数
'queue_error_count' => $queueErrorCount,
'mail_error_count' => count($mailErrors), // メール異常終了件数(分離)
'total_error_count' => count($errors) // 全体エラー件数
'total_error_count' => count($errors)
]);
// 仕様書に基づく内部変数.ステータスコメント生成
$statusComment = sprintf(
'メール正常終了件数:%dメール異常終了件数%dキュー登録正常終了件数%dキュー登録異常終了件数%d',
$notificationSuccessCount,
count($mailErrors), // メール異常終了件数(キュー失敗を除外)
$queueSuccessCount ?? 0, // 実際のキュー登録成功件数
$queueErrorCount ?? 0 // 実際のキュー登録失敗件数
);
// SHJ-8 バッチ処理ログ作成
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-5',
'SHJ-5空き待ち通知',
'success',
$statusComment,
$today,
$today
);
Log::info('SHJ-8 バッチ処理ログ作成完了');
} catch (Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage()
]);
}
return [
'success' => true,
'message' => 'SHJ-5 空き待ち通知処理が正常に完了しました',
'message' => 'SHJ-5 駐輪場空きチェック処理が正常に完了しました',
'processed_parks_count' => $processedParksCount,
'vacant_parks_count' => $vacantParksCount,
'total_waiting_users' => $totalWaitingUsers,
'notification_success_count' => $notificationSuccessCount,
'operator_queue_count' => $queueSuccessCount ?? 0, // 仕様書:正常完了件数を使用
'notification_success_count' => $globalMailSuccessCount,
'operator_queue_count' => $globalQueueSuccessCount,
'mail_error_count' => $globalMailErrorCount,
'queue_error_count' => $globalQueueErrorCount,
'error_count' => count($errors),
'errors' => $errors,
'duration_seconds' => $duration,
'status_comment' => $statusComment // SHJ-8用の完全なステータスコメント
'job4_log_count' => $job4LogCount,
'status_comment' => sprintf(
'処理対象駐輪場数:%d空きあり駐輪場数:%dメール正常:%dメール異常:%dキュー正常:%dキュー異常:%d',
$processedParksCount,
$vacantParksCount,
$globalMailSuccessCount,
$globalMailErrorCount,
$globalQueueSuccessCount,
$globalQueueErrorCount
)
];
} catch (Exception $e) {
Log::error('SHJ-5 空き待ち通知処理でエラーが発生', [
Log::error('SHJ-5 駐輪場空きチェック処理でエラーが発生', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// 異常時もJOB4errorを出力する
$this->createBatchProcessLog('error', 'SHJ-5処理エラー' . $e->getMessage());
return [
'success' => false,
'message' => 'SHJ-5 空き待ち通知処理でエラーが発生: ' . $e->getMessage(),
'message' => 'SHJ-5 駐輪場空きチェック処理でエラーが発生: ' . $e->getMessage(),
'error_details' => $e->getMessage()
];
}
}
/**
* 処理1】駐輪場の空き状況を取得する
* JOB1】駐輪場の空き状況を取得する
*
* 仕様書に基づくSQLSQL-1:
* - 「駐輪場マスタ」と「ゾーンマスタ」より空き状況を取得する
* - zone表から zone_number現在契約台数、zone_tolerance限界収容台数を取得
* - 空き台数 = 限界収容台数 - 現在契約台数
*
* @return array 駐輪場空き状況リスト
* @return array 駐輪場空き状況リスト(全レコード、フィルターなし)
*/
private function getParkVacancyStatus(): array
{
@ -290,7 +269,7 @@ class ShjFiveService
->get()
->map(function($record) {
// 【JOB1-STEP1】空き台数 = 限界収容台数 - 現在契約台数
$vacant_count = max(0, $record->sum_zone_tolerance - $record->sum_zone_number);
$vacant_count = (int) $record->sum_zone_tolerance - (int) $record->sum_zone_number;
return (object)[
'park_id' => $record->park_id,
@ -306,17 +285,10 @@ class ShjFiveService
'vacant_count' => $vacant_count, // 内部変数.空き台数
];
})
->filter(function($zone) {
// 【判断1】空き状況判定空きがあるもののみ
return $zone->vacant_count > 0;
})
->values();
Log::info('駐輪場空き状況取得完了仕様書SQL-1準拠', [
'total_records' => count($vacancyList),
'vacant_records' => $vacancyList->filter(function($v) {
return $v->vacant_count > 0;
})->count()
'total_records' => count($vacancyList)
]);
return $vacancyList->toArray();
@ -330,9 +302,9 @@ class ShjFiveService
}
/**
* 処理2】空き待ち者の情報を取得する
* JOB2】空き待ち者の情報を取得する
*
* 仕様書JOB2に基づくSQL
* 仕様書JOB2に基づくSQLSQL-2
* - 空きが発生している「駐輪場ID」「駐輪分類ID」「車種区分ID」で空き待ちしている利用者を抽出する
* - reserve表 + user表のみ仕様書準拠
* - JOIN条件T1.user_id = T2.user_seq重要
@ -355,11 +327,6 @@ class ShjFiveService
'T2.user_primemail', // メールアドレス
'T2.user_submail', // 予備メールアドレス
'T1.reserve_manual', // 手動通知
// 以下は処理に必要な追加フィールド
'T1.park_id',
'T1.psection_id',
'T1.ptype_id',
'T1.reserve_date',
])
// 仕様書準拠T1.user_id = T2.user_sequser表のPK
->join('user as T2', 'T1.user_id', '=', 'T2.user_seq')
@ -396,57 +363,67 @@ class ShjFiveService
}
/**
* 処理3】空き待ち者への通知、またはオペレーターキュー追加処理
* JOB3】空き待ち者への通知、またはオペレーターキュー追加処理
*
* 仕様書に基づく分岐処理:
* - 手動通知フラグ判定reserve_manual
* - メール送信成功時のreserve.sent_date更新
* - 失敗時のオペレーターキュー追加(最終統計で処理)
* 仕様SHJ-5-1準拠の分岐処理:
* - 手動通知 <> 1: SHJ-7メール送信 成功時SQL-3更新 / 失敗時はカウント+バッチコメントのみ
* - 手動通知 = 1: SQL-4オペレーターキュー登録
*
* @param array $waitingUsers 空き待ち者リスト
* @param object $parkVacancy 駐輪場空き情報
* @return array 通知処理結果
* @return array 通知処理結果(駐輪場毎の統計)
*/
private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array
{
$notificationSuccessCount = 0;
$operatorQueueCount = 0;
// 仕様準拠:駐輪場毎のカウント
$mailSuccessCount = 0;
$mailErrorCount = 0;
$queueSuccessCount = 0;
$queueErrorCount = 0;
$errors = [];
$queueItems = []; // オペレーターキュー作成用データ収集
$processedReserveIds = []; // 仕様:内部変数.対象予約ID
$batchComments = []; // 仕様:内部変数.バッチコメント
$queuedIds = []; // SQL-4で登録したキューID
try {
// 空きがある分だけ処理(先着順)
$availableSpots = min($parkVacancy->vacant_count, count($waitingUsers));
for ($i = 0; $i < $availableSpots; $i++) {
$waitingUserData = $waitingUsers[$i];
// 配列をオブジェクトに変換
$waitingUser = (object) $waitingUserData;
$waitingUser = (object) $waitingUsers[$i];
$processedReserveIds[] = $waitingUser->reserve_id;
try {
// 【仕様判断】手動通知フラグチェック
if ($waitingUser->reserve_manual == 1) {
// 手動通知 → オペレーターキュー作成データ収集
$batchComment = '手動通知フラグ設定のため予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
// 手動通知 = 1 → SQL-4 オペレーターキュー登録
$queueResult = $this->addToOperatorQueue($waitingUser, $parkVacancy);
Log::info('手動通知フラグによりオペレーターキュー登録予定', [
'user_id' => $waitingUser->user_id,
'reserve_id' => $waitingUser->reserve_id
]);
if ($queueResult['success']) {
// 仕様:キュー登録正常終了件数+1
$queueSuccessCount++;
if (!empty($queueResult['que_id'])) {
$queuedIds[] = (int) $queueResult['que_id'];
}
Log::info('オペレーターキュー登録成功', [
'user_id' => $waitingUser->user_id,
'reserve_id' => $waitingUser->reserve_id
]);
} else {
// 仕様:キュー登録異常終了件数+1 → バッチコメント設定
$queueErrorCount++;
$errorMsg = $queueResult['error'] ?? 'キュー登録エラー';
$batchComments[] = '予約ID' . $waitingUser->reserve_id . '' . $errorMsg;
$errors[] = $errorMsg;
}
} else {
// 自動通知 → メール送信を試行
// 手動通知 <> 1 → SHJ-7 メール送信
$mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy);
if ($mailResult['success']) {
// メール送信成功 → reserve.sent_date更新
// 仕様:メール送信成功 → SQL-3 reserve更新 → メール正常終了件数+1
$this->updateReserveSentDate($waitingUser->reserve_id);
$notificationSuccessCount++;
$mailSuccessCount++;
Log::info('空き待ち通知メール送信成功', [
'user_id' => $waitingUser->user_id,
@ -454,16 +431,12 @@ class ShjFiveService
'park_id' => $parkVacancy->park_id
]);
} else {
// メール送信失敗 → オペレーターキュー作成データ収集
$shjSevenError = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー';
$batchComment = $shjSevenError . '予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
$errors[] = $shjSevenError;
// 仕様:メール送信失敗 → メール異常終了件数+1 → バッチコメント設定 → 次のユーザーへ
// ※オペレーターキューは作成しない(仕様準拠)
$mailErrorCount++;
$errorMsg = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー';
$batchComments[] = '予約ID' . $waitingUser->reserve_id . '' . $errorMsg;
$errors[] = $errorMsg;
}
}
@ -474,23 +447,56 @@ class ShjFiveService
'error' => $e->getMessage()
]);
// エラー発生時もオペレーターキュー作成データ収集
$batchComment = 'システムエラー:' . $e->getMessage() . '予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
// 仕様準拠:分岐に応じて異常件数を加算
if ((int) $waitingUser->reserve_manual === 1) {
$queueErrorCount++;
} else {
$mailErrorCount++;
}
$batchComments[] = '予約ID' . $waitingUser->reserve_id . ':システムエラー:' . $e->getMessage();
$errors[] = $e->getMessage();
}
}
// SQL関連仕様準拠que_status_commentに集計情報を反映
if (!empty($queuedIds)) {
$reserveIdText = !empty($processedReserveIds)
? implode('、', $processedReserveIds)
: '-';
$batchCommentText = implode('、', $batchComments);
$queueStatusComment = sprintf(
'%s%s%s空き台数%d対象予約ID %s%sメール正常終了件数%d、メール異常終了件数%d、キュー登録正常終了件数%d、キュー登録異常終了件数%d',
(string) $parkVacancy->park_name,
(string) $parkVacancy->ptype_subject,
(string) $parkVacancy->psection_subject,
(int) $parkVacancy->vacant_count,
$reserveIdText,
$batchCommentText,
$mailSuccessCount,
$mailErrorCount,
$queueSuccessCount,
$queueErrorCount
);
DB::table('operator_que')
->whereIn('que_id', $queuedIds)
->update([
'que_status_comment' => mb_strimwidth($queueStatusComment, 0, 255, ''),
'work_instructions' => '空き待ち者への連絡をお願いします。',
'updated_at' => now(),
]);
}
return [
'notification_success_count' => $notificationSuccessCount,
'operator_queue_count' => $operatorQueueCount,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'processed_reserve_ids' => $processedReserveIds,
'batch_comment' => implode('、', $batchComments),
'errors' => $errors,
'queue_items' => $queueItems // 後でキュー作成用
];
} catch (Exception $e) {
@ -578,7 +584,7 @@ class ShjFiveService
}
/**
* reserve.sent_date及びvalid_flag更新
* SQL-3: reserve.sent_date及びvalid_flag更新
*
* 仕様書準拠メール送信成功時にreserve.sent_dateとvalid_flag=0を同時更新
* 重複通知を防ぎ、処理済みマークを設定
@ -613,74 +619,51 @@ class ShjFiveService
}
/**
* オペレーターキューに追加
* SQL-4: オペレーターキューに追加
*
* 仕様書に基づくキュー登録:
* SHJ-5-1準拠のキュー登録:
* - que_comment: 空文字列
* - que_status_comment: 仕様書完全準拠形式(統計情報含む)
* - que_status_comment: 初期登録は空文字列(フロー仕様)
* - work_instructions: 初期登録は空文字列(フロー仕様)
* - operator_id: 9999999固定
*
* @param object $waitingUser 空き待ち者情報
* @param object $parkVacancy 駐輪場空き情報
* @param string $batchComment 内部変数.バッチコメント
* @param int $mailSuccessCount メール正常終了件数
* @param int $queueSuccessCount キュー登録正常終了件数
* @param int $mailErrorCount メール異常終了件数
* @param int $queueErrorCount キュー登録異常終了件数
* @return array 追加結果
*/
private function addToOperatorQueue(object $waitingUser, object $parkVacancy, string $batchComment, int $mailSuccessCount, int $queueSuccessCount, int $mailErrorCount, int $queueErrorCount): array
private function addToOperatorQueue(object $waitingUser, object $parkVacancy): array
{
try {
// 仕様書完全準拠駐輪場名駐輪分類名車種区分名空き台数…対象予約ID…内部変数.バッチコメント/内部変数.メール正常終了件数…メール異常終了件数…キュー登録正常終了件数…キュー登録異常終了件数…
$statusComment = sprintf(
'%s%s%s空き台数%d台対象予約ID%d%sメール正常終了件数%dメール異常終了件数%dキュー登録正常終了件数%dキュー登録異常終了件数%d',
$parkVacancy->park_name ?? '', // JOB1から取得
$parkVacancy->ptype_subject ?? '', // 駐輪分類名JOB1から取得
$parkVacancy->psection_subject ?? '', // 車種区分名JOB1から取得
$parkVacancy->vacant_count ?? 0,
$waitingUser->reserve_id ?? 0,
$batchComment, // 内部変数.バッチコメント
$mailSuccessCount, // 内部変数.メール正常終了件数
$mailErrorCount,
$queueSuccessCount,
$queueErrorCount
);
OperatorQue::create([
$queue = OperatorQue::create([
'que_class' => 4, // 予約告知通知
'user_id' => $waitingUser->user_id,
'contract_id' => null,
'park_id' => $waitingUser->park_id,
'park_id' => $parkVacancy->park_id,
'que_comment' => '', // 仕様書:空文字列
'que_status' => 1, // キュー発生
'que_status_comment' => $statusComment, // 仕様書:完全準拠形式
'work_instructions' => '空き待ち者への連絡をお願いします。',
'que_status_comment' => '', // フロー仕様: 空文字列
'work_instructions' => '', // フロー仕様: 空文字列
'operator_id' => 9999999, // 仕様書固定値9999999
]);
Log::info('オペレーターキュー追加成功', [
'user_id' => $waitingUser->user_id,
'park_id' => $waitingUser->park_id,
'park_id' => $parkVacancy->park_id,
'reserve_id' => $waitingUser->reserve_id,
'que_class' => 4,
'operator_id' => 9999999,
'batch_comment' => $batchComment,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'status_comment' => $statusComment
]);
return ['success' => true];
return [
'success' => true,
'que_id' => (int) $queue->que_id,
];
} catch (Exception $e) {
Log::error('オペレーターキュー追加エラー', [
'user_id' => $waitingUser->user_id,
'park_id' => $waitingUser->park_id,
'park_id' => $parkVacancy->park_id,
'reserve_id' => $waitingUser->reserve_id ?? null,
'batch_comment' => $batchComment,
'error' => $e->getMessage()
]);
@ -690,4 +673,50 @@ class ShjFiveService
];
}
}
/**
* 【JOB4】SHJ-8バッチ処理ログを作成
*
* @param string $status success|error
* @param string $statusComment ステータスコメント
* @return void
*/
private function createBatchProcessLog(string $status, string $statusComment): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
// SHJ-8の制約255文字以内に合わせる
$statusComment = mb_strimwidth($statusComment, 0, 255, '');
if ($statusComment === '') {
$statusComment = 'SHJ-5処理実行';
}
$result = $this->shjEightService->execute(
$deviceId,
'SHJ-5',
'SHJ-5 駐輪場空きチェック',
$status,
$statusComment,
$today,
$today
);
if (($result['result'] ?? 1) !== 0) {
Log::error('SHJ-5 JOB4バッチログ作成失敗', [
'status' => $status,
'status_comment' => $statusComment,
'result' => $result,
]);
}
} catch (Exception $e) {
Log::error('SHJ-5 JOB4バッチログ作成例外', [
'status' => $status,
'status_comment' => $statusComment,
'error' => $e->getMessage(),
]);
}
}
}

View File

@ -9,11 +9,13 @@ use App\Models\PriceA;
use App\Models\BatJobLog;
use App\Models\Device;
use App\Models\User;
use App\Services\ShjFourCService;
use App\Services\ShjThirteenService;
use App\Services\ShjEightService;
use App\Services\ShjMailSendService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
/**
@ -45,6 +47,13 @@ class ShjFourBService
*/
protected $shjEightService;
/**
* SHJ-4C 車室割り当てサービス
*
* @var ShjFourCService
*/
protected $shjFourCService;
/**
* SHJ-7 メール送信サービス
*
@ -57,13 +66,16 @@ class ShjFourBService
*
* @param ShjEightService $shjEightService
* @param ShjMailSendService $mailSendService
* @param ShjFourCService $shjFourCService
*/
public function __construct(
ShjEightService $shjEightService,
ShjMailSendService $mailSendService
ShjMailSendService $mailSendService,
ShjFourCService $shjFourCService
) {
$this->shjEightService = $shjEightService;
$this->mailSendService = $mailSendService;
$this->shjFourCService = $shjFourCService;
}
/**
@ -99,7 +111,7 @@ class ShjFourBService
if (!$contractResult['found']) {
// 対象レコードなしの場合
$result = $this->handleNoTargetRecord($settlement, $contractResult);
$this->createBatchLog($settlement, null, null, true, $result['message']);
$this->createBatchLog($settlement, null, null, true, null, null, null, $result['message']);
return $result;
}
@ -107,7 +119,7 @@ class ShjFourBService
// 登録済みの場合
$result = $this->handleAlreadyProcessed($settlement, $contractResult);
$contract = $contractResult['contract'];
$this->createBatchLog($settlement, $contract, null, true, $result['message']);
$this->createBatchLog($settlement, $contract, null, true, null, null, null, $result['message']);
return $result;
}
@ -117,20 +129,36 @@ class ShjFourBService
$statusResult = $this->judgeReceiptStatus($settlement, $contract);
if (!$statusResult['valid']) {
// 授受状態が異常な場合
// 授受済みの場合(仕様: JOB5メール送信 → JOB6バッチログ → 終了)
$result = $this->handleInvalidStatus($settlement, $contract, $statusResult);
$this->createBatchLog($settlement, $contract, null, true, $result['message']);
// 【JOB5】メール送信授受済みでも送信
$mailResult = $this->sendUserNotificationMail($settlement, $contract);
$mailCommentSuffix = $mailResult['batch_comment_suffix'] ?? null;
// 【JOB6】バッチログ授受済みメッセージを直接statusCommentとして渡す
$this->createBatchLog(
$settlement, $contract, null, true, null, $mailCommentSuffix,
null, $result['message']
);
return $result;
}
// 【判断2】金額チェック
// 【JOB3-0】SHJ-4C 車室割り当て(新規のみ)
// 仕様:定期契約継続フラグ(update_flag) = 2新規の場合に呼び出す
// ※異常があったとしても、処理は継続する
$shjFourCResult = null;
if ((int) $contract->update_flag === 2) {
$shjFourCResult = $this->triggerShjFourC($contract);
}
// 【JOB3-1】契約更新処理実行SHJ-4C結果を含む
// 仕様フロー順: JOB3-0 → JOB3-1/3-4 → JOB3-STEP1金額チェック
$updateResult = $this->executeContractUpdate($settlement, $contract, $shjFourCResult);
// 【判断2】金額チェック仕様のJOB3-STEP1
$amountResult = $this->judgeAmountComparison($settlement, $contract);
// 【処理3】契約更新処理実行
$updateResult = $this->executeContractUpdate($settlement, $contract, $amountResult);
// 副作用処理実行
$sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult);
// 副作用処理実行SHJ-4C結果をSHJ-13に渡す
$sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult, $shjFourCResult);
$result = [
'success' => true,
@ -147,7 +175,8 @@ class ShjFourBService
// 【処理】バッチ処理ログ作成SHJ-8呼び出し
$mailCommentSuffix = $sideEffectResult['user_mail']['batch_comment_suffix'] ?? null;
$this->createBatchLog($settlement, $contract, $amountResult, true, null, $mailCommentSuffix);
$photoDeletionResult = $sideEffectResult['photo_deletion'] ?? null;
$this->createBatchLog($settlement, $contract, $amountResult, true, null, $mailCommentSuffix, $photoDeletionResult);
return $result;
@ -173,6 +202,96 @@ class ShjFourBService
}
}
/**
* ウェルネットPUSHデータ処理入口
*
* 仕様順序: JOB1(SQL-1 契約検索) JOB2(SQL-2 重複チェック SQL-3 INSERT) 既存処理
* 既存の processSettlementTransaction(id) は再処理・後方互換の入口として残す
*
* @param array $wellnetData ウェルネットからの生データ
* @return array 処理結果
*/
public function processWellnetPush(array $wellnetData): array
{
$contractPaymentNumber = $wellnetData['contract_payment_number'];
Log::info('SHJ-4B ウェルネットPUSHデータ処理開始', [
'contract_payment_number' => $contractPaymentNumber,
]);
// 【JOB1】SQL-1: 定期契約マスタの対象レコード取得judgeTargetContractと同一条件
$contract = DB::table('regular_contract as T1')
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->join('price_a as T4', function($join) {
$join->on('T1.price_parkplaceid', '=', 'T4.price_parkplaceid')
->on('T1.park_id', '=', 'T4.park_id');
})
->where('T1.contract_payment_number', $contractPaymentNumber)
->where('T1.contract_cancel_flag', '!=', 1)
->whereNotNull('T1.contract_flag')
->select('T1.contract_id')
->first();
// JOB1-STEP1: 対象レコード判定
if (!$contract) {
Log::warning('SHJ-4B JOB1: 受付番号不正(対象契約なし)', [
'contract_payment_number' => $contractPaymentNumber,
]);
// 仕様: JOB6 バッチログ → 終了settlement_transaction未作成
$statusComment = "受付番号が不正です。(受付番号:{$contractPaymentNumber}";
$this->createWellnetPushBatchLog($statusComment);
return [
'success' => true,
'result' => 'no_target',
'message' => $statusComment,
];
}
// 【JOB2】SQL-2: 重複チェック
// 補足: 受付番号は contract_payment_numberrnoで照合する
$existCount = DB::table('settlement_transaction')
->where('contract_payment_number', $contractPaymentNumber)
->count();
if ($existCount >= 1) {
Log::info('SHJ-4B SQL-2: 既に登録済み', [
'contract_payment_number' => $contractPaymentNumber,
]);
return [
'success' => true,
'result' => 'already_registered',
'message' => '決済トランザクションは既に登録済みです',
];
}
// 【SQL-3】決済トランザクション挿入<JOB1>.定期契約ID を使用)
$settlementId = DB::table('settlement_transaction')->insertGetId([
'contract_id' => $contract->contract_id,
'status' => $wellnetData['status'] ?? null,
'pay_code' => $wellnetData['pay_code'] ?? null,
'contract_payment_number' => $contractPaymentNumber,
'corp_code' => $wellnetData['corp_code'] ?? null,
'mms_date' => $wellnetData['mms_date'] ?? null,
'cvs_code' => $wellnetData['cvs_code'] ?? null,
'shop_code' => $wellnetData['shop_code'] ?? null,
'pay_date' => $wellnetData['pay_date'] ?? null,
'settlement_amount' => $wellnetData['settlement_amount'] ?? null,
'stamp_flag' => $wellnetData['stamp_flag'] ?? null,
'md5_string' => $wellnetData['md5_string'] ?? null,
'created_at' => now(),
'updated_at' => now(),
]);
Log::info('SHJ-4B SQL-3: 決済トランザクション登録', [
'settlement_transaction_id' => $settlementId,
'contract_payment_number' => $contractPaymentNumber,
'contract_id' => $contract->contract_id,
]);
// JOB2-STEP2以降: 既存フローへ
return $this->processSettlementTransaction($settlementId);
}
/**
* 決済トランザクション取得
*
@ -212,7 +331,7 @@ class ShjFourBService
'contract_payment_number' => $settlement->contract_payment_number,
]);
// 文档要求のSQL構造に基づく対象レコード取得
// 仕様書要件のSQL構造に基づく対象レコード取得
// regular_contract T1 inner join park T2 inner join price_a T4
$contractQuery = DB::table('regular_contract as T1')
->select([
@ -234,13 +353,17 @@ class ShjFourBService
'T1.contract_created_at',
'T1.contract_cancel_flag',
'T2.park_name',
'T2.immediate_use_permit',
'T2.update_grace_period_start_date',
'T2.update_grace_period_end_date',
'T2.parking_start_grace_period',
'T4.price_month',
'T4.price'
])
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->join('price_a as T4', function($join) {
$join->on('T1.price_parkplaceid', '=', 'T4.price_parkplaceid')
->on('T1.park_id', '=', 'T4.park_id'); // 文档要求の第二条件追加
->on('T1.park_id', '=', 'T4.park_id'); // 仕様書要件の第2条件
})
->where('T1.contract_payment_number', $settlement->contract_payment_number)
->where('T1.contract_cancel_flag', '!=', 1) // 解約されていない
@ -257,7 +380,7 @@ class ShjFourBService
'found' => false,
'contract' => null,
'reason' => '対象レコードなし',
'message' => "契約番号に一致する有効な定期契約が見つかりません: {$settlement->contract_payment_number}",
'message' => "受付番号が不正です。(受付番号:{$settlement->contract_payment_number}",
];
}
@ -366,49 +489,36 @@ class ShjFourBService
*/
private function judgeReceiptStatus(SettlementTransaction $settlement, $contract): array
{
// 授受状態の基本チェック
$statusChecks = [
'settlement_status' => $settlement->status === 'received',
'pay_date_exists' => !empty($settlement->pay_date),
'settlement_amount_valid' => $settlement->settlement_amount > 0,
'contract_not_cancelled' => $contract->contract_cancel_flag != 1,
];
$allValid = array_reduce($statusChecks, function($carry, $check) {
return $carry && $check;
}, true);
if (!$allValid) {
$failedChecks = array_keys(array_filter($statusChecks, function($check) {
return !$check;
}));
Log::warning('SHJ-4B 判断1: 授受状態異常', [
// 仕様JOB2-STEP2: 授受フラグ = 0 の場合、処理続行
if ((int) $contract->contract_flag === 0) {
Log::info('SHJ-4B 判断1: 授受フラグ=0 処理続行', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'failed_checks' => $failedChecks,
'status_checks' => $statusChecks,
]);
return [
'valid' => false,
'reason' => '授受状態異常',
'failed_checks' => $failedChecks,
'message' => '決済トランザクションまたは契約の状態が更新処理に適していません',
'valid' => true,
'reason' => '授受フラグ未処理',
'message' => '授受フラグチェックに合格しました',
];
}
Log::info('SHJ-4B 判断1: 授受状態正常', [
// その他の場合(授受済み)
$message = sprintf(
'支払いステータスチェック授受済みです。定期契約ID%s',
$contract->contract_id
);
Log::warning('SHJ-4B 判断1: 授受済み', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'status_checks' => $statusChecks,
'contract_flag' => $contract->contract_flag,
]);
return [
'valid' => true,
'reason' => '授受状態正常',
'status_checks' => $statusChecks,
'message' => '授受状態チェックに合格しました',
'valid' => false,
'reason' => '授受済み',
'message' => $message,
];
}
@ -421,7 +531,7 @@ class ShjFourBService
*/
private function judgeAmountComparison(SettlementTransaction $settlement, $contract): array
{
// 文档要求:請求額=授受額の厳密比較
// 仕様書要件: 請求額=授受額の厳密比較
$billingAmount = (int) $contract->billing_amount; // 整数として比較
$settlementAmount = (int) $settlement->settlement_amount; // 整数として比較
@ -486,49 +596,45 @@ class ShjFourBService
}
/**
* パターンA/B判断
* 【SQL-4】シール印刷可能日を計算仕様パターンA/B準拠
*
* 月を跨らないパターンAvs 月を跨るパターンBの判定
* パターン判定:
* - 即利用許可=1 入金日の年月日
* - 更新期間開始日 <= 更新期間終了日パターンA:
* - A-1: 駐輪開始猶予期間 > 本日の日 入金日の年月 + 猶予期間日
* - A-2: それ以外 入金日の年月日
* - 更新期間開始日 > 更新期間終了日パターンB 入金日の年月日
*
* @param object $contract
* @param SettlementTransaction $settlement
* @return array
* @param object $contract
* @return string Y-m-d形式の日付
*/
private function judgeContractPattern($contract, SettlementTransaction $settlement): array
private function calculatePrintableDate(SettlementTransaction $settlement, $contract): string
{
$payDate = Carbon::parse($settlement->pay_date);
$contractStart = !empty($contract->contract_periods) ? Carbon::parse($contract->contract_periods) : null;
$contractEnd = !empty($contract->contract_periode) ? Carbon::parse($contract->contract_periode) : null;
// パターン判定ロジック
$isPatternB = false; // デフォルトはパターンA
$patternReason = 'パターンA月を跨らない';
if ($contractEnd) {
// 支払日が契約終了月の翌月以降の場合、パターンB跨月
if ($payDate->month > $contractEnd->month || $payDate->year > $contractEnd->year) {
$isPatternB = true;
$patternReason = 'パターンB月を跨る';
}
// 契約後即利用許可 = 1
if ((int) ($contract->immediate_use_permit ?? 0) === 1) {
return $payDate->format('Y-m-d');
}
Log::info('SHJ-4B パターン判定', [
'contract_id' => $contract->contract_id,
'pay_date' => $payDate->format('Y-m-d'),
'contract_periods' => $contractStart?->format('Y-m-d'),
'contract_periode' => $contractEnd?->format('Y-m-d'),
'pattern' => $isPatternB ? 'B' : 'A',
'reason' => $patternReason,
]);
$startDate = (int) ($contract->update_grace_period_start_date ?? 0);
$endDate = (int) ($contract->update_grace_period_end_date ?? 0);
$gracePeriod = (int) ($contract->parking_start_grace_period ?? 0);
$todayDay = (int) now()->format('d');
return [
'pattern' => $isPatternB ? 'B' : 'A',
'is_pattern_b' => $isPatternB,
'reason' => $patternReason,
'pay_date' => $payDate,
'contract_start' => $contractStart,
'contract_end' => $contractEnd,
];
// パターンA: 更新期間開始日 <= 更新期間終了日
if ($startDate <= $endDate) {
if ($gracePeriod > $todayDay) {
// A-1: 入金日の年月 + 猶予期間日
return $payDate->format('Y-m') . '-' . str_pad($gracePeriod, 2, '0', STR_PAD_LEFT);
}
// A-2: 入金日の年月日
return $payDate->format('Y-m-d');
}
// パターンB: 全サブケース同一 → 入金日の年月日
return $payDate->format('Y-m-d');
}
/**
@ -560,87 +666,78 @@ class ShjFourBService
}
/**
* 処理3】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新
* JOB3-1】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新
*
* @param SettlementTransaction $settlement
* @param object $contract
* @param array $amountResult
* @param array|null $shjFourCResult SHJ-4C車室割り当て結果新規のみ
* @return array
*/
private function executeContractUpdate(
SettlementTransaction $settlement,
$contract,
array $amountResult
?array $shjFourCResult = null
): array {
$updateData = [];
$updated = false;
try {
// パターンA/B判定
$pattern = $this->judgeContractPattern($contract, $settlement);
DB::transaction(function() use ($settlement, $contract, $amountResult, $pattern, &$updateData, &$updated) {
// 基本更新項目
DB::transaction(function() use ($settlement, $contract, $shjFourCResult, &$updateData, &$updated) {
// 【SQL-4】基本更新項目仕様準拠
$updateData = [
'contract_payment_day' => Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'),
'contract_updated_at' => now(),
'contract_money' => $settlement->settlement_amount,
'contact_shop_code' => $settlement->shop_code,
'contract_cvs_class' => $settlement->cvs_code,
'contract_flag' => 1,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_permission' => 1,
'printable_date' => $this->calculatePrintableDate($settlement, $contract),
'contract_payment_number' => $settlement->contract_payment_number,
'updated_at' => now(),
];
// 金額比較結果に基づく contract_flag 設定
switch ($amountResult['comparison']) {
case self::AMOUNT_MATCH:
$updateData['contract_flag'] = self::CONTRACT_FLAG_UPDATED;
$updateData['contract_money'] = $settlement->settlement_amount;
break;
case self::AMOUNT_SHORTAGE:
case self::AMOUNT_EXCESS:
$updateData['contract_flag'] = self::CONTRACT_FLAG_ERROR;
$updateData['contract_money'] = $settlement->settlement_amount;
break;
// 【新規】SHJ-4Cの結果を反映仕様SQL-4準拠
if ((int) $contract->update_flag === 2 && $shjFourCResult
&& ($shjFourCResult['success'] ?? false)
&& !empty($shjFourCResult['zone_id']) && !empty($shjFourCResult['pplace_no'])) {
$updateData['zone_id'] = $shjFourCResult['zone_id'];
$updateData['pplace_no'] = $shjFourCResult['pplace_no'];
}
// パターンBの場合の特殊処理
if ($pattern['is_pattern_b']) {
// 契約期間の延長処理等
if ($pattern['contract_end']) {
$newEndDate = $pattern['contract_end']->addMonth();
$updateData['contract_periode'] = $newEndDate->format('Y-m-d');
}
}
// 【定期契約マスタ更新】
// 【SQL-4】定期契約マスタ更新
$affectedRows = DB::table('regular_contract')
->where('contract_id', $contract->contract_id)
->update($updateData);
$updated = $affectedRows > 0;
// 【定期予約マスタ更新】reserve_idが設定されている場合
// 【SQL-5】契約元の定期契約マスタを更新旧契約の更新済フラグ
if (!empty($contract->old_contract_id)) {
DB::table('regular_contract')
->where('contract_id', $contract->old_contract_id)
->update(['contract_renewal' => 1, 'updated_at' => now()]);
Log::info('SHJ-4B SQL-5 旧契約更新済フラグ更新', [
'old_contract_id' => $contract->old_contract_id,
'contract_id' => $contract->contract_id,
]);
}
// 【SQL-8】定期予約マスタ更新reserve_idがnull以外の場合
if (!empty($contract->reserve_id)) {
$reserveUpdateData = [
'updated_at' => now(),
];
// 金額一致の場合、予約を有効化
if ($amountResult['comparison'] === self::AMOUNT_MATCH) {
$reserveUpdateData['valid_flag'] = 1;
// パターンBの場合、予約期間も延長
if ($pattern['is_pattern_b'] && $pattern['contract_end']) {
$reserveUpdateData['reserve_end'] = $pattern['contract_end']->format('Y-m-d');
}
}
$reserveAffectedRows = DB::table('reserve')
DB::table('reserve')
->where('reserve_id', $contract->reserve_id)
->update($reserveUpdateData);
->update([
'contract_id' => $contract->contract_id,
'contract_created_at' => now(),
'valid_flag' => 0,
'updated_at' => now(),
]);
Log::info('SHJ-4B 定期予約マスタ更新完了', [
'reserve_id' => $contract->reserve_id,
'contract_id' => $contract->contract_id,
'reserve_update_data' => $reserveUpdateData,
'reserve_affected_rows' => $reserveAffectedRows,
]);
}
@ -649,7 +746,6 @@ class ShjFourBService
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'update_data' => $updateData,
'affected_rows' => $affectedRows,
'pattern' => $pattern['pattern'],
]);
});
@ -679,28 +775,30 @@ class ShjFourBService
* @param object $contract
* @param array $amountResult
* @param array $updateResult
* @param array|null $shjFourCResult SHJ-4C車室割り当て結果新規のみ
* @return array
*/
private function executeSideEffects(
SettlementTransaction $settlement,
$contract,
array $amountResult,
array $updateResult
array $updateResult,
?array $shjFourCResult = null
): array {
$sideEffects = [];
try {
// 【処理3】写真削除処理金額一致かつ更新成功の場合
if ($amountResult['comparison'] === self::AMOUNT_MATCH && $updateResult['updated']) {
$sideEffects['photo_deletion'] = $this->executePhotoDeletion($contract);
}
// 【JOB3-2】写真削除処理SQL-4/SQL-5の後、金額比較結果に関わらず実行
$sideEffects['photo_deletion'] = $this->executePhotoDeletion($contract);
// 【新規のみ】SHJ-13実行処理
if ($updateResult['updated'] && $amountResult['comparison'] === self::AMOUNT_MATCH) {
$isNewContract = $this->isNewContract($contract);
if ($isNewContract) {
$sideEffects['shj13_trigger'] = $this->triggerShjThirteen($contract);
}
// 【JOB3-3】写真削除バッチログ独立SHJ-8呼出し
$this->createPhotoDeletionBatchLog($sideEffects['photo_deletion']);
// 【JOB3-STEP1】SHJ-13契約台数追加新規 かつ shortage以外
// 仕様shortage → SHJ-13呼出なし、match/excess → SHJ-13呼出
if ((int) $contract->update_flag === 2
&& $amountResult['comparison'] !== self::AMOUNT_SHORTAGE) {
$sideEffects['shj13_trigger'] = $this->triggerShjThirteen($contract, $shjFourCResult);
}
// 【処理4】異常時のオペレーターキュー登録処理
@ -708,10 +806,8 @@ class ShjFourBService
$sideEffects['operator_queue'] = $this->registerToOperatorQueue($settlement, $contract, $amountResult);
}
// 【処理5】利用者メール送信処理
if ($updateResult['updated']) {
$sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult);
}
// 【JOB5】利用者メール送信処理仕様: フロー到達時は一律送信)
$sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult);
Log::info('SHJ-4B 副作用処理完了', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
@ -778,20 +874,15 @@ class ShjFourBService
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'reason' => $statusResult['reason'],
'failed_checks' => $statusResult['failed_checks'],
]);
// TODO: オペレーターキューへの登録や管理者通知
return [
'success' => false,
'success' => true, // 授受済みは仕様上エラーではなく正常分岐
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'result' => 'invalid_status',
'result' => 'already_received',
'reason' => $statusResult['reason'],
'failed_checks' => $statusResult['failed_checks'],
'message' => $statusResult['message'],
'action_required' => 'オペレーターによる手動処理が必要です',
];
}
@ -803,47 +894,214 @@ class ShjFourBService
*/
private function executePhotoDeletion($contract): array
{
// TODO: 実際の写真削除ロジックを実装
// 現在はプレースホルダー
// 【SQL-6】利用者の写真ファイル名取得
$userData = DB::table('regular_contract as T1')
->join('user as T2', 'T1.user_id', '=', 'T2.user_seq')
->where('T1.contract_payment_number', $contract->contract_payment_number)
->where('T1.contract_flag', 1)
->select('T2.user_id', 'T2.user_seq', 'T2.user_name', 'T2.photo_filename1', 'T2.photo_filename2')
->first();
Log::info('SHJ-4B 写真削除処理実行', [
'contract_id' => $contract->contract_id,
'user_id' => $contract->user_id,
if (!$userData) {
Log::info('SHJ-4B 写真削除: 対象利用者なし', [
'contract_id' => $contract->contract_id,
]);
return ['executed' => false, 'message' => '対象利用者なし'];
}
$deletedFiles = [];
// ファイル削除
foreach (['photo_filename1', 'photo_filename2'] as $field) {
if (!empty($userData->$field)) {
$path = 'photo/' . $userData->$field;
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
$deletedFiles[] = $userData->$field;
}
}
}
// 【SQL-7】利用者の写真ファイル名をNULLに更新
$userUpdateQuery = DB::table('user');
if (!empty($userData->user_id)) {
// 仕様書上の利用者IDuser.user_idで更新
$userUpdateQuery->where('user_id', $userData->user_id);
} else {
// user_id未設定データの後方互換
$userUpdateQuery->where('user_seq', $userData->user_seq);
}
$userUpdateQuery->update([
'photo_filename1' => null,
'photo_filename2' => null,
'updated_at' => now(),
]);
Log::info('SHJ-4B 写真削除処理完了', [
'user_seq' => $userData->user_seq,
'deleted_files' => $deletedFiles,
]);
return [
'executed' => true,
'method' => 'placeholder',
'message' => '写真削除処理は実装予定です',
'user_id' => $userData->user_id,
'user_seq' => $userData->user_seq,
'user_name' => $userData->user_name,
'photo_filename1' => $userData->photo_filename1,
'photo_filename2' => $userData->photo_filename2,
'deleted_files' => $deletedFiles,
];
}
/**
* ウェルネットPUSH処理用バッチログ作成settlement_transaction未作成時
*
* processWellnetPush内でJOB1不合格受付番号不正の場合に使用
*
* @param string $statusComment ステータスコメント
* @return void
*/
private function createWellnetPushBatchLog(string $statusComment): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-4B',
'SHJ-4支払いステータスチェック',
'success',
$statusComment,
$today,
$today
);
} catch (\Exception $e) {
Log::error('SHJ-4B ウェルネットPUSHバッチログ作成エラー', [
'error' => $e->getMessage(),
]);
}
}
/**
* 【JOB3-3】写真削除バッチログ作成独立SHJ-8呼出し)
*
* @param array $photoDeletionResult executePhotoDeletion()の戻り値
* @return void
*/
private function createPhotoDeletionBatchLog(array $photoDeletionResult): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
if ($photoDeletionResult['executed'] ?? false) {
$status = 'success';
$userId = $photoDeletionResult['user_id'] ?? '';
$userName = $photoDeletionResult['user_name'] ?? '';
$file1 = $photoDeletionResult['photo_filename1'] ?? '';
$file2 = $photoDeletionResult['photo_filename2'] ?? '';
$statusComment = "利用者ID:{$userId}、利用者名:{$userName}、ファイル名:{$file1},{$file2}";
} else {
$status = 'error';
$statusComment = $photoDeletionResult['message'] ?? '写真削除対象なし';
}
$this->shjEightService->execute(
$deviceId,
'SHJ-4B',
'SHJ-4B本人確認写真削除',
$status,
$statusComment,
$today,
$today
);
} catch (\Exception $e) {
Log::error('SHJ-4B JOB3-3 写真削除バッチログ作成エラー', [
'error' => $e->getMessage(),
]);
}
}
/**
* 【JOB3-0】SHJ-4C 車室割り当て処理(新規のみ)
*
* 仕様:定期契約継続フラグ = 2(新規)の場合に呼び出す
* ※異常があったとしても、処理は継続する
*
* @param object $contract 契約データ
* @return array|null 処理結果異常時はnull
*/
private function triggerShjFourC($contract): ?array
{
Log::info('SHJ-4B SHJ-4C車室割り当て処理実行', [
'contract_id' => $contract->contract_id,
'park_id' => $contract->park_id,
'ptype_id' => $contract->ptype_id,
'psection_id' => $contract->psection_id,
]);
try {
// SHJ-4Cサービス実行
$result = $this->shjFourCService->executeRoomAllocation(
(int) $contract->park_id,
(int) $contract->ptype_id,
(int) $contract->psection_id
);
Log::info('SHJ-4B SHJ-4C車室割り当て処理完了', [
'contract_id' => $contract->contract_id,
'result' => $result,
]);
return $result;
} catch (\Throwable $e) {
// ※異常があったとしても、処理は継続する
Log::error('SHJ-4B SHJ-4C車室割り当て処理エラー', [
'contract_id' => $contract->contract_id,
'error' => $e->getMessage(),
]);
return null;
}
}
/**
* SHJ-13実行処理(新規のみ)
*
* ShjThirteenServiceを使用した契約台数追加処理
* 仕様パラメーター4 ゾーンID = 【SHJ-4C.ゾーンID】
*
* @param object $contract
* @param array|null $shjFourCResult SHJ-4C車室割り当て結果
* @return array
*/
private function triggerShjThirteen($contract): array
private function triggerShjThirteen($contract, ?array $shjFourCResult = null): array
{
// 仕様準拠ゾーンID = SHJ-4C.ゾーンID
$zoneId = ($shjFourCResult && !empty($shjFourCResult['zone_id']))
? $shjFourCResult['zone_id']
: $contract->zone_id;
Log::info('SHJ-4B SHJ-13実行処理', [
'contract_id' => $contract->contract_id,
'park_id' => $contract->park_id,
'psection_id' => $contract->psection_id,
'ptype_id' => $contract->ptype_id,
'zone_id' => $contract->zone_id,
'zone_id' => $zoneId,
]);
try {
// 契約データ準備
// 契約データ準備仕様パラメーター4 ゾーンID = SHJ-4C.ゾーンID
$contractData = [
'contract_id' => $contract->contract_id,
'park_id' => $contract->park_id,
'psection_id' => $contract->psection_id,
'ptype_id' => $contract->ptype_id,
'zone_id' => $contract->zone_id,
'zone_id' => $zoneId,
];
// ShjThirteenService実行
@ -888,7 +1146,7 @@ class ShjFourBService
* @param array $amountResult
* @return array 処理結果 ['success' => bool, 'mail_status' => string, 'batch_comment_suffix' => string]
*/
private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, array $amountResult): array
private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, ?array $amountResult = null): array
{
try {
// 【処理5】利用者マスタよりメールアドレス、予備メールアドレスを取得する
@ -918,7 +1176,7 @@ class ShjFourBService
'user_name' => $user->user_name,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'mail_address' => $mailAddress,
'amount_comparison' => $amountResult['comparison'],
'amount_comparison' => $amountResult['comparison'] ?? 'already_received',
]);
// 共通処理「SHJ-7メール送信」を呼び出し
@ -990,19 +1248,33 @@ class ShjFourBService
$contract,
array $amountResult
): array {
// TODO: OperatorQue モデルを使用したキューへの登録処理を実装
// 【SQL-9】キュー種別: shortage=8(支払い催促), excess=6(返金処理)
$queClass = $amountResult['comparison'] === self::AMOUNT_SHORTAGE ? 8 : 6;
Log::info('SHJ-4B オペレーターキュー登録処理実行', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
$queId = DB::table('operator_que')->insertGetId([
'que_class' => $queClass,
'user_id' => $contract->user_id,
'contract_id' => $contract->contract_id,
'park_id' => $contract->park_id,
'que_comment' => '収納請求金額照合エラー',
'que_status' => 1,
'que_status_comment' => '',
'work_instructions' => '',
'operator_id' => 'SHJ-4',
'created_at' => now(),
'updated_at' => now(),
]);
Log::info('SHJ-4B オペレーターキュー登録完了', [
'que_id' => $queId,
'que_class' => $queClass,
'contract_id' => $contract->contract_id,
'amount_comparison' => $amountResult['comparison'],
'difference' => $amountResult['difference'],
]);
return [
'registered' => true,
'method' => 'placeholder',
'message' => 'オペレーターキュー登録処理は実装予定です',
'que_id' => $queId,
'que_class' => $queClass,
];
}
@ -1016,7 +1288,8 @@ class ShjFourBService
* @param array|null $amountResult
* @param bool $isSuccess
* @param string|null $errorMessage
* @param string|null $mailCommentSuffix メール送信結果コメント(例: "メール正常終了件数1" or "メール異常終了件数1、{error_info}"
* @param string|null $mailCommentSuffix メール送信結果コメント
* @param array|null $photoDeletionResult 写真削除結果SQL-6情報含む)
* @return void
*/
private function createBatchLog(
@ -1025,7 +1298,9 @@ class ShjFourBService
?array $amountResult = null,
bool $isSuccess = true,
?string $errorMessage = null,
?string $mailCommentSuffix = null
?string $mailCommentSuffix = null,
?array $photoDeletionResult = null,
?string $statusCommentOverride = null
): void {
try {
$device = Device::orderBy('device_id')->first();
@ -1033,30 +1308,35 @@ class ShjFourBService
$today = now()->format('Y/m/d');
// ステータスコメント生成(内部変数.バッチコメント)
if ($errorMessage) {
if ($statusCommentOverride) {
// 直接指定(授受済み等)
$statusComment = $statusCommentOverride;
} elseif ($errorMessage) {
// エラー時
$statusComment = "支払いステータスチェックエラー決済トランザクションID{$settlement->settlement_transaction_id} - {$errorMessage}";
} elseif ($amountResult) {
// 正常処理時
$contractId = $contract ? $contract->contract_id : 'N/A';
switch ($amountResult['comparison']) {
case self::AMOUNT_MATCH:
$statusComment = "支払いステータスチェックOK決済トランザクションID{$settlement->settlement_transaction_id}、契約ID{$contractId}、金額一致)";
// 仕様: OK + 利用者情報 + 写真ファイル名
$userId = $photoDeletionResult['user_id'] ?? '';
$userName = $photoDeletionResult['user_name'] ?? '';
$file1 = $photoDeletionResult['photo_filename1'] ?? '';
$file2 = $photoDeletionResult['photo_filename2'] ?? '';
$statusComment = "支払いステータスチェックOK、決済トランザクションID{$settlement->settlement_transaction_id}、利用者ID:{$userId}、利用者名:{$userName}、ファイル名:{$file1},{$file2}";
break;
case self::AMOUNT_SHORTAGE:
$statusComment = "支払いステータスチェック請求金額より授受金額が少ないです決済トランザクションID{$settlement->settlement_transaction_id}、契約ID{$contractId}、差額:{$amountResult['difference']}円)";
$statusComment = "支払いステータスチェック:請求金額より授受金額が少ないです決済トランザクションID{$settlement->settlement_transaction_id}";
break;
case self::AMOUNT_EXCESS:
$statusComment = "支払いステータスチェック:請求金額より授受金額が多いです決済トランザクションID{$settlement->settlement_transaction_id}、契約ID{$contractId}、差額:{$amountResult['difference']}";
$statusComment = "支払いステータスチェック:請求金額より授受金額が多いです決済トランザクションID{$settlement->settlement_transaction_id}";
break;
default:
$statusComment = "支払いステータスチェック処理完了決済トランザクションID{$settlement->settlement_transaction_id}、契約ID{$contractId}";
$statusComment = "支払いステータスチェック処理完了決済トランザクションID{$settlement->settlement_transaction_id}";
}
} else {
// その他のケース(対象なし、登録済み等)
$contractId = $contract ? $contract->contract_id : 'N/A';
$statusComment = "支払いステータスチェック処理完了決済トランザクションID{$settlement->settlement_transaction_id}、契約ID{$contractId}";
$statusComment = "支払いステータスチェック処理完了決済トランザクションID{$settlement->settlement_transaction_id}";
}
// メール送信結果をバッチコメントに追加
@ -1064,12 +1344,15 @@ class ShjFourBService
$statusComment .= $mailCommentSuffix;
}
// SHJ-8サービス呼び出し
// 重複判定用マーカー追加checkAlreadyProcessed/ShjFourBCheckCommand互換
$statusComment .= " settlement_transaction_id:{$settlement->settlement_transaction_id}";
// SHJ-8サービス呼び出し仕様JOB6: success/error動的判定
$this->shjEightService->execute(
$deviceId,
'SHJ-4B',
'SHJ-4支払いステータスチェック',
'success',
$isSuccess ? 'success' : 'error',
$statusComment,
$today,
$today

View File

@ -86,19 +86,24 @@ class ShjFourCService
if (empty($zoneInfo)) {
$message = '対象のゾーン情報が見つかりません';
$status = 'error';
$statusComment = sprintf('エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$message, $parkId, $ptypeId, $psectionId);
// 式様準拠ステータスは常にsuccess
$status = 'success';
// 式様準拠:異常情報フォーマット
$statusComment = sprintf(
'車室割り当てNG車室割り当てできません。/%d/%d/%d',
$parkId, $ptypeId, $psectionId
);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
// JOB3: ゾーンID, 車室番号, 異常情報を返却
return [
'success' => false,
'success' => true,
'message' => $message,
'zone_id' => null,
'pplace_no' => null,
'error_info' => $message
'error_info' => $statusComment
];
}
@ -106,12 +111,13 @@ class ShjFourCService
$allocationResult = $this->performAllocationJudgment($zoneInfo, $parkId, $ptypeId, $psectionId);
if (!$allocationResult['can_allocate']) {
// 割当NGの場合、対象事室番号を設定
$this->setTargetRoomNumber($allocationResult['target_room_number']);
$status = 'warning';
$statusComment = sprintf('割当NG: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$allocationResult['reason'], $parkId, $ptypeId, $psectionId);
// 式様準拠ステータスは常にsuccess
$status = 'success';
// 式様準拠:異常情報フォーマット
$statusComment = sprintf(
'車室割り当てNG車室割り当てできません。/%d/%d/%d',
$parkId, $ptypeId, $psectionId
);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
@ -119,15 +125,19 @@ class ShjFourCService
// JOB3: ゾーンID, 車室番号, 異常情報を返却割当NG = 空き車室なし)
return [
'success' => true,
'message' => $allocationResult['reason'],
'zone_id' => null,
'pplace_no' => null,
'error_info' => $allocationResult['reason']
'error_info' => $statusComment
];
}
// 【処理2】バッチログ作成
$statusComment = sprintf('室割当処理完了 (park_id:%d, ptype_id:%d, psection_id:%d, zone_id:%d, pplace_no:%d)',
$parkId, $ptypeId, $psectionId, $allocationResult['zone_id'], $allocationResult['pplace_no']);
// 式様準拠:割り当て情報フォーマット
$statusComment = sprintf(
'車室割り当てOKゾーンID%d車室番号%d',
$allocationResult['zone_id'], $allocationResult['pplace_no']
);
$this->createBatchLog($status, $statusComment);
@ -147,15 +157,19 @@ class ShjFourCService
];
} catch (\Exception $e) {
$errorMessage = 'SHJ-4C 室割当処理でエラーが発生: ' . $e->getMessage();
$status = 'error';
$statusComment = sprintf('例外エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$e->getMessage(), $parkId, $ptypeId, $psectionId);
$errorMessage = 'SHJ-4C 車室割り当て処理でエラーが発生: ' . $e->getMessage();
// 式様準拠ステータスは常にsuccess
$status = 'success';
// 式様準拠:異常情報フォーマット
$statusComment = sprintf(
'車室割り当てNG車室割り当てできません。/%d/%d/%d',
$parkId, $ptypeId, $psectionId
);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
Log::error('SHJ-4C 室割当処理エラー', [
Log::error('SHJ-4C 室割処理エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
@ -163,6 +177,7 @@ class ShjFourCService
// JOB3: ゾーンID, 車室番号, 異常情報を返却
return [
'success' => false,
'message' => $errorMessage,
'zone_id' => null,
'pplace_no' => null,
'error_info' => $errorMessage
@ -185,10 +200,15 @@ class ShjFourCService
$today = now()->format('Y/m/d');
// SHJ-8の制約≤255文字に準拠して切り詰め
if (mb_strlen($statusComment) > 255) {
$statusComment = mb_substr($statusComment, 0, 252) . '...';
}
Log::info('SHJ-8バッチ処理ログ作成', [
'device_id' => $deviceId,
'process_name' => 'SHJ-4C',
'job_name' => 'SHJ-4C室割当',
'job_name' => 'SHJ-4C室割',
'status' => $status,
'status_comment' => $statusComment
]);
@ -197,7 +217,7 @@ class ShjFourCService
$this->shjEightService->execute(
$deviceId,
'SHJ-4C',
'SHJ-4C室割当',
'SHJ-4C室割',
$status,
$statusComment,
$today,
@ -342,7 +362,7 @@ class ShjFourCService
return [
'can_allocate' => false,
'target_room_number' => $targetRoomNumber,
'reason' => "車室割り当てNG: " . $targetRoomNumber
'reason' => sprintf('車室割り当てNG車室割り当てできません。/%s', $targetRoomNumber)
];
} catch (\Exception $e) {
@ -408,7 +428,8 @@ class ShjFourCService
*/
private function generateTargetRoomNumber(int $parkId, int $ptypeId, int $psectionId): string
{
return sprintf('%d_%d_%d', $parkId, $ptypeId, $psectionId);
// 式様準拠:スラッシュ区切り
return sprintf('%d/%d/%d', $parkId, $ptypeId, $psectionId);
}
/**

View File

@ -100,7 +100,7 @@ class ShjMailSendService
}
// 【処理3】メールを送信する
$mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo);
$mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo, $additionalParams);
if (!$mailSendResult['success']) {
$errorInfo = $mailSendResult['error_info'];
@ -295,7 +295,12 @@ class ShjMailSendService
* @param MailTemplate $templateInfo テンプレート情報
* @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string]
*/
private function sendMail(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array
private function sendMail(
string $mailAddress,
string $backupMailAddress,
MailTemplate $templateInfo,
array $additionalParams = []
): array
{
try {
// 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス)
@ -305,6 +310,11 @@ class ShjMailSendService
$subject = $templateInfo->getSubject() ?? '';
$message = $templateInfo->getText() ?? '';
// SHJ-5等で渡された追加パラメータをテンプレートへ反映
if (!empty($additionalParams)) {
[$subject, $message] = $this->replaceTemplateParams($subject, $message, $additionalParams);
}
// 追加ヘッダ設定BCC、From等
$headers = $this->buildMailHeaders($templateInfo);
@ -363,6 +373,58 @@ class ShjMailSendService
}
}
/**
* テンプレート文字列のプレースホルダ置換
*
* :
* - %reserve_id%
* - %expiry%
* - {reserve_id}
* - {expiry}
*
* @param string $subject 件名
* @param string $message 本文
* @param array $params 置換パラメータ
* @return array [subject, message]
*/
private function replaceTemplateParams(string $subject, string $message, array $params): array
{
$replaced = false;
foreach ($params as $key => $value) {
$token1 = '%' . $key . '%';
$token2 = '{' . $key . '}';
$token3 = '{{' . $key . '}}';
$replace = (string) $value;
$newSubject = str_replace([$token1, $token2, $token3], $replace, $subject);
$newMessage = str_replace([$token1, $token2, $token3], $replace, $message);
if ($newSubject !== $subject || $newMessage !== $message) {
$replaced = true;
}
$subject = $newSubject;
$message = $newMessage;
}
// プレースホルダがテンプレート内に無い場合は末尾へクエリ形式で補完
if (!$replaced && !empty($params)) {
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
$queryString = implode('&', $pairs);
if ($queryString !== '') {
$message = rtrim($message) . "\n" . $queryString;
}
}
return [$subject, $message];
}
/**
* メールヘッダを構築
*

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* ゾーンテーブルに車室番号範囲カラムを追加
* SHJ-4C式様準拠
*/
public function up(): void
{
Schema::table('zone', function (Blueprint $table) {
// 車室番号開始デフォルト1
$table->unsignedInteger('zone_pplace_start')
->default(1)
->after('zone_sort')
->comment('車室番号開始');
// 車室番号終了NULLの場合は開始番号+許容台数-1
$table->unsignedInteger('zone_pplace_end')
->nullable()
->after('zone_pplace_start')
->comment('車室番号終了');
});
}
/**
* カラム削除
*/
public function down(): void
{
Schema::table('zone', function (Blueprint $table) {
$table->dropColumn(['zone_pplace_start', 'zone_pplace_end']);
});
}
};

View File

@ -70,11 +70,13 @@ Route::middleware(['api.key'])->group(function () {
| API 3 - 決済結果通知Callback
|--------------------------------------------------------------------------
|
| POST /api/newwipe/callback - Wellnet入金通知受信
| POST /api/newwipe/callback - Wellnet入金通知受信
| POST /api/newwipe/callback/shj4a - SHJ-4A ウェルネット収納情報受付
|
| 認証: IP白名単のみAPI Key不要
|
*/
Route::middleware(['wellnet.ip'])->group(function () {
Route::match(['get', 'post'], 'newwipe/callback', [PaymentCallbackController::class, 'receive']);
Route::match(['get', 'post'], 'newwipe/callback/shj4a', [PaymentCallbackController::class, 'receiveShj4a']);
});

View File

@ -10,3 +10,6 @@ Artisan::command('inspire', function () {
// 支払期限切れチェック15分毎
Schedule::command('payment:expire')->everyFifteenMinutes();
// SHJ-5 駐輪場空きチェック毎月20日 11:00
Schedule::command('shj:5')->monthlyOn(20, '11:00');