All checks were successful
Deploy api / deploy (push) Successful in 22s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
187 lines
7.0 KiB
PHP
187 lines
7.0 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Api;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Http\Requests\Api\RefundRequest;
|
||
use App\Models\PaymentTransaction;
|
||
use App\Services\Wellnet\WellnetCreditService;
|
||
use Illuminate\Http\JsonResponse;
|
||
use RuntimeException;
|
||
|
||
/**
|
||
* 返金コントローラ(API 6)
|
||
*/
|
||
class RefundController extends Controller
|
||
{
|
||
private const REFUND_STATUS_REFUNDED = 'REFUNDED';
|
||
private const REFUND_STATUS_PENDING = 'REFUND_PENDING';
|
||
|
||
/**
|
||
* 返金処理
|
||
*
|
||
* POST /api/newwipe/refund
|
||
*/
|
||
public function refund(
|
||
RefundRequest $request,
|
||
WellnetCreditService $creditService
|
||
): JsonResponse {
|
||
$validated = $request->validated();
|
||
|
||
// トランザクション検索
|
||
$transaction = PaymentTransaction::where('syuno_recv_num', $validated['SyunoRecvNum'])->first();
|
||
|
||
if (!$transaction) {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'E17',
|
||
'message' => '対象取引が未入金または存在しません。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
// 入金済みチェック
|
||
if ($transaction->status !== '入金済み') {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'E17',
|
||
'message' => '未入金のため返金できません。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
// クレジット決済のみ返金可能
|
||
if ($transaction->payment_type !== 'credit') {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'INVALID_REQUEST',
|
||
'message' => 'コンビニ決済の返金はこのAPIでは対応していません。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
// 既に返金済みチェック
|
||
if ($transaction->refund_status === 'REFUNDED') {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'D01',
|
||
'message' => '既に返金処理済みです。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
// 決済番号チェック(CAFIS決済番号が必要)
|
||
if (empty($transaction->payment_number)) {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'E20',
|
||
'message' => 'クレジット決済番号(paymentNumber)が未取得のため返金できません。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
// 返金額計算
|
||
$refundAmount = $validated['refundAmount'] ?? $transaction->amount;
|
||
$afterRefundPayAmount = $transaction->amount - $refundAmount;
|
||
|
||
if ($afterRefundPayAmount < 0) {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => 'INVALID_REQUEST',
|
||
'message' => '返金額が決済金額を超えています。',
|
||
]
|
||
], 400);
|
||
}
|
||
|
||
$creditResponse = null;
|
||
try {
|
||
// 全額かつ与信取消可能な場合
|
||
if ($afterRefundPayAmount === 0) {
|
||
$creditResponse = $creditService->cancelAuthorize($transaction->payment_number);
|
||
} else {
|
||
// 部分返金(差額返金)
|
||
$creditResponse = $creditService->refund($transaction->payment_number, $afterRefundPayAmount);
|
||
}
|
||
} catch (RuntimeException $e) {
|
||
$mapped = $this->mapCreditExceptionToApiError($e);
|
||
// 与信取消失敗時はrefundで再試行
|
||
if ($afterRefundPayAmount === 0 && str_contains($e->getMessage(), 'INVALID_OPERATION')) {
|
||
try {
|
||
$creditResponse = $creditService->refund($transaction->payment_number, 0);
|
||
} catch (RuntimeException $retryException) {
|
||
$retryMapped = $this->mapCreditExceptionToApiError($retryException);
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => $retryMapped['code'],
|
||
'message' => $retryMapped['message'],
|
||
]
|
||
], $retryMapped['code'] === 'E99' ? 500 : 400);
|
||
}
|
||
} else {
|
||
return response()->json([
|
||
'error' => [
|
||
'code' => $mapped['code'],
|
||
'message' => $mapped['message'],
|
||
]
|
||
], $mapped['code'] === 'E99' ? 500 : 400);
|
||
}
|
||
}
|
||
|
||
// トランザクション更新
|
||
$refundId = 'REF-' . date('YmdHis') . '-' . substr(uniqid(), -6);
|
||
$refundStatus = $creditResponse['refundStatus'] ?? self::REFUND_STATUS_REFUNDED;
|
||
$transaction->update([
|
||
'refund_amount' => $refundAmount,
|
||
'refund_status' => $refundStatus,
|
||
'refund_id' => $refundId,
|
||
]);
|
||
|
||
$response = [
|
||
'SyunoRecvNum' => $transaction->syuno_recv_num,
|
||
'refundStatus' => $refundStatus,
|
||
'refundAmount' => $refundAmount,
|
||
'originalAmount' => $transaction->amount,
|
||
'refundId' => $refundId,
|
||
];
|
||
if ($transaction->payment_number) {
|
||
$response['transactionId'] = $transaction->payment_number;
|
||
}
|
||
|
||
return response()->json($response);
|
||
}
|
||
|
||
/**
|
||
* Wellnet/CAFISエラーをapi.pdfの返金APIエラーコードへ寄せる
|
||
*/
|
||
private function mapCreditExceptionToApiError(RuntimeException $e): array
|
||
{
|
||
$message = $e->getMessage();
|
||
|
||
if (preg_match('/\\bE29\\b/u', $message) || str_contains($message, '有効期限が過ぎ')) {
|
||
return ['code' => 'E29', 'message' => '期限経過: ' . $message];
|
||
}
|
||
if (str_contains($message, 'PAYMENT_NUMBER_DOES_NOT_EXIST')) {
|
||
return ['code' => 'E20', 'message' => '取引不明: ' . $message];
|
||
}
|
||
if (str_contains($message, 'INVALID_OPERATION_REFUNDED')) {
|
||
return ['code' => 'D01', 'message' => '返金不可: ' . $message];
|
||
}
|
||
if (str_contains($message, 'INVALID_OPERATION_CANCELED')) {
|
||
return ['code' => 'D01', 'message' => '返金不可: ' . $message];
|
||
}
|
||
if (preg_match('/\\bD0[2-8]\\b/u', $message) || str_contains($message, 'D81')) {
|
||
// D02: 売上前 / D03: 差額なし(取消推奨) / D04: 返金後金額不正 / D05: 対象なし
|
||
// D06-08: 二重等 / D81: 差額返金済の為取消不可
|
||
return ['code' => 'E30', 'message' => '処理不可: ' . $message];
|
||
}
|
||
if (str_contains($message, 'HTTP 423')) {
|
||
return ['code' => 'E30', 'message' => '処理不可: ' . $message];
|
||
}
|
||
if (str_contains($message, 'USER_ACTION_IN_PROGRESS')) {
|
||
return ['code' => 'E30', 'message' => '処理不可: ' . $message];
|
||
}
|
||
|
||
return ['code' => 'E99', 'message' => '返金処理に失敗しました: ' . $message];
|
||
}
|
||
}
|