Merge branch 'main' of https://git.so-manager-dev.com/so-manager/krgm.so-manager-dev.com
All checks were successful
Deploy main / deploy (push) Successful in 23s

This commit is contained in:
kin.rinzen 2025-09-12 20:43:53 +09:00
commit 2ed9280f6a
11 changed files with 1827 additions and 13 deletions

View File

@ -0,0 +1,317 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\SettlementTransaction;
use App\Models\Batch\BatchLog;
use App\Jobs\ProcessSettlementJob;
use App\Services\ShjFourBService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Carbon\Carbon;
/**
* SHJ-4B チェックコマンド
*
* 未処理の決済トランザクションを検索し、ProcessSettlementJobをディスパッチする兜底処理
*
* 実行方法:
* php artisan shj4b:check
* php artisan shj4b:check --dry-run # 実際の処理は行わず、対象のみ表示
* php artisan shj4b:check --limit=50 # 処理件数制限
*
* Cron設定例10分毎実行:
* 0,10,20,30,40,50 * * * * cd /path/to/project && php artisan shj4b:check > /dev/null 2>&1
*/
class ShjFourBCheckCommand extends Command
{
/**
* コマンド名と説明
*
* @var string
*/
protected $signature = 'shj4b:check
{--dry-run : 実際の処理を行わず対象のみ表示}
{--limit=100 : 処理する最大件数}
{--hours=24 : 指定時間以内の決済のみ対象}';
/**
* コマンドの説明
*
* @var string
*/
protected $description = 'SHJ-4B 兜底チェック - 未処理の決済トランザクションを検索してProcessSettlementJobをディスパッチ';
/**
* SHJ-4B サービス
*
* @var ShjFourBService
*/
protected $shjFourBService;
/**
* コンストラクタ
*/
public function __construct(ShjFourBService $shjFourBService)
{
parent::__construct();
$this->shjFourBService = $shjFourBService;
}
/**
* コマンド実行
*
* @return int
*/
public function handle()
{
$startTime = now();
$isDryRun = $this->option('dry-run');
$limit = (int) $this->option('limit');
$hours = (int) $this->option('hours');
$this->info("SHJ-4B チェックコマンド開始");
$this->info("実行モード: " . ($isDryRun ? "ドライラン(実際の処理なし)" : "本実行"));
$this->info("処理制限: {$limit}");
$this->info("対象期間: {$hours}時間以内");
// バッチログ作成
$batch = BatchLog::createBatchLog(
'shj4b_check',
BatchLog::STATUS_START,
[
'command' => 'shj4b:check',
'options' => [
'dry_run' => $isDryRun,
'limit' => $limit,
'hours' => $hours,
],
'start_time' => $startTime,
],
'SHJ-4B チェックコマンド開始'
);
try {
// 未処理の決済トランザクション取得
$unprocessedSettlements = $this->getUnprocessedSettlements($hours, $limit);
$this->info("未処理決済トランザクション: " . $unprocessedSettlements->count() . "");
if ($unprocessedSettlements->isEmpty()) {
$this->info("処理対象なし");
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B チェック完了 - 処理対象なし',
'success_count' => 0,
]);
return 0;
}
// 対象一覧表示
$this->displayTargets($unprocessedSettlements);
if ($isDryRun) {
$this->info("ドライランモードのため、実際の処理はスキップします");
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B チェック完了 - ドライラン',
'success_count' => 0,
'parameters' => json_encode(['targets' => $unprocessedSettlements->pluck('settlement_transaction_id')->toArray()]),
]);
return 0;
}
// 実際の処理実行
$processed = $this->processSettlements($unprocessedSettlements);
$this->info("処理完了: {$processed['success']}件成功, {$processed['failed']}件失敗");
$batch->update([
'status' => $processed['failed'] > 0 ? BatchLog::STATUS_ERROR : BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => "SHJ-4B チェック完了 - 成功:{$processed['success']}件, 失敗:{$processed['failed']}",
'success_count' => $processed['success'],
'error_count' => $processed['failed'],
'parameters' => json_encode($processed),
]);
return $processed['failed'] > 0 ? 1 : 0;
} catch (\Throwable $e) {
$this->error("SHJ-4B チェック処理でエラーが発生しました: " . $e->getMessage());
Log::error('SHJ-4B チェックコマンドエラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$batch->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-4B チェック失敗: ' . $e->getMessage(),
'error_details' => $e->getTraceAsString(),
'error_count' => 1,
]);
return 1;
}
}
/**
* 未処理の決済トランザクション取得
*
* @param int $hours
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
private function getUnprocessedSettlements(int $hours, int $limit)
{
$cutoffTime = Carbon::now()->subHours($hours);
// 条件:
// 1. 指定時間以内に作成された
// 2. contract_payment_numberがnullでない
// 3. まだregular_contractのsettlement_transaction_idに関連付けられていない
// 4. ProcessSettlementJobが実行されていないbatch_logで確認
$query = SettlementTransaction::where('created_at', '>=', $cutoffTime)
->whereNotNull('contract_payment_number')
->whereNotNull('pay_date')
->whereNotNull('settlement_amount')
->orderBy('created_at', 'asc');
$settlements = $query->limit($limit)->get();
// 既に処理済みのものを除外
$unprocessed = $settlements->filter(function ($settlement) {
return !$this->isAlreadyProcessed($settlement);
});
return $unprocessed;
}
/**
* 既に処理済みかチェック
*
* @param SettlementTransaction $settlement
* @return bool
*/
private function isAlreadyProcessed(SettlementTransaction $settlement): bool
{
// 1. regular_contractの同一contract_payment_numberが既に処理済みかチェック
$linkedContract = DB::table('regular_contract')
->where('contract_payment_number', $settlement->contract_payment_number)
->whereNotNull('contract_payment_day')
->exists();
if ($linkedContract) {
return true;
}
// 2. batch_logで処理完了記録があるかチェック
$processedInBatch = BatchLog::where('process_name', 'shj4b')
->where('status', BatchLog::STATUS_SUCCESS)
->where('parameters', 'like', '%"settlement_transaction_id":' . $settlement->settlement_transaction_id . '%')
->exists();
if ($processedInBatch) {
return true;
}
// 3. 現在キューに入っているかチェック(簡易版)
// 注: より正確にはRedis/DBキューの内容を確認する必要がある
$recentJobDispatched = BatchLog::where('process_name', 'shj4b')
->where('parameters', 'like', '%"settlement_transaction_id":' . $settlement->settlement_transaction_id . '%')
->where('created_at', '>=', Carbon::now()->subHours(1))
->exists();
return $recentJobDispatched;
}
/**
* 対象一覧表示
*
* @param \Illuminate\Database\Eloquent\Collection $settlements
*/
private function displayTargets($settlements)
{
$this->info("対象の決済トランザクション:");
$this->table(
['ID', '契約支払番号', '決済金額', '支払日', '作成日時'],
$settlements->map(function ($settlement) {
return [
$settlement->settlement_transaction_id,
$settlement->contract_payment_number,
number_format($settlement->settlement_amount) . '円',
Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'),
$settlement->created_at->format('Y-m-d H:i:s'),
];
})->toArray()
);
}
/**
* 決済処理実行
*
* @param \Illuminate\Database\Eloquent\Collection $settlements
* @return array
*/
private function processSettlements($settlements): array
{
$success = 0;
$failed = 0;
$results = [];
foreach ($settlements as $settlement) {
try {
$this->info("処理中: 決済トランザクションID {$settlement->settlement_transaction_id}");
// ProcessSettlementJobをディスパッチ
ProcessSettlementJob::dispatch(
$settlement->settlement_transaction_id,
[
'source' => 'shj4b_check_command',
'triggered_at' => now()->toISOString(),
]
);
$success++;
$results[] = [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'status' => 'dispatched',
'message' => 'ProcessSettlementJobディスパッチ成功',
];
$this->info("✓ 成功: {$settlement->settlement_transaction_id}");
} catch (\Throwable $e) {
$failed++;
$results[] = [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'status' => 'failed',
'error' => $e->getMessage(),
];
$this->error("✗ 失敗: {$settlement->settlement_transaction_id} - {$e->getMessage()}");
Log::error('SHJ-4B チェック 個別処理失敗', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'error' => $e->getMessage(),
]);
}
}
return [
'success' => $success,
'failed' => $failed,
'results' => $results,
'total' => $settlements->count(),
];
}
}

View File

@ -106,3 +106,4 @@ class ShjThreeCommand extends Command
}
}
}

View File

@ -77,9 +77,13 @@ class LoginController extends Controller
*/
protected function validateLogin(Request $request)
{
// 個別未入力メッセージ仕様1,2
$request->validate([
'ope_id' => 'required|string', // オペレータID旧システムと同じ
'ope_pass' => 'required|string', // オペレータパスワード(旧システムと同じ)
'ope_id' => 'required|string',
'ope_pass' => 'required|string',
], [
'ope_id.required' => 'ログインIDが未入力です。',
'ope_pass.required' => 'パスワードが未入力です。',
]);
}
@ -91,6 +95,13 @@ class LoginController extends Controller
*/
protected function attemptLogin(Request $request)
{
// 先にIDのみでオペレータ取得して退職フラグを確認仕様5-1
$opeId = $request->input('ope_id');
$operator = \App\Models\Ope::where('ope_id', $opeId)->first();
if ($operator && (int)($operator->ope_quit_flag) === 1) {
// 退職扱いは認証失敗と同じメッセージ仕様5-1 と 3/4 統一表示)
return false;
}
return Auth::attempt($this->credentials($request), false);
}
@ -118,9 +129,9 @@ class LoginController extends Controller
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
// 仕様5: ログインIDをセッション保持
$request->session()->put('login_ope_id', $request->input('ope_id'));
return redirect()->intended($this->redirectTo);
}

View File

@ -0,0 +1,427 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use App\Models\Batch\BatchLog;
use App\Models\SettlementTransaction;
use App\Models\RegularContract;
use App\Jobs\ProcessSettlementJob;
use Carbon\Carbon;
class WellnetController extends Controller
{
/**
* Wellnet PUSH 受信 (SHJ-4A)
* 受け取った SOAP/XML を解析し、settlement_transaction に登録。
* 幂等性チェック、データ検証、キュー投入を含む完全な処理を実行。
*/
public function receive(Request $request)
{
$startedAt = now();
$raw = $request->getContent();
$md5Hash = md5($raw);
// IP白名单检查如果配置了
if (!$this->validateClientIp($request->ip())) {
Log::warning('SHJ-4A IP白名单验证失败', [
'ip' => $request->ip(),
'content_length' => strlen($raw),
]);
return $this->errorResponse('Unauthorized IP', 403);
}
// 事前にログ記録(サイズ上限に注意)
Log::info('SHJ-4A Wellnet PUSH received', [
'length' => strlen($raw),
'content_type' => $request->header('Content-Type'),
'ip' => $request->ip(),
'md5_hash' => $md5Hash,
]);
// 共通バッチログ: start
$batch = BatchLog::createBatchLog(
'shj4a',
BatchLog::STATUS_START,
[
'ip' => $request->ip(),
'content_type' => $request->header('Content-Type'),
'content_length' => strlen($raw),
'md5_hash' => $md5Hash,
],
'SHJ-4A Wellnet PUSH start'
);
try {
// 【処理1】幂等性检查 - MD5重复检查
$existingByMd5 = SettlementTransaction::where('md5_string', $md5Hash)->first();
if ($existingByMd5) {
Log::info('SHJ-4A 幂等性: MD5重复检测', [
'md5_hash' => $md5Hash,
'existing_id' => $existingByMd5->settlement_transaction_id,
]);
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A 幂等性: MD5重复直接返回成功',
'success_count' => 0, // 幂等返回不计入成功数
]);
return $this->successResponse('処理済み(幂等性)');
}
// 【処理2】SOAP/XML解析
$xml = @simplexml_load_string($raw);
if (!$xml) {
throw new \RuntimeException('Invalid XML/SOAP payload');
}
// Body 以下の最初の要素を取得
$nsBody = $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body ?? null;
$payloadNode = $nsBody ? current($nsBody->children()) : $xml; // SOAPでなければ素のXML想定
// XML -> 配列化
$payloadArray = json_decode(json_encode($payloadNode), true) ?? [];
// 【処理3】データ抽出と正規化
$data = $this->extractSettlementData($payloadArray, $md5Hash);
// 【処理4】必須フィールド検証
$this->validateRequiredFields($data);
// 【処理5】複合キー重复检查contract_payment_number + pay_date + settlement_amount
$existingByComposite = $this->findExistingByCompositeKey($data);
if ($existingByComposite) {
Log::info('SHJ-4A 幂等性: 複合キー重复検出', [
'contract_payment_number' => $data['contract_payment_number'],
'pay_date' => $data['pay_date'],
'settlement_amount' => $data['settlement_amount'],
'existing_id' => $existingByComposite->settlement_transaction_id,
]);
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A 幂等性: 複合キー重复,直接返回成功',
'success_count' => 0,
]);
return $this->successResponse('処理済み(幂等性)');
}
// 【処理6】データベース取込と関連処理
$settlementId = null;
DB::transaction(function() use ($data, $batch, &$settlementId) {
// 決済トランザクション登録
$settlement = SettlementTransaction::create($data);
$settlementId = $settlement->settlement_transaction_id;
// 契約テーブルの軽微な更新SHJ-4Bで正式更新
RegularContract::where('contract_payment_number', $data['contract_payment_number'])
->update(['contract_updated_at' => now()]);
// バッチログ成功更新
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A Wellnet PUSH stored successfully',
'success_count' => 1,
'parameters' => json_encode([
'settlement_transaction_id' => $settlementId,
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
]),
]);
Log::info('SHJ-4A 決済トランザクション登録成功', [
'settlement_transaction_id' => $settlementId,
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
]);
});
// 【処理7】SHJ-4B用キュージョブ投入
try {
$jobContext = [
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
'pay_date' => $data['pay_date'],
'pay_code' => $data['pay_code'],
'triggered_by' => 'shj4a_webhook',
'triggered_at' => $startedAt->toISOString(),
];
ProcessSettlementJob::dispatch($settlementId, $jobContext);
Log::info('SHJ-4A ProcessSettlementJob投入成功', [
'settlement_transaction_id' => $settlementId,
'job_context' => $jobContext,
]);
} catch (\Throwable $jobError) {
// キュー投入失敗は警告レベル(メイン処理は成功済み)
Log::warning('SHJ-4A ProcessSettlementJob投入失敗', [
'settlement_transaction_id' => $settlementId,
'error' => $jobError->getMessage(),
'note' => '兜底巡検で処理される予定',
]);
}
return $this->successResponse();
} catch (\Throwable $e) {
Log::error('SHJ-4A error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'md5_hash' => $md5Hash,
]);
if (isset($batch)) {
$batch->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-4A failed: ' . $e->getMessage(),
'error_details' => $e->getTraceAsString(),
'error_count' => 1,
]);
}
return $this->errorResponse($e->getMessage());
}
}
/**
* IP白名单验证
*
* @param string $clientIp
* @return bool
*/
private function validateClientIp(string $clientIp): bool
{
$whitelist = config('services.wellnet.ip_whitelist', '');
if (empty($whitelist)) {
return true; // 白名单为空时不验证
}
$allowedIps = array_map('trim', explode(',', $whitelist));
foreach ($allowedIps as $allowedIp) {
if (strpos($allowedIp, '/') !== false) {
// CIDR記法対応
if ($this->ipInRange($clientIp, $allowedIp)) {
return true;
}
} else {
// 直接IP比較
if ($clientIp === $allowedIp) {
return true;
}
}
}
return false;
}
/**
* CIDR範囲でのIP检查
*
* @param string $ip
* @param string $range
* @return bool
*/
private function ipInRange(string $ip, string $range): bool
{
list($subnet, $bits) = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
return ($ip & $mask) == $subnet;
}
/**
* 決済データの抽出と正規化
*
* @param array $payloadArray
* @param string $md5Hash
* @return array
*/
private function extractSettlementData(array $payloadArray, string $md5Hash): array
{
// inData/Result系の取り出しキー名差異に寛容
$first = function(array $arr, array $keys, $default = null) {
foreach ($keys as $k) {
if (isset($arr[$k])) return is_array($arr[$k]) ? $arr[$k] : (string)$arr[$k];
}
return $default;
};
$flat = $payloadArray;
// よくある入れ子: { YoyakuNyukin: { inData: {...} } } / { YoyakuNyukinResponse: { YoyakuNyukinResult: {...} } }
foreach (['inData','YoyakuSyunoBarCodeResult','YoyakuNyukinResult','YoyakuSyunoETicketResult'] as $k) {
if (isset($flat[$k]) && is_array($flat[$k])) { $flat = $flat[$k]; }
}
$data = [
'pay_code' => $first($flat, ['NyukinPayCode','SyunoPayCode','BcPayCode']),
'contract_payment_number' => $first($flat, ['NyukinRecvNum','SyunoRecvNum','RecvNum','contract_payment_number']),
'corp_code' => $first($flat, ['NyukinCorpCode','SyunoCorpCode','BcCorpCode','CorpCode']),
'mms_date' => $first($flat, ['NyukinReferDate','SyunoMMSNo','MmsDate']),
'cvs_code' => $first($flat, ['NyukinCvsCode','CvsCode']),
'shop_code' => $first($flat, ['NyukinShopCode','ShopCode']),
'pay_date' => $first($flat, ['NyukinPaidDate','PaidDate']),
'settlement_amount' => $first($flat, ['NyukinPaidAmount','SyunoPayAmount','PaidAmount']),
'stamp_flag' => $first($flat, ['NyukinInshiFlag','InshiFlag']),
'status' => 'received',
'md5_string' => $md5Hash,
];
// データ正規化処理
$data = $this->normalizeSettlementData($data);
return $data;
}
/**
* 決済データの正規化
*
* @param array $data
* @return array
*/
private function normalizeSettlementData(array $data): array
{
// 金額を数値化(非負数)
if (!empty($data['settlement_amount'])) {
$amount = preg_replace('/[^\d.]/', '', $data['settlement_amount']);
$data['settlement_amount'] = max(0, (float)$amount);
} else {
$data['settlement_amount'] = null;
}
// 支払日時の正規化
if (!empty($data['pay_date'])) {
try {
$data['pay_date'] = Carbon::parse($data['pay_date'])->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
Log::warning('SHJ-4A 支払日時解析失敗', [
'original_pay_date' => $data['pay_date'],
'error' => $e->getMessage(),
]);
$data['pay_date'] = null;
}
}
// 文字列フィールドのトリム
$stringFields = ['pay_code', 'contract_payment_number', 'corp_code', 'mms_date', 'cvs_code', 'shop_code', 'stamp_flag'];
foreach ($stringFields as $field) {
if (isset($data[$field])) {
$data[$field] = trim($data[$field]) ?: null;
}
}
return $data;
}
/**
* 必須フィールドの検証
*
* @param array $data
* @throws \RuntimeException
*/
private function validateRequiredFields(array $data): void
{
// 必須フィールドのチェック
if (empty($data['contract_payment_number'])) {
throw new \RuntimeException('必須フィールドが不足: contract_payment_number (RecvNum)');
}
if (!isset($data['settlement_amount']) || $data['settlement_amount'] === null) {
throw new \RuntimeException('必須フィールドが不足: settlement_amount');
}
if (empty($data['pay_date'])) {
throw new \RuntimeException('必須フィールドが不足: pay_date');
}
}
/**
* 複合キーによる既存レコード検索
*
* @param array $data
* @return SettlementTransaction|null
*/
private function findExistingByCompositeKey(array $data): ?SettlementTransaction
{
return SettlementTransaction::where('contract_payment_number', $data['contract_payment_number'])
->where('pay_date', $data['pay_date'])
->where('settlement_amount', $data['settlement_amount'])
->first();
}
/**
* 成功レスポンスの生成
*
* @param string $message
* @return \Illuminate\Http\Response
*/
private function successResponse(string $message = '正常処理'): \Illuminate\Http\Response
{
$responseFormat = config('services.wellnet.response_format', 'json');
if ($responseFormat === 'soap') {
return $this->soapResponse(0, $message);
} else {
return response()->json(['result' => 0, 'message' => $message]);
}
}
/**
* エラーレスポンスの生成
*
* @param string $message
* @param int $httpCode
* @return \Illuminate\Http\Response
*/
private function errorResponse(string $message, int $httpCode = 500): \Illuminate\Http\Response
{
$responseFormat = config('services.wellnet.response_format', 'json');
if ($responseFormat === 'soap') {
return $this->soapResponse(1, $message, $httpCode);
} else {
$resultCode = ($httpCode >= 500) ? 1 : 2; // サーバーエラー:1, クライアントエラー:2
return response()->json(['result' => $resultCode, 'error' => $message], $httpCode);
}
}
/**
* SOAP形式のレスポンス生成
*
* @param int $resultCode
* @param string $message
* @param int $httpCode
* @return \Illuminate\Http\Response
*/
private function soapResponse(int $resultCode, string $message, int $httpCode = 200): \Illuminate\Http\Response
{
$soapEnvelope = '<?xml version="1.0" encoding="utf-8"?>'
. '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">'
. '<soap:Body>'
. '<WellnetPushResponse>'
. '<Result>' . htmlspecialchars($resultCode) . '</Result>'
. '<Message>' . htmlspecialchars($message) . '</Message>'
. '</WellnetPushResponse>'
. '</soap:Body>'
. '</soap:Envelope>';
return response($soapEnvelope, $httpCode)
->header('Content-Type', 'text/xml; charset=utf-8');
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Models\Batch\BatchLog;
use App\Models\SettlementTransaction;
use App\Services\ShjFourBService;
/**
* SHJ-4B 決済トランザクション処理ジョブ
*
* SHJ-4Aで登録された決済情報を基に定期契約の更新処理を行う
*/
class ProcessSettlementJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* ジョブの実行可能回数
*
* @var int
*/
public $tries = 3;
/**
* ジョブの実行間隔(秒)
*
* @var array
*/
public $backoff = [60, 300, 900];
/**
* 使用するキュー名
*
* @var string
*/
public $queue = 'settlement';
/**
* 決済トランザクションID
*
* @var int
*/
protected $settlementTransactionId;
/**
* 追加のコンテキスト情報
*
* @var array
*/
protected $context;
/**
* コンストラクタ
*
* @param int $settlementTransactionId 決済トランザクションID
* @param array $context 追加のコンテキスト情報
*/
public function __construct(int $settlementTransactionId, array $context = [])
{
$this->settlementTransactionId = $settlementTransactionId;
$this->context = $context;
}
/**
* ジョブを実行
*
* SHJ-4Bサービスを使用して決済トランザクション処理を実行
*
* @return void
*/
public function handle()
{
$startTime = now();
// バッチログの開始記録
$batch = BatchLog::createBatchLog(
'shj4b',
BatchLog::STATUS_START,
[
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'job_id' => $this->job->getJobId(),
],
'SHJ-4B ProcessSettlementJob start'
);
try {
Log::info('SHJ-4B ProcessSettlementJob開始', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'start_time' => $startTime,
]);
// SHJ-4Bサービスを使用して決済トランザクション処理を実行
$shjFourBService = app(ShjFourBService::class);
$result = $shjFourBService->processSettlementTransaction(
$this->settlementTransactionId,
$this->context
);
// 処理結果に基づいてバッチログを更新
if ($result['success']) {
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob completed successfully',
'success_count' => 1,
'parameters' => json_encode([
'result' => $result,
]),
]);
Log::info('SHJ-4B ProcessSettlementJob完了', [
'settlement_transaction_id' => $this->settlementTransactionId,
'execution_time' => now()->diffInSeconds($startTime),
'result' => $result,
]);
} else {
// ビジネスロジック上の問題(エラーではない)
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob completed with issues: ' . $result['reason'],
'success_count' => 0,
'parameters' => json_encode([
'result' => $result,
'requires_manual_action' => true,
]),
]);
Log::warning('SHJ-4B ProcessSettlementJob要手動対応', [
'settlement_transaction_id' => $this->settlementTransactionId,
'result' => $result,
]);
}
} catch (\Throwable $e) {
Log::error('SHJ-4B ProcessSettlementJob失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// バッチログのエラー記録
$batch->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob failed: ' . $e->getMessage(),
'error_details' => $e->getTraceAsString(),
'error_count' => 1,
]);
// ジョブを失敗させて再試行を促す
throw $e;
}
}
/**
* ジョブが失敗した場合の処理
*
* @param \Throwable $exception
* @return void
*/
public function failed(\Throwable $exception)
{
Log::error('SHJ-4B ProcessSettlementJob最終失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'error' => $exception->getMessage(),
'attempts' => $this->attempts(),
]);
// 最終失敗時の追加処理があればここに記述
// 例:管理者への通知、障害キューへの登録など
}
}

View File

@ -0,0 +1,849 @@
<?php
namespace App\Services;
use App\Models\SettlementTransaction;
use App\Models\RegularContract;
use App\Models\Park;
use App\Models\PriceA;
use App\Models\Batch\BatchLog;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
/**
* SHJ-4B 定期契約更新処理サービス
*
* SHJ-4Aで登録された決済情報を基に定期契約の更新処理を実行する
* ウェルネットのPUSH通知を契機とした決済トランザクション処理
*/
class ShjFourBService
{
/**
* 金額比較結果の定数
*/
const AMOUNT_MATCH = 'match'; // 金額一致
const AMOUNT_SHORTAGE = 'shortage'; // 授受過少
const AMOUNT_EXCESS = 'excess'; // 授受超過
/**
* 契約フラグの状態定数
*/
const CONTRACT_FLAG_PROCESSING = 0; // 処理中
const CONTRACT_FLAG_UPDATED = 1; // 更新済
const CONTRACT_FLAG_ERROR = 2; // エラー状態
/**
* 決済トランザクション処理メイン実行
*
* SHJ-4Bの3段階判断処理を実装
* 【判断0】対象契約取得判定
* 【判断1】授受状態チェック
* 【判断2】金額チェック
*
* @param int $settlementTransactionId 決済トランザクションID
* @param array $context 追加のコンテキスト情報
* @return array 処理結果
*/
public function processSettlementTransaction(int $settlementTransactionId, array $context = []): array
{
$startTime = now();
Log::info('SHJ-4B 決済トランザクション処理開始', [
'settlement_transaction_id' => $settlementTransactionId,
'context' => $context,
'start_time' => $startTime,
]);
try {
// 【前処理】決済トランザクション取得
$settlement = $this->getSettlementTransaction($settlementTransactionId);
// 【処理1】定期契約マスタの対象レコード取得
// 【判断0】取得判定登録済み判定を含む
$contractResult = $this->judgeTargetContract($settlement);
if (!$contractResult['found']) {
// 対象レコードなしの場合
return $this->handleNoTargetRecord($settlement, $contractResult);
}
if ($contractResult['already_processed']) {
// 登録済みの場合
return $this->handleAlreadyProcessed($settlement, $contractResult);
}
$contract = $contractResult['contract'];
// 【判断1】授受状態チェック
$statusResult = $this->judgeReceiptStatus($settlement, $contract);
if (!$statusResult['valid']) {
// 授受状態が異常な場合
return $this->handleInvalidStatus($settlement, $contract, $statusResult);
}
// 【判断2】金額チェック
$amountResult = $this->judgeAmountComparison($settlement, $contract);
// 【処理3】契約更新処理実行
$updateResult = $this->executeContractUpdate($settlement, $contract, $amountResult);
// 副作用処理実行
$sideEffectResult = $this->executeSideEffects($settlement, $contract, $amountResult, $updateResult);
$result = [
'success' => true,
'settlement_transaction_id' => $settlementTransactionId,
'contract_id' => $contract->contract_id,
'contract_payment_number' => $settlement->contract_payment_number,
'amount_comparison' => $amountResult['comparison'],
'contract_updated' => $updateResult['updated'],
'side_effects' => $sideEffectResult,
'execution_time' => now()->diffInSeconds($startTime),
];
Log::info('SHJ-4B 決済トランザクション処理完了', $result);
return $result;
} catch (\Throwable $e) {
Log::error('SHJ-4B 決済トランザクション処理失敗', [
'settlement_transaction_id' => $settlementTransactionId,
'context' => $context,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
/**
* 決済トランザクション取得
*
* @param int $settlementTransactionId
* @return SettlementTransaction
* @throws \RuntimeException
*/
private function getSettlementTransaction(int $settlementTransactionId): SettlementTransaction
{
$settlement = SettlementTransaction::find($settlementTransactionId);
if (!$settlement) {
throw new \RuntimeException("SettlementTransaction not found: {$settlementTransactionId}");
}
Log::info('SHJ-4B 決済トランザクション取得成功', [
'settlement_transaction_id' => $settlementTransactionId,
'contract_payment_number' => $settlement->contract_payment_number,
'settlement_amount' => $settlement->settlement_amount,
'pay_date' => $settlement->pay_date,
]);
return $settlement;
}
/**
* 【処理1】定期契約マスタの対象レコード取得
* 【判断0】取得判定登録済み判定を含む
*
* @param SettlementTransaction $settlement
* @return array
*/
private function judgeTargetContract(SettlementTransaction $settlement): array
{
Log::info('SHJ-4B 処理1: 定期契約マスタの対象レコード取得開始', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_payment_number' => $settlement->contract_payment_number,
]);
// 文档要求のSQL構造に基づく対象レコード取得
// regular_contract T1 inner join park T2 inner join price_a T4
$contractQuery = DB::table('regular_contract as T1')
->select([
'T1.contract_id',
'T1.old_contract_id',
'T1.park_id',
'T1.user_id',
'T1.contract_flag',
'T1.billing_amount',
'T4.price_ptypeid as ptype_id',
'T1.psection_id',
'T1.update_flag',
'T1.reserve_id',
'T1.contract_payment_number',
'T1.contract_payment_day',
'T1.contract_periods',
'T1.contract_periode',
'T1.contract_created_at',
'T1.contract_cancel_flag',
'T2.park_name',
'T4.price_month',
'T4.price'
])
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->join('price_a as T4', function($join) {
$join->on('T1.price_parkplaceid', '=', 'T4.price_parkplaceid')
->on('T1.park_id', '=', 'T4.park_id'); // 文档要求の第二条件追加
})
->where('T1.contract_payment_number', $settlement->contract_payment_number)
->where('T1.contract_cancel_flag', '!=', 1) // 解約されていない
->whereNotNull('T1.contract_flag') // 状態が設定済み
->first();
if (!$contractQuery) {
Log::warning('SHJ-4B 判断0: 対象レコードなし', [
'contract_payment_number' => $settlement->contract_payment_number,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
]);
return [
'found' => false,
'contract' => null,
'reason' => '対象レコードなし',
'message' => "契約番号に一致する有効な定期契約が見つかりません: {$settlement->contract_payment_number}",
];
}
// 登録済み判定
$isAlreadyProcessed = $this->checkAlreadyProcessed($contractQuery, $settlement);
if ($isAlreadyProcessed['processed']) {
Log::info('SHJ-4B 判断0: 登録済み検出', [
'contract_id' => $contractQuery->contract_id,
'contract_payment_number' => $settlement->contract_payment_number,
'reason' => $isAlreadyProcessed['reason'],
]);
return [
'found' => true,
'contract' => $contractQuery,
'already_processed' => true,
'reason' => '登録済み',
'message' => "この決済は既に処理済みです: " . $isAlreadyProcessed['reason'],
];
}
Log::info('SHJ-4B 判断0: 対象契約取得成功', [
'contract_id' => $contractQuery->contract_id,
'contract_payment_number' => $settlement->contract_payment_number,
'billing_amount' => $contractQuery->billing_amount,
'contract_flag' => $contractQuery->contract_flag,
'park_name' => $contractQuery->park_name,
'price_month' => $contractQuery->price_month,
]);
return [
'found' => true,
'contract' => $contractQuery,
'already_processed' => false,
'reason' => '対象契約取得成功',
'message' => "契約ID {$contractQuery->contract_id} を取得しました",
];
}
/**
* 登録済み判定
*
* 複数の条件で既に処理済みかを判定
*
* @param object $contract
* @param SettlementTransaction $settlement
* @return array
*/
private function checkAlreadyProcessed($contract, SettlementTransaction $settlement): array
{
// 条件1: contract_payment_dayが既に設定済みで、今回の支払日以降
if (!empty($contract->contract_payment_day)) {
$existingPaymentDate = Carbon::parse($contract->contract_payment_day);
$currentPaymentDate = Carbon::parse($settlement->pay_date);
if ($existingPaymentDate->gte($currentPaymentDate)) {
return [
'processed' => true,
'reason' => "既に支払日 {$existingPaymentDate->format('Y-m-d')} が設定済み",
];
}
}
// 条件2: 同一の決済条件(contract_payment_number + pay_date + settlement_amount)が
// 既に他のsettlement_transactionで処理済み
$existingTransaction = SettlementTransaction::where('contract_payment_number', $settlement->contract_payment_number)
->where('pay_date', $settlement->pay_date)
->where('settlement_amount', $settlement->settlement_amount)
->where('settlement_transaction_id', '!=', $settlement->settlement_transaction_id)
->first();
if ($existingTransaction) {
return [
'processed' => true,
'reason' => "同一条件の決済トランザクション {$existingTransaction->settlement_transaction_id} が既に存在",
];
}
// 条件3: batch_logで同一決済の処理完了記録があるか
$existingBatchLog = BatchLog::where('process_name', 'shj4b')
->where('status', BatchLog::STATUS_SUCCESS)
->where('parameters', 'like', '%"settlement_transaction_id":' . $settlement->settlement_transaction_id . '%')
->exists();
if ($existingBatchLog) {
return [
'processed' => true,
'reason' => "batch_logに処理完了記録が存在",
];
}
return [
'processed' => false,
'reason' => '未処理',
];
}
/**
* 【判断1】授受状態チェック
*
* @param SettlementTransaction $settlement
* @param object $contract
* @return array
*/
private function judgeReceiptStatus(SettlementTransaction $settlement, $contract): array
{
// 授受状態の基本チェック
$statusChecks = [
'settlement_status' => $settlement->status === 'received',
'pay_date_exists' => !empty($settlement->pay_date),
'settlement_amount_valid' => $settlement->settlement_amount > 0,
'contract_not_cancelled' => $contract->contract_cancel_flag != 1,
];
$allValid = array_reduce($statusChecks, function($carry, $check) {
return $carry && $check;
}, true);
if (!$allValid) {
$failedChecks = array_keys(array_filter($statusChecks, function($check) {
return !$check;
}));
Log::warning('SHJ-4B 判断1: 授受状態異常', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'failed_checks' => $failedChecks,
'status_checks' => $statusChecks,
]);
return [
'valid' => false,
'reason' => '授受状態異常',
'failed_checks' => $failedChecks,
'message' => '決済トランザクションまたは契約の状態が更新処理に適していません',
];
}
Log::info('SHJ-4B 判断1: 授受状態正常', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'status_checks' => $statusChecks,
]);
return [
'valid' => true,
'reason' => '授受状態正常',
'status_checks' => $statusChecks,
'message' => '授受状態チェックに合格しました',
];
}
/**
* 【判断2】金額チェック
*
* @param SettlementTransaction $settlement
* @param object $contract
* @return array
*/
private function judgeAmountComparison(SettlementTransaction $settlement, $contract): array
{
// 文档要求:請求額=授受額の厳密比較
$billingAmount = (int) $contract->billing_amount; // 整数として比較
$settlementAmount = (int) $settlement->settlement_amount; // 整数として比較
$difference = $settlementAmount - $billingAmount;
if ($difference === 0) {
$comparison = self::AMOUNT_MATCH;
$result = '正常(金額一致)';
} elseif ($difference < 0) {
$comparison = self::AMOUNT_SHORTAGE;
$result = '授受過少';
} else {
$comparison = self::AMOUNT_EXCESS;
$result = '授受超過';
}
Log::info('SHJ-4B 判断2: 金額チェック完了', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'billing_amount' => $billingAmount,
'settlement_amount' => $settlementAmount,
'difference' => $difference,
'comparison' => $comparison,
'result' => $result,
]);
return [
'comparison' => $comparison,
'result' => $result,
'billing_amount' => $billingAmount,
'settlement_amount' => $settlementAmount,
'difference' => $difference,
'message' => "請求額: {$billingAmount}円, 授受額: {$settlementAmount}円, 結果: {$result}",
];
}
/**
* 登録済み処理
*
* @param SettlementTransaction $settlement
* @param array $contractResult
* @return array
*/
private function handleAlreadyProcessed(SettlementTransaction $settlement, array $contractResult): array
{
Log::info('SHJ-4B 登録済み処理', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contractResult['contract']->contract_id,
'reason' => $contractResult['reason'],
]);
return [
'success' => true,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contractResult['contract']->contract_id,
'contract_payment_number' => $settlement->contract_payment_number,
'result' => 'already_processed',
'reason' => $contractResult['reason'],
'message' => $contractResult['message'],
'skipped' => true,
];
}
/**
* パターンA/B判断
*
* 月を跨らないパターンAvs 月を跨るパターンBの判定
*
* @param object $contract
* @param SettlementTransaction $settlement
* @return array
*/
private function judgeContractPattern($contract, SettlementTransaction $settlement): array
{
$payDate = Carbon::parse($settlement->pay_date);
$contractStart = !empty($contract->contract_periods) ? Carbon::parse($contract->contract_periods) : null;
$contractEnd = !empty($contract->contract_periode) ? Carbon::parse($contract->contract_periode) : null;
// パターン判定ロジック
$isPatternB = false; // デフォルトはパターンA
$patternReason = 'パターンA月を跨らない';
if ($contractEnd) {
// 支払日が契約終了月の翌月以降の場合、パターンB跨月
if ($payDate->month > $contractEnd->month || $payDate->year > $contractEnd->year) {
$isPatternB = true;
$patternReason = 'パターンB月を跨る';
}
}
Log::info('SHJ-4B パターン判定', [
'contract_id' => $contract->contract_id,
'pay_date' => $payDate->format('Y-m-d'),
'contract_periods' => $contractStart?->format('Y-m-d'),
'contract_periode' => $contractEnd?->format('Y-m-d'),
'pattern' => $isPatternB ? 'B' : 'A',
'reason' => $patternReason,
]);
return [
'pattern' => $isPatternB ? 'B' : 'A',
'is_pattern_b' => $isPatternB,
'reason' => $patternReason,
'pay_date' => $payDate,
'contract_start' => $contractStart,
'contract_end' => $contractEnd,
];
}
/**
* 新規契約判定
*
* @param object $contract
* @return bool
*/
private function isNewContract($contract): bool
{
if (empty($contract->contract_created_at)) {
return false;
}
$createdAt = Carbon::parse($contract->contract_created_at);
$thirtyDaysAgo = Carbon::now()->subDays(30);
// 作成から30日以内を新規とみなす調整可能
$isNew = $createdAt->gte($thirtyDaysAgo);
Log::info('SHJ-4B 新規契約判定', [
'contract_id' => $contract->contract_id,
'contract_created_at' => $createdAt->format('Y-m-d H:i:s'),
'is_new' => $isNew,
'days_since_created' => $createdAt->diffInDays(Carbon::now()),
]);
return $isNew;
}
/**
* 【処理3】決済授受および写真削除 + 定期契約マスタ、定期予約マスタ更新
*
* @param SettlementTransaction $settlement
* @param object $contract
* @param array $amountResult
* @return array
*/
private function executeContractUpdate(
SettlementTransaction $settlement,
$contract,
array $amountResult
): array {
$updateData = [];
$updated = false;
try {
// パターンA/B判定
$pattern = $this->judgeContractPattern($contract, $settlement);
DB::transaction(function() use ($settlement, $contract, $amountResult, $pattern, &$updateData, &$updated) {
// 基本更新項目
$updateData = [
'contract_payment_day' => Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'),
'contract_updated_at' => now(),
];
// 金額比較結果に基づく contract_flag 設定
switch ($amountResult['comparison']) {
case self::AMOUNT_MATCH:
$updateData['contract_flag'] = self::CONTRACT_FLAG_UPDATED;
$updateData['contract_money'] = $settlement->settlement_amount;
break;
case self::AMOUNT_SHORTAGE:
case self::AMOUNT_EXCESS:
$updateData['contract_flag'] = self::CONTRACT_FLAG_ERROR;
$updateData['contract_money'] = $settlement->settlement_amount;
break;
}
// パターンBの場合の特殊処理
if ($pattern['is_pattern_b']) {
// 契約期間の延長処理等
if ($pattern['contract_end']) {
$newEndDate = $pattern['contract_end']->addMonth();
$updateData['contract_periode'] = $newEndDate->format('Y-m-d');
}
}
// 【定期契約マスタ更新】
$affectedRows = DB::table('regular_contract')
->where('contract_id', $contract->contract_id)
->update($updateData);
$updated = $affectedRows > 0;
// 【定期予約マスタ更新】reserve_idが設定されている場合
if (!empty($contract->reserve_id)) {
$reserveUpdateData = [
'updated_at' => now(),
];
// 金額一致の場合、予約を有効化
if ($amountResult['comparison'] === self::AMOUNT_MATCH) {
$reserveUpdateData['valid_flag'] = 1;
// パターンBの場合、予約期間も延長
if ($pattern['is_pattern_b'] && $pattern['contract_end']) {
$reserveUpdateData['reserve_end'] = $pattern['contract_end']->format('Y-m-d');
}
}
$reserveAffectedRows = DB::table('reserve')
->where('reserve_id', $contract->reserve_id)
->update($reserveUpdateData);
Log::info('SHJ-4B 定期予約マスタ更新完了', [
'reserve_id' => $contract->reserve_id,
'contract_id' => $contract->contract_id,
'reserve_update_data' => $reserveUpdateData,
'reserve_affected_rows' => $reserveAffectedRows,
]);
}
Log::info('SHJ-4B 定期契約マスタ更新完了', [
'contract_id' => $contract->contract_id,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'update_data' => $updateData,
'affected_rows' => $affectedRows,
'pattern' => $pattern['pattern'],
]);
});
return [
'updated' => $updated,
'update_data' => $updateData,
'message' => $updated ? '契約更新に成功しました' : '契約更新対象が見つかりませんでした',
];
} catch (\Throwable $e) {
Log::error('SHJ-4B 契約更新処理失敗', [
'contract_id' => $contract->contract_id,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* 副作用処理実行
*
* 決済授受および写真削除、新規連動等の処理
*
* @param SettlementTransaction $settlement
* @param object $contract
* @param array $amountResult
* @param array $updateResult
* @return array
*/
private function executeSideEffects(
SettlementTransaction $settlement,
$contract,
array $amountResult,
array $updateResult
): array {
$sideEffects = [];
try {
// 【処理3】写真削除処理金額一致かつ更新成功の場合
if ($amountResult['comparison'] === self::AMOUNT_MATCH && $updateResult['updated']) {
$sideEffects['photo_deletion'] = $this->executePhotoDeletion($contract);
}
// 【新規のみ】SHJ-13実行処理
if ($updateResult['updated'] && $amountResult['comparison'] === self::AMOUNT_MATCH) {
$isNewContract = $this->isNewContract($contract);
if ($isNewContract) {
$sideEffects['shj13_trigger'] = $this->triggerShjThirteen($contract);
}
}
// 【処理4】異常時のオペレーターキュー登録処理
if ($amountResult['comparison'] !== self::AMOUNT_MATCH) {
$sideEffects['operator_queue'] = $this->registerToOperatorQueue($settlement, $contract, $amountResult);
}
// 【処理5】利用者メール送信処理
if ($updateResult['updated']) {
$sideEffects['user_mail'] = $this->sendUserNotificationMail($settlement, $contract, $amountResult);
}
Log::info('SHJ-4B 副作用処理完了', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'side_effects' => array_keys($sideEffects),
]);
return $sideEffects;
} catch (\Throwable $e) {
Log::error('SHJ-4B 副作用処理失敗', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'error' => $e->getMessage(),
]);
// 副作用処理の失敗はメイン処理を止めない
return ['error' => $e->getMessage()];
}
}
/**
* 対象レコードなしの場合の処理
*
* @param SettlementTransaction $settlement
* @param array $contractResult
* @return array
*/
private function handleNoTargetRecord(SettlementTransaction $settlement, array $contractResult): array
{
Log::warning('SHJ-4B 対象レコードなし処理', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_payment_number' => $settlement->contract_payment_number,
'reason' => $contractResult['reason'],
]);
// TODO: 必要に応じて管理者通知やオペレーターキューへの登録
return [
'success' => true, // エラーではなく、正常な結果として扱う
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_payment_number' => $settlement->contract_payment_number,
'result' => 'no_target',
'reason' => $contractResult['reason'],
'message' => $contractResult['message'],
'action_required' => '管理者による手動確認が必要です',
];
}
/**
* 授受状態異常の場合の処理
*
* @param SettlementTransaction $settlement
* @param RegularContract $contract
* @param array $statusResult
* @return array
*/
private function handleInvalidStatus(
SettlementTransaction $settlement,
$contract,
array $statusResult
): array {
Log::warning('SHJ-4B 授受状態異常処理', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'reason' => $statusResult['reason'],
'failed_checks' => $statusResult['failed_checks'],
]);
// TODO: オペレーターキューへの登録や管理者通知
return [
'success' => false,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'result' => 'invalid_status',
'reason' => $statusResult['reason'],
'failed_checks' => $statusResult['failed_checks'],
'message' => $statusResult['message'],
'action_required' => 'オペレーターによる手動処理が必要です',
];
}
/**
* 写真削除処理
*
* @param object $contract
* @return array
*/
private function executePhotoDeletion($contract): array
{
// TODO: 実際の写真削除ロジックを実装
// 現在はプレースホルダー
Log::info('SHJ-4B 写真削除処理実行', [
'contract_id' => $contract->contract_id,
'user_id' => $contract->user_id,
]);
return [
'executed' => true,
'method' => 'placeholder',
'message' => '写真削除処理は実装予定です',
];
}
/**
* SHJ-13実行処理(新規のみ)
*
* @param object $contract
* @return array
*/
private function triggerShjThirteen($contract): array
{
// TODO: SHJ-13の具体的な処理を実装
// 現在はプレースホルダー
Log::info('SHJ-4B SHJ-13実行処理', [
'contract_id' => $contract->contract_id,
'user_id' => $contract->user_id,
'park_id' => $contract->park_id,
]);
return [
'triggered' => true,
'method' => 'placeholder',
'message' => 'SHJ-13処理は実装予定です',
'contract_id' => $contract->contract_id,
];
}
/**
* 利用者メール送信処理
*
* @param SettlementTransaction $settlement
* @param object $contract
* @param array $amountResult
* @return array
*/
private function sendUserNotificationMail(SettlementTransaction $settlement, $contract, array $amountResult): array
{
// TODO: 実際のメール送信処理を実装
// 現在はプレースホルダー
$mailType = ($amountResult['comparison'] === self::AMOUNT_MATCH) ? 'success' : 'error';
Log::info('SHJ-4B 利用者メール送信処理', [
'contract_id' => $contract->contract_id,
'user_id' => $contract->user_id,
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'mail_type' => $mailType,
'amount_comparison' => $amountResult['comparison'],
]);
return [
'sent' => true,
'method' => 'placeholder',
'mail_type' => $mailType,
'message' => '利用者メール送信処理は実装予定です',
];
}
/**
* オペレーターキューへの登録
*
* @param SettlementTransaction $settlement
* @param object $contract
* @param array $amountResult
* @return array
*/
private function registerToOperatorQueue(
SettlementTransaction $settlement,
$contract,
array $amountResult
): array {
// TODO: OperatorQue モデルを使用したキューへの登録処理を実装
Log::info('SHJ-4B オペレーターキュー登録処理実行', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'contract_id' => $contract->contract_id,
'amount_comparison' => $amountResult['comparison'],
'difference' => $amountResult['difference'],
]);
return [
'registered' => true,
'method' => 'placeholder',
'message' => 'オペレーターキュー登録処理は実装予定です',
];
}
}

View File

@ -11,7 +11,12 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
// SHJ-4A ウェルネット決済情報受信用エンドポイントのCSRF例外設定
// 外部システムからのPOSTリクエストのためCSRF保護を無効化
$middleware->validateCsrfTokens(except: [
'/shj4a', // SHJ-4A本番用エンドポイント
'/webhook/wellnet', // SHJ-4A開発・デバッグ用エンドポイント
]);
})
->withExceptions(function (Exceptions $exceptions) {
//

View File

@ -35,4 +35,11 @@ return [
],
],
'wellnet' => [
'response_format' => env('WELLNET_WEBHOOK_RESPONSE', 'json'),
'ip_whitelist' => env('WELLNET_IP_WHITELIST', ''),
'verify_signature' => env('WELLNET_VERIFY_SIGNATURE', false),
'signature_header' => env('WELLNET_SIGNATURE_HEADER', 'X-Wellnet-Signature'),
],
];

View File

@ -12,6 +12,6 @@ return [
|
*/
'failed' => '認証情報と一致するレコードがありません。',
'failed' => 'ログインID・パスワードが不正です。',
'throttle' => 'ログインの試行回数が多すぎます。:seconds 秒後にお試しください。',
];

View File

@ -11,12 +11,12 @@
<p class="login-box-msg">{{ __('ログインID、パスワードを入力して') }}<br
class="d-none d-lg-inline">{{ __('ログインボタンをクリックしてください') }}</p>
<form action="{{ route('login') }}" method="post">
<form action="{{ route('login') }}" method="post" novalidate>
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="input-group mb-3 {{ $errors->has('ope_id') ? 'error_input' : '' }}">
<input type="text"
class="form-control form-control-lg"
placeholder="{{ __('ログインID') }}" name="ope_id" value="{{ old('ope_id') }}" required>
<input type="text"
class="form-control form-control-lg"
placeholder="{{ __('ログインID') }}" name="ope_id" value="{{ old('ope_id') }}">
<div class="input-group-append">
<span class="fa fa-user input-group-text"></span>
</div>
@ -27,9 +27,9 @@
</div>
@endif
<div class="input-group mb-3 {{ $errors->has('ope_pass') ? 'error_input' : '' }}">
<input type="password"
class="form-control form-control-lg "
placeholder="{{ __('パスワード') }}" name="ope_pass" required>
<input type="password"
class="form-control form-control-lg "
placeholder="{{ __('パスワード') }}" name="ope_pass">
<div class="input-group-append">
<span class="fa fa-lock input-group-text"></span>
</div>

View File

@ -489,3 +489,17 @@ Route::middleware('auth')->group(function () {
//kin end
});
// Wellnet PUSH webhook (SHJ-4A)
// 外部からのウェルネット決済情報受信用エンドポイント
// 本番用SHJ-4A専用エンドポイント外部からのPUSH通知受信
Route::post('/shj4a', [App\Http\Controllers\Webhook\WellnetController::class, 'receive'])
->name('shj4a.webhook')
->withoutMiddleware(['auth', 'verified']); // 認証不要、外部アクセス用
// 開発・デバッグ用の既存エンドポイント(内部テスト用として保持)
Route::post('/webhook/wellnet', [App\Http\Controllers\Webhook\WellnetController::class, 'receive'])
->name('wellnet.webhook.receive')
->withoutMiddleware(['auth', 'verified']); // 認証不要