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