api.so-manager-dev.com/app/Http/Controllers/Api/PaymentCallbackController.php
Your Name 41814dd908
All checks were successful
Deploy api / deploy (push) Successful in 23s
SHJ-4 SHJ-5 SHJ-6 変更点実装
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:25:40 +09:00

305 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PaymentTransaction;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* 決済結果通知callback受信コントローラAPI 3
*/
class PaymentCallbackController extends Controller
{
/**
* パラメータマッピング(短縮名 → 論理名)
*/
private const PARAM_MAP = [
'ret' => 'ret',
'scd' => 'SyunoPayCode',
'rno' => 'SyunoRecvNum',
'kcd' => 'SyunoCorpCode',
'sdt' => 'SyunoReserveDateTime',
'ccd' => 'CvsCode',
'tcd' => 'SyunoStoreCode',
'nno' => 'MmsNumber',
'ndt' => 'SyunoPayDatetime',
'pri' => 'SyunoPayAmount',
'ifg' => 'StampFlag',
'pwd' => 'Md5Hash',
];
/**
* 入金通知受信
*
* POST /api/newwipe/callback
*/
public function receive(Request $request): Response
{
try {
// Wellnet疎通確認用のGETパラメータなしは常に200+000で応答
if ($request->isMethod('get') && $request->all() === []) {
return response('000', 200)->header('Content-Type', 'text/plain');
}
// パラメータ解析(短縮名・論理名 両方対応)
$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'] ?? '');
$priRaw = (string) ($params['pri'] ?? '');
// 仕様上、各項目が空白埋めされる可能性があるためトリム値も用意
$ret = trim($retRaw);
$scd = trim($scdRaw);
$rno = trim($rnoRaw);
$kcd = trim($kcdRaw);
$pwd = trim($pwdRaw);
$pri = trim($priRaw);
if ($rno === '') {
return response('600', 200)->header('Content-Type', 'text/plain');
}
// MD5検証
$secretKey = config('wellnet.callback.md5_secret');
if (empty($secretKey)) {
if (app()->environment('production')) {
\Illuminate\Support\Facades\Log::critical('WELLNET_MD5_SECRET未設定のためcallback拒否');
return response('800', 200)->header('Content-Type', 'text/plain');
}
\Illuminate\Support\Facades\Log::warning('WELLNET_MD5_SECRET未設定: MD5検証スキップ');
} else {
// 空白埋めの有無によりMD5算出対象が異なる可能性があるため、raw/trimの両方を許容
$expectedHashRaw = md5("ret{$retRaw}scd{$scdRaw}rno{$rnoRaw}{$secretKey}");
$expectedHashTrim = md5("ret{$ret}scd{$scd}rno{$rno}{$secretKey}");
if ($pwd !== $expectedHashRaw && $pwd !== $expectedHashTrim) {
return response('800', 200)->header('Content-Type', 'text/plain');
}
}
// トランザクション検索
$transaction = PaymentTransaction::where('syuno_recv_num', $rno)->first();
if (!$transaction) {
return response('600', 200)->header('Content-Type', 'text/plain');
}
// pay_code / corp_code / 金額の整合性チェック(データ不備扱い)
$expectedPayCode = (string) config('wellnet.payment.pay_code', '');
if ($expectedPayCode !== '' && $scd !== '' && $scd !== $expectedPayCode) {
return response('600', 200)->header('Content-Type', 'text/plain');
}
$expectedCorpCode = (string) config('wellnet.payment.corp_code', '');
if ($expectedCorpCode !== '' && $kcd !== '' && $kcd !== $expectedCorpCode) {
return response('600', 200)->header('Content-Type', 'text/plain');
}
if ($pri !== '' && ctype_digit($pri)) {
if ((int) $pri !== (int) $transaction->amount) {
return response('600', 200)->header('Content-Type', 'text/plain');
}
}
// 冪等性チェック:既に入金済みの場合は正常応答
if ($transaction->status === '入金済み') {
return response('000', 200)->header('Content-Type', 'text/plain');
}
// retが"000"(正常入金)でない場合はステータス更新しない
if ($ret !== '000') {
return response('000', 200)->header('Content-Type', 'text/plain');
}
// ステータス更新
$updateData = [
'status' => '入金済み',
];
if (!empty($ndt)) {
$updateData['paid_datetime'] = $this->parseDatetime($ndt);
}
if (!empty($tcd)) {
$updateData['store_code'] = $tcd;
}
if (!empty($ccd)) {
$updateData['cvs_code'] = $ccd;
}
// クレジット決済の場合、CAFIS決済番号paymentNumberが送られてくる契約形態に備えて保存
$paymentNumber = $request->input('paymentNumber')
?? $request->input('payment_number')
?? $request->input('paymentnumber')
?? $request->input('pno')
?? null;
if (!empty($paymentNumber) && empty($transaction->payment_number)) {
$updateData['payment_number'] = $paymentNumber;
}
$transaction->update($updateData);
return response('000', 200)->header('Content-Type', 'text/plain');
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('Wellnet callback処理エラー: ' . $e->getMessage(), [
'exception' => $e,
]);
return response('100', 200)->header('Content-Type', 'text/plain');
}
}
/**
* 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');
}
}
/**
* リクエストパラメータ解析(短縮名・論理名 両方対応)
*/
private function parseParams(Request $request): array
{
$params = [];
// 短縮名で取得
$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) {
$params[$name] = $value;
}
}
// 論理名でも取得(短縮名が無い場合のフォールバック)
$logicalMap = [
'SyunoRecvNum' => 'rno',
'SyunoPayCode' => 'scd',
'SyunoCorpCode' => 'kcd',
'SyunoPayDatetime' => 'ndt',
'SyunoPayAmount' => 'pri',
'SyunoStoreCode' => 'tcd',
'CvsCode' => 'ccd',
'Md5Hash' => 'pwd',
];
foreach ($logicalMap as $logical => $short) {
if (!isset($params[$short])) {
$value = $request->input($logical);
if ($value !== null) {
$params[$short] = $value;
}
}
}
// retが未設定の場合のフォールバック
if (!isset($params['ret'])) {
$value = $request->input('result');
if ($value !== null) {
$params['ret'] = $value;
}
}
return $params;
}
/**
* Wellnet日時文字列YYYYMMDDHHMMSSをdatetime形式に変換
*/
private function parseDatetime(string $datetime): string
{
if (strlen($datetime) >= 14) {
return substr($datetime, 0, 4) . '-'
. substr($datetime, 4, 2) . '-'
. substr($datetime, 6, 2) . ' '
. substr($datetime, 8, 2) . ':'
. substr($datetime, 10, 2) . ':'
. substr($datetime, 12, 2);
}
return $datetime;
}
}