api.so-manager-dev.com/app/Http/Controllers/Api/PaymentCallbackController.php
Your Name f139a3f608
All checks were successful
Deploy api / deploy (push) Successful in 22s
支払いAPI実装
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 20:02:25 +09:00

219 lines
8.1 KiB
PHP
Raw Permalink 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');
}
}
/**
* リクエストパラメータ解析(短縮名・論理名 両方対応)
*/
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;
}
}