api.so-manager-dev.com/app/Http/Controllers/Api/CreditPaymentController.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

328 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\Exceptions\WellnetSoapException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreditLinkRequest;
use App\Http\Requests\Api\SubscriptionChargeRequest;
use App\Models\CreditPaymentLog;
use App\Models\CreditSubscription;
use App\Models\PaymentTransaction;
use App\Services\Wellnet\PaymentLinkBuilder;
use App\Services\Wellnet\WellnetCreditService;
use App\Services\Wellnet\WellnetSoapService;
use Illuminate\Http\JsonResponse;
use RuntimeException;
/**
* クレジット決済コントローラAPI 1.1 + 1.2
*/
class CreditPaymentController extends Controller
{
/**
* クレジット支払リンク生成API 1.1
*
* POST /api/newwipe/credit/link
*/
public function createLink(
CreditLinkRequest $request,
WellnetSoapService $soapService,
PaymentLinkBuilder $linkBuilder
): JsonResponse {
$validated = $request->validated();
// 受付番号の重複チェック
$existing = PaymentTransaction::where('syuno_recv_num', $validated['SyunoRecvNum'])->first();
if ($existing) {
// 期限切れDB未更新を補正
if ($existing->status === '入金待ち' && $existing->pay_limit && $existing->pay_limit->lessThan(now())) {
$existing->update(['status' => '支払期限切れ']);
}
if ($existing->status === '入金済み') {
return response()->json([
'error' => ['code' => 'E17', 'message' => '入金処理完了済みの受付番号です。']
], 400);
}
if ($existing->status === '支払期限切れ') {
return response()->json([
'error' => ['code' => 'E23', 'message' => '支払期限切れの受付番号です。']
], 400);
}
if ($existing->status === '取消済み') {
return response()->json([
'error' => ['code' => 'E24', 'message' => '取消済みの受付番号です。']
], 400);
}
// 冪等対応:入金待ちの既存データがある場合は同一リンクを返す
if (!empty($existing->kessai_no) && $existing->status === '入金待ち') {
$creditUrl = $linkBuilder->buildCreditUrl($existing->kessai_no);
return response()->json([
'result' => 'OK',
'creditPaymentUrl' => $creditUrl,
'SyunoRecvNum' => $existing->syuno_recv_num,
]);
}
return response()->json([
'error' => ['code' => 'INVALID_REQUEST', 'message' => '既に登録済みの受付番号です。']
], 400);
}
// SyunoFreeArray構築
$freeArray = $this->buildFreeArray($validated);
// SOAP送信データ構築
$inData = [
'SyunoRecvNum' => $validated['SyunoRecvNum'],
'SyunoTel' => $validated['SyunoTel'],
'SyunoNameKanji' => $validated['SyunoNameKanji'],
'SyunoPayLimit' => $validated['SyunoPayLimit'],
'SyunoPayAmount' => (string) $validated['SyunoPayAmount'],
'SyunoFreeArray' => $freeArray,
];
if (!empty($validated['SyunoNameKana'])) {
$inData['SyunoNameKana'] = $validated['SyunoNameKana'];
}
if (!empty($validated['SyunoReserveNum'])) {
$inData['SyunoReserveNum'] = $validated['SyunoReserveNum'];
}
try {
// Wellnet受付登録
$result = $soapService->register($inData);
} catch (WellnetSoapException $e) {
$code = $e->getWellnetCode();
$errorCode = $this->normalizeWellnetErrorCode($code);
return response()->json([
'error' => [
'code' => $errorCode,
'message' => 'Wellnet受付登録に失敗しました: ' . $e->getMessage(),
]
], 400);
} catch (RuntimeException $e) {
return response()->json([
'error' => [
'code' => 'E99',
'message' => 'Wellnet通信エラー: ' . $e->getMessage(),
]
], 500);
}
$kessaiNo = $result['KKessaiNo'];
// 決済リンク生成
$creditUrl = $linkBuilder->buildCreditUrl($kessaiNo);
// トランザクション保存
PaymentTransaction::create([
'syuno_recv_num' => $validated['SyunoRecvNum'],
'payment_type' => 'credit',
'status' => '入金待ち',
'amount' => $validated['SyunoPayAmount'],
'pay_limit' => $this->parsePayLimit($validated['SyunoPayLimit']),
'kessai_no' => $kessaiNo,
'name_kanji' => $validated['SyunoNameKanji'],
'name_kana' => $validated['SyunoNameKana'] ?? null,
'tel' => $validated['SyunoTel'],
'subscription_flg' => $validated['subscription_flg'],
'free_area' => $this->buildFreeAreaJson($validated),
'wellnet_response' => json_encode($result, JSON_UNESCAPED_UNICODE),
]);
return response()->json([
'result' => 'OK',
'creditPaymentUrl' => $creditUrl,
'SyunoRecvNum' => $validated['SyunoRecvNum'],
]);
}
/**
* 継続課金請求API 1.2
*
* POST /api/newwipe/credit/subscription/charge
*/
public function chargeSubscription(
SubscriptionChargeRequest $request,
WellnetCreditService $creditService
): JsonResponse {
$validated = $request->validated();
// 継続課金会員情報取得
$subscription = CreditSubscription::where('credit_member_id', $validated['member_id'])
->where('subscription_status', 1)
->first();
if (!$subscription) {
return response()->json([
'error' => [
'code' => 'E21',
'message' => '継続課金が未登録です。',
]
], 400);
}
$cardSeq = $validated['card_seq'] ?? $subscription->credit_card_seq;
// execute_dateログ用途
$executeDate = $validated['execute_date'];
$executeDateParsed = \Carbon\Carbon::createFromFormat('Ymd', $executeDate, 'Asia/Tokyo')->startOfDay();
$today = \Carbon\Carbon::now('Asia/Tokyo')->startOfDay();
// execute_dateをDB保存用に変換YYYYMMDD → YYYY-MM-DD
$executeDateFormatted = $executeDateParsed->format('Y-m-d');
// api.pdf 上は execute_date の制約が明記されていないため、日付の実在性のみをバリデーションで担保し、
// ここでは未来日/過去日の制限は行わずそのまま処理する。
// 請求履歴レコード作成processing
$log = CreditPaymentLog::create([
'credit_subscription_id' => $subscription->credit_subscription_id,
'contract_id' => $validated['contract_id'],
'user_id' => $subscription->user_id,
'request_amount' => $validated['amount'],
'execute_date' => $executeDateFormatted,
'request_at' => now(),
'status' => 'pending',
]);
try {
// 与信照会
$authResult = $creditService->authorize(
$subscription->credit_member_id,
$validated['amount'],
$cardSeq
);
$paymentNumber = $authResult['paymentNumber'] ?? null;
// 売上確定
$creditService->capture($paymentNumber);
// 成功時の履歴更新
$log->update([
'response_result' => 'OK',
'payment_number' => $paymentNumber,
'status' => 'success',
]);
// payment_transactionにも記録返金用にpayment_numberを保持
// syuno_recv_numは20文字以内の英数字で生成契約IDの文字種に依存しない
$syunoRecvNum = 'S' . substr(hash('sha256', $validated['contract_id'] . '|' . $executeDate), 0, 19);
PaymentTransaction::create([
'syuno_recv_num' => $syunoRecvNum,
'payment_type' => 'credit',
'status' => '入金済み',
'amount' => $validated['amount'],
'payment_number' => $paymentNumber,
'subscription_flg' => 1,
'paid_datetime' => now(),
]);
return response()->json([
'result' => 'OK',
'contract_id' => $validated['contract_id'],
'member_id' => $validated['member_id'],
]);
} catch (RuntimeException $e) {
// エラー時の履歴更新
$log->update([
'response_result' => 'NG',
'response_message' => $e->getMessage(),
'status' => 'error',
]);
$mapped = $this->mapCreditException($e);
return response()->json([
'error' => [
'code' => $mapped['code'],
'message' => $mapped['message'],
]
], $mapped['http']);
}
}
/**
* SyunoFreeArray構築Index付き配列
*/
private function buildFreeArray(array $validated): array
{
$freeArray = [];
for ($i = 1; $i <= 23; $i++) {
$key = 'SyunoFree' . $i;
if (isset($validated[$key]) && $validated[$key] !== '') {
$freeArray[$i] = $validated[$key];
}
}
return $freeArray;
}
/**
* Free AreaをJSON保存用に構築
*/
private function buildFreeAreaJson(array $validated): ?array
{
$freeArea = [];
for ($i = 1; $i <= 23; $i++) {
$key = 'SyunoFree' . $i;
if (isset($validated[$key]) && $validated[$key] !== '') {
$freeArea[$key] = $validated[$key];
}
}
return !empty($freeArea) ? $freeArea : null;
}
/**
* 支払期限文字列YYYYMMDDhhmmをdatetime形式に変換
*/
private function parsePayLimit(string $payLimit): string
{
// YYYYMMDDhhmm → YYYY-MM-DD hh:mm:00
return substr($payLimit, 0, 4) . '-'
. substr($payLimit, 4, 2) . '-'
. substr($payLimit, 6, 2) . ' '
. substr($payLimit, 8, 2) . ':'
. substr($payLimit, 10, 2) . ':00';
}
/**
* WellnetエラーコードをAPIエラーコード形式へ正規化
*/
private function normalizeWellnetErrorCode(string $code): string
{
if (!ctype_digit($code)) {
return $code;
}
$trimmed = ltrim($code, '0');
return 'R' . ($trimmed === '' ? '0' : $trimmed);
}
/**
* クレジットAPIの例外をAPIエラーへマップ
*/
private function mapCreditException(RuntimeException $e): array
{
$status = (int) $e->getCode();
$message = $e->getMessage();
if ($status === 401 || $status === 403) {
return ['code' => 'E01', 'message' => '利用不可: ' . $message, 'http' => 400];
}
if ($status === 404) {
return ['code' => 'E02', 'message' => 'データエラー: ' . $message, 'http' => 400];
}
if ($status === 400) {
return ['code' => 'E09', 'message' => 'データエラー: ' . $message, 'http' => 400];
}
if (str_contains($message, 'INVALID_OPERATION')) {
return ['code' => 'E13', 'message' => 'データエラー: ' . $message, 'http' => 400];
}
return ['code' => 'E99', 'message' => '継続課金処理に失敗しました: ' . $message, 'http' => 500];
}
}