All checks were successful
Deploy api / deploy (push) Successful in 22s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
219 lines
8.1 KiB
PHP
219 lines
8.1 KiB
PHP
<?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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* リクエストパラメータ解析(短縮名・論理名 両方対応)
|
||
*/
|
||
private function parseParams(Request $request): array
|
||
{
|
||
$params = [];
|
||
|
||
// 短縮名で取得
|
||
$shortNames = ['ret', 'scd', 'rno', 'kcd', 'sdt', '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;
|
||
}
|
||
}
|