'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; } }