fix(nicepayments): 가상계좌 close-report 상태 보존

가상계좌 발급 후 입금대기 상태인 주문에 PC 결제창 닫힘 보고가 뒤늦게 도착해도 주문/결제 상태를 실패로 덮지 않도록 READY 상태만 실패 처리 대상으로 제한한다.
This commit is contained in:
thisgun
2026-07-09 12:53:16 +09:00
parent 5f99b104c1
commit 0200f1d043
3 changed files with 55 additions and 4 deletions
@@ -18,6 +18,7 @@
### Fixed
- 나이스페이먼츠 가상계좌 발급 후 뒤늦게 PC 결제창 닫힘 보고가 들어와도 입금대기 주문이 결제실패/취소로 덮이지 않도록 보강했습니다.
- 이미 부분취소된 나이스페이먼츠 거래의 남은 잔액을 다시 취소할 때 PG 취소 요청이 전액취소로 잘못 전달되어 실패하던 문제를 수정했습니다.
- 비회원 나이스페이먼츠 주문완료/주문상세에서 매출전표 조회가 인증 사용자 전용 라우트에 막히지 않도록 게스트 주문 토큰과 결제 성공 후 임시 영수증 쿠키 접근을 지원했습니다.
- 나이스페이먼츠 인증 단계 실패(`AuthResultCode != 0000`)는 실제 PG 승인 전이므로 주문/결제/주문옵션을 취소 상태로 바꾸지 않고 체크아웃 재시도가 가능하도록 수정했습니다.
@@ -8,6 +8,7 @@ use App\Helpers\ResponseHelper;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Modules\Sirsoft\Ecommerce\Models\Order;
use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService;
use Plugins\Sirsoft\PayNicepayments\Concerns\RecordsPaymentWindowClosure;
@@ -129,13 +130,15 @@ class PaymentCloseReportController
$payment = $lockedOrder->payment()->lockForUpdate()->first();
$lockedOrder->setRelation('payment', $payment);
// 결제 성공 콜백과 결제창 닫힘 보고가 경쟁할 때, 카드 주문은 승인 직전까지
// order_status=PENDING_ORDER(=isBeforePayment) 라 위 가드를 통과한다. payment_status 가
// 이미 PAID 면 결제가 성공한 것이므로 실패 처리하지 않는다(markPaymentWindowClosed 가
// 옵션을 취소로 덮어 주문/옵션 상태가 어긋나는 race 차단). 행 락 획득 직후 재확인한다.
// close-report 는 결제창이 승인/발급 단계로 넘어가기 전 READY 상태만 실패 처리한다.
// 카드 승인 콜백과 경쟁해 PAID 가 먼저 저장됐거나, 가상계좌 발급으로 WAITING_DEPOSIT 이
// 된 주문은 닫힘 보고가 뒤늦게 도착해도 주문/결제 상태를 덮으면 안 된다.
if ($payment?->isPaid()) {
return ['status' => 'ignored', 'reason' => 'payment_already_paid'];
}
if (! $payment || $payment->payment_status !== PaymentStatusEnum::READY) {
return ['status' => 'ignored', 'reason' => 'payment_not_ready'];
}
$expectedPrice = $this->resolveExpectedPaymentPriceOrNull($lockedOrder, 'close_report_locked', [
'oid' => $oid,
@@ -293,6 +293,53 @@ class PaymentCloseReportControllerTest extends PluginTestCase
$this->assertNotEquals(OrderStatusEnum::CANCELLED, $order->order_status);
}
public function test_close_report_ignores_vbank_order_waiting_for_deposit(): void
{
$order = OrderFactory::new()->create([
'order_number' => 'ORD-NICE-CLOSE-VBANK-WAIT',
'order_status' => OrderStatusEnum::PENDING_ORDER,
'currency' => 'KRW',
'currency_snapshot' => self::krwCurrencySnapshot(),
'subtotal_amount' => 10000,
'total_amount' => 10000,
'total_due_amount' => 10000,
'total_paid_amount' => 0,
]);
OrderPaymentFactory::new()->create([
'order_id' => $order->id,
'payment_status' => PaymentStatusEnum::WAITING_DEPOSIT,
'payment_method' => PaymentMethodEnum::VBANK,
'pg_provider' => 'nicepayments',
'paid_amount_local' => 0,
'transaction_id' => 'TID_VBANK_WAITING',
'vbank_name' => '국민은행',
'vbank_number' => '1234567890',
'vbank_issued_at' => now(),
'payment_meta' => [
'vbank_tid' => 'TID_VBANK_WAITING',
'vbank_num' => '1234567890',
'vbank_name' => '국민은행',
],
]);
$response = $this->postJson('/api/plugins/sirsoft-pay_nicepayments/payment/close-report', [
'oid' => 'ORD-NICE-CLOSE-VBANK-WAIT',
'price' => 10000,
'payment_method' => 'vbank',
'reason' => 'nicepay-window-closed',
]);
$response->assertOk()
->assertJsonPath('data.status', 'ignored')
->assertJsonPath('data.reason', 'payment_not_ready');
$order->refresh();
$this->assertEquals(OrderStatusEnum::PENDING_ORDER, $order->order_status);
$this->assertEquals(PaymentStatusEnum::WAITING_DEPOSIT, $order->payment->payment_status);
$this->assertSame('TID_VBANK_WAITING', $order->payment->transaction_id);
$this->assertNull($order->order_meta['payment_failure_code'] ?? null);
}
private function makeOrder(string $orderNumber, int $amount): Order
{
$order = new Order;