Files
Gnuboard7/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/ShippingAndCouponCancellationTest.php
T
2026-07-01 10:30:32 +09:00

685 lines
29 KiB
PHP

<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayKginicis\Tests\Feature\Controllers;
use App\Extension\HookListenerRegistrar;
use App\Extension\HookManager;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory;
use Modules\Sirsoft\Ecommerce\Enums\CouponIssueRecordStatus;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\SequenceType;
use Modules\Sirsoft\Ecommerce\Models\Coupon;
use Modules\Sirsoft\Ecommerce\Models\CouponIssue;
use Modules\Sirsoft\Ecommerce\Models\Order;
use Modules\Sirsoft\Ecommerce\Models\OrderOption;
use Modules\Sirsoft\Ecommerce\Models\OrderPayment;
use Modules\Sirsoft\Ecommerce\Models\OrderRefund;
use Modules\Sirsoft\Ecommerce\Models\OrderShipping;
use Modules\Sirsoft\Ecommerce\Models\Sequence;
use PHPUnit\Framework\MockObject\MockObject;
use Plugins\Sirsoft\PayKginicis\Listeners\PaymentRefundListener;
use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService;
use Plugins\Sirsoft\PayKginicis\Tests\PluginTestCase;
/**
* KG 이니시스 결제 플러그인 — 배송비·쿠폰 적용 취소 통합 테스트
*
* CLAUDE.local.md 의 "미검증 시나리오" 4건 (배송정책 + 전체/부분취소,
* 프로모션 + 전체/부분취소) 을 KG 이니시스 결제 경로에 대해 검증.
* NicePayments 와 동일한 시나리오 매트릭스를 따라 OrderCancellationService →
* PaymentRefundListener → KgInicisApiService 의 통합 경로를 점검한다.
*
* KgInicisApiService 만 mock 으로 교체하여 PG 호출의 인자 / 응답 / 예외 경로를
* 제어하고, 코어 OrderCancellationService 의 환불 금액 계산 + 쿠폰 복원 + 환불 레코드
* 생성 동작은 실제로 실행시킨다.
*/
class ShippingAndCouponCancellationTest extends PluginTestCase
{
private const TID = 'TID_KGINICIS_TEST_001';
private const CANCEL_SUCCESS = [
'resultCode' => '00',
'resultMsg' => '취소 성공',
'tid' => 'TID_CANCEL_KGINICIS',
];
protected User $adminUser;
protected function setUp(): void
{
parent::setUp();
// HookArgumentSerializer PHP 8.1 enum 직렬화 버그 우회
Queue::fake();
$this->adminUser = $this->createAdminUser(['sirsoft-ecommerce.orders.update']);
$this->createCancelSequences();
// 테스트 DB 시더가 plugin 레코드를 등록하지 않아
// PluginManager::registerPluginHookListeners() 의 active 가드에서 skip 되므로
// 테스트에서 직접 listener 를 등록한다. HookListenerRegistrar 가 idempotent.
HookListenerRegistrar::register(PaymentRefundListener::class, 'sirsoft-pay_kginicis');
}
protected function tearDown(): void
{
HookManager::resetAll();
HookListenerRegistrar::clear();
parent::tearDown();
}
// ────────────────────────────────────────────
// Mock 헬퍼
// ────────────────────────────────────────────
private function stubKgInicisCancelSuccess(): void
{
$mock = $this->createMock(KgInicisApiService::class);
$mock->method('cancelPayment')->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
}
private function expectKgInicisCancelOnce(): MockObject
{
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->once())
->method('cancelPayment')
->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
return $mock;
}
private function expectKgInicisCancelNever(): void
{
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->never())->method('cancelPayment');
$this->app->instance(KgInicisApiService::class, $mock);
}
private function stubKgInicisCancelFailure(string $errorMsg = 'KG 이니시스 취소 실패'): void
{
$mock = $this->createMock(KgInicisApiService::class);
$mock->method('cancelPayment')->willThrowException(new \Exception($errorMsg));
$this->app->instance(KgInicisApiService::class, $mock);
}
private function mockCouponRestore(): void
{
HookManager::addAction(
'sirsoft-ecommerce.coupon.restore',
function (Order $order, array $couponIssueIds) {
CouponIssue::whereIn('id', $couponIssueIds)->update([
'status' => CouponIssueRecordStatus::AVAILABLE->value,
'used_at' => null,
'order_id' => null,
]);
},
10
);
}
// ────────────────────────────────────────────
// 주문 생성 헬퍼
// ────────────────────────────────────────────
private function createCancelSequences(): void
{
foreach ([SequenceType::CANCEL, SequenceType::REFUND] as $type) {
$cfg = $type->getDefaultConfig();
Sequence::firstOrCreate(
['type' => $type->value],
[
'algorithm' => $cfg['algorithm']->value,
'prefix' => $cfg['prefix'],
'current_value' => 0,
'increment' => 1,
'min_value' => 1,
'max_value' => $cfg['max_value'],
'cycle' => false,
'pad_length' => $cfg['pad_length'],
]
);
}
}
/**
* @return array{order: Order, options: OrderOption[], payment: OrderPayment, shipping: OrderShipping}
*/
private function createKgInicisOrderWithShipping(
int $optionCount = 1,
int $unitPrice = 20000,
int $shippingFee = 3000,
string $tid = self::TID,
): array {
$user = User::factory()->create();
$subtotal = $unitPrice * $optionCount;
$total = $subtotal + $shippingFee;
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'subtotal_amount' => $subtotal,
'total_shipping_amount' => $shippingFee,
'total_amount' => $total,
'total_paid_amount' => $total,
'total_due_amount' => 0,
'total_cancelled_amount' => 0,
'cancellation_count' => 0,
'paid_at' => now(),
'promotions_applied_snapshot' => [],
'shipping_policy_applied_snapshot' => [],
]);
$options = [];
for ($i = 0; $i < $optionCount; $i++) {
$options[] = OrderOption::factory()->forOrder($order)->create([
'quantity' => 1,
'unit_price' => $unitPrice,
'subtotal_price' => $unitPrice,
'subtotal_paid_amount' => $unitPrice,
'subtotal_discount_amount' => 0,
'option_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'product_snapshot' => [
'id' => null, 'name' => ['ko' => '테스트상품', 'en' => 'Test Product'],
'product_code' => null, 'sku' => null, 'brand_id' => null,
'list_price' => $unitPrice, 'selling_price' => $unitPrice,
'currency_code' => 'KRW', 'stock_quantity' => 100,
'tax_status' => 'taxable', 'tax_rate' => 10,
'has_options' => false, 'option_groups' => null, 'thumbnail_url' => null,
],
'option_snapshot' => [
'id' => null, 'option_code' => null, 'option_values' => null,
'option_name' => '기본', 'price_adjustment' => 0,
'list_price' => $unitPrice, 'selling_price' => $unitPrice,
'currency_code' => 'KRW', 'stock_quantity' => 100,
'weight' => 0.5, 'volume' => 0.01, 'sku' => null, 'is_default' => true,
],
]);
}
// payment_meta.pay_method 는 PaymentRefundListener 가 KG 이니시스 cancelPayment 의
// payMethod 인자로 그대로 사용. 'Card' 가 기본 (신용카드 결제).
$payment = OrderPaymentFactory::new()->forOrder($order)->create([
'payment_status' => PaymentStatusEnum::PAID,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'kginicis',
'paid_amount_local' => $total,
'paid_amount_base' => $total,
'cancelled_amount' => 0,
'paid_at' => now(),
'transaction_id' => $tid,
'payment_meta' => ['pay_method' => 'Card'],
]);
$shipping = OrderShipping::factory()->forOrder($order)->create([
'order_option_id' => $options[0]->id,
'base_shipping_amount' => $shippingFee,
'total_shipping_amount' => $shippingFee,
]);
return compact('order', 'options', 'payment', 'shipping');
}
/**
* @return array{order: Order, options: OrderOption[], payment: OrderPayment, couponIssue: CouponIssue}
*/
private function createKgInicisOrderWithCoupon(
int $optionCount = 1,
int $unitPrice = 30000,
int $couponDiscount = 3000,
int $minOrderAmount = 0,
string $tid = self::TID,
): array {
$user = User::factory()->create();
$subtotal = $unitPrice * $optionCount;
$paid = $subtotal - $couponDiscount;
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'subtotal_amount' => $subtotal,
'total_shipping_amount' => 0,
'total_amount' => $subtotal,
'total_coupon_discount_amount' => $couponDiscount,
'total_order_coupon_discount_amount' => $couponDiscount,
'total_paid_amount' => $paid,
'total_due_amount' => 0,
'total_cancelled_amount' => 0,
'cancellation_count' => 0,
'paid_at' => now(),
'shipping_policy_applied_snapshot' => [],
]);
$options = [];
$perBase = (int) round($couponDiscount / $optionCount);
for ($i = 0; $i < $optionCount; $i++) {
$perDisc = $i < ($optionCount - 1)
? $perBase
: $couponDiscount - $perBase * ($optionCount - 1);
$options[] = OrderOption::factory()->forOrder($order)->create([
'quantity' => 1,
'unit_price' => $unitPrice,
'subtotal_price' => $unitPrice,
'subtotal_paid_amount' => $unitPrice - $perDisc,
'subtotal_discount_amount' => $perDisc,
'option_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'product_snapshot' => [
'id' => null, 'name' => ['ko' => '테스트상품', 'en' => 'Test Product'],
'product_code' => null, 'sku' => null, 'brand_id' => null,
'list_price' => $unitPrice, 'selling_price' => $unitPrice,
'currency_code' => 'KRW', 'stock_quantity' => 100,
'tax_status' => 'taxable', 'tax_rate' => 10,
'has_options' => false, 'option_groups' => null, 'thumbnail_url' => null,
],
'option_snapshot' => [
'id' => null, 'option_code' => null, 'option_values' => null,
'option_name' => '기본', 'price_adjustment' => 0,
'list_price' => $unitPrice, 'selling_price' => $unitPrice,
'currency_code' => 'KRW', 'stock_quantity' => 100,
'weight' => 0.5, 'volume' => 0.01, 'sku' => null, 'is_default' => true,
],
]);
}
$payment = OrderPaymentFactory::new()->forOrder($order)->create([
'payment_status' => PaymentStatusEnum::PAID,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'kginicis',
'paid_amount_local' => $paid,
'paid_amount_base' => $paid,
'cancelled_amount' => 0,
'paid_at' => now(),
'transaction_id' => $tid,
'payment_meta' => ['pay_method' => 'Card'],
]);
$coupon = Coupon::create([
'name' => '검수용 테스트 쿠폰',
'target_type' => 'order_amount',
'discount_type' => 'fixed',
'discount_value' => $couponDiscount,
'issue_method' => 'direct',
'issue_condition' => 'manual',
'issue_status' => 'issuing',
'total_quantity' => 100,
'issued_count' => 1,
'per_user_limit' => 1,
'valid_type' => 'period',
'is_combinable' => false,
]);
$couponIssue = CouponIssue::create([
'coupon_id' => $coupon->id,
'user_id' => $order->user_id,
'coupon_code' => 'TEST-'.strtoupper(uniqid()),
'status' => CouponIssueRecordStatus::USED->value,
'issued_at' => now(),
'used_at' => now(),
'order_id' => $order->id,
'discount_amount' => $couponDiscount,
]);
$order->update([
'promotions_applied_snapshot' => [
'coupon_issue_ids' => [$couponIssue->id],
'order_promotions' => [
'coupons' => [[
'coupon_issue_id' => $couponIssue->id,
'discount_type' => 'fixed',
'discount_value' => $couponDiscount,
'min_order_amount' => $minOrderAmount,
'applied_amount' => $couponDiscount,
]],
],
],
]);
return compact('order', 'options', 'payment', 'couponIssue');
}
// ====================================================
// 1. 배송정책 + 전체취소
// ====================================================
public function test_full_cancel_with_shipping_calls_kginicis_once_and_refunds_shipping(): void
{
$this->expectKgInicisCancelOnce();
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertOk()->assertJsonPath('success', true);
$order->refresh();
$this->assertEquals(OrderStatusEnum::CANCELLED, $order->order_status);
$refund = OrderRefund::where('order_id', $order->id)->first();
$this->assertNotNull($refund);
$this->assertEquals(3000, (float) $refund->refund_shipping_amount, '배송비 3,000원이 환불 레코드에 포함되어야 합니다');
$this->assertEquals(23000, (float) $refund->refund_amount, '상품(20,000) + 배송비(3,000) = 23,000원 환불');
}
public function test_full_cancel_passes_tid_and_amount_to_kginicis(): void
{
// KG 이니시스 cancelPayment 시그니처: (tid, payMethod, cancelPrice|null, msg, totalAmount|null)
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->once())
->method('cancelPayment')
->with(
self::TID, // tid
'Card', // payMethod (from payment_meta)
$this->anything(), // cancelPrice (전액취소 시 null, 부분취소 시 int — listener 가 결정)
$this->anything(), // cancelMsg
$this->anything(), // totalAmount
)
->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
])->assertOk()->assertJsonPath('success', true);
}
public function test_standard_cancel_restores_payment_time_mid_and_mode_before_refund(): void
{
$paymentMid = 'SIRLIVE001';
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->once())
->method('useStoredCredentials')
->with(false, $paymentMid);
$mock->expects($this->once())
->method('cancelPayment')
->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$payment = $data['payment'];
$payment->update([
'payment_meta' => [
'pay_method' => 'Card',
'mid' => $paymentMid,
'is_test_mode' => false,
],
]);
$this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
])->assertOk()->assertJsonPath('success', true);
}
public function test_full_cancel_free_shipping_refunds_only_product_amount(): void
{
$this->stubKgInicisCancelSuccess();
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 30000, shippingFee: 0);
$order = $data['order'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertOk()->assertJsonPath('success', true);
$refund = OrderRefund::where('order_id', $order->id)->first();
$this->assertNotNull($refund);
$this->assertEquals(0, (float) $refund->refund_shipping_amount);
$this->assertEquals(30000, (float) $refund->refund_amount);
}
// ====================================================
// 2. 배송정책 + 부분취소
// ====================================================
public function test_partial_cancel_with_shipping_creates_refund_record(): void
{
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->once())
->method('cancelPayment')
->with(
self::TID,
'Card',
23000,
$this->anything(),
43000,
)
->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
$data = $this->createKgInicisOrderWithShipping(optionCount: 2, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$options = $data['options'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'partial',
'items' => [['order_option_id' => $options[0]->id, 'cancel_quantity' => 1]],
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertOk()->assertJsonPath('success', true);
// 부분취소는 별도 주문 상태(partial_cancelled)를 두지 않는다 — 일부 옵션만 CANCELLED 로
// 전이되고 isPartiallyCancelled() 가 옵션 상태로 부분취소를 판정한다 (OrderCancellationService 정책).
$this->assertTrue($order->fresh(['options'])->isPartiallyCancelled(), '부분취소 후 부분취소 상태로 판정되어야 합니다');
$refund = OrderRefund::where('order_id', $order->id)->first();
$this->assertNotNull($refund, '부분취소 후 환불 레코드가 생성되어야 합니다');
$this->assertEquals(3000, (float) $refund->refund_shipping_amount, '부분취소 시 배송비 차액 3,000원이 포함되어야 합니다');
$this->assertEquals(23000, (float) $refund->refund_amount, '상품(20,000) + 배송비 차액(3,000) = 23,000원 환불');
}
public function test_partial_cancel_without_pg_never_calls_kginicis_api(): void
{
$this->expectKgInicisCancelNever();
$data = $this->createKgInicisOrderWithShipping(optionCount: 2, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$options = $data['options'];
$originalPaid = (float) $order->total_paid_amount;
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'partial',
'items' => [['order_option_id' => $options[0]->id, 'cancel_quantity' => 1]],
'reason' => 'changed_mind',
'cancel_pg' => false,
]);
$response->assertOk()->assertJsonPath('success', true);
$refund = OrderRefund::where('order_id', $order->id)->first();
$this->assertNotNull($refund);
$this->assertGreaterThan(0, (float) $refund->refund_amount);
$this->assertLessThanOrEqual($originalPaid, (float) $refund->refund_amount);
}
// ====================================================
// 3. 쿠폰 적용 + 전체취소
// ====================================================
public function test_full_cancel_with_coupon_refunds_paid_amount_and_restores_coupon(): void
{
$this->stubKgInicisCancelSuccess();
$this->mockCouponRestore();
$data = $this->createKgInicisOrderWithCoupon(optionCount: 1, unitPrice: 30000, couponDiscount: 3000);
$order = $data['order'];
$couponIssue = $data['couponIssue'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertOk()->assertJsonPath('success', true);
$refund = OrderRefund::where('order_id', $order->id)->first();
$this->assertNotNull($refund);
$this->assertEquals(27000, (float) $refund->refund_amount, '쿠폰 할인 후 실결제금액(27,000원)이 환불되어야 합니다');
$couponIssue->refresh();
$this->assertEquals(CouponIssueRecordStatus::AVAILABLE, $couponIssue->status, '쿠폰이 AVAILABLE 로 복원되어야 합니다');
$this->assertNull($couponIssue->order_id, '복원된 쿠폰의 order_id 가 null 이어야 합니다');
}
public function test_full_cancel_with_coupon_passes_net_paid_to_kginicis(): void
{
// 쿠폰 할인 후 실결제금액(27,000원)이 PG cancelPrice 또는 cancelMsg 인자로 전달되어야 함
$mock = $this->createMock(KgInicisApiService::class);
$mock->expects($this->once())
->method('cancelPayment')
->with(
self::TID,
'Card',
$this->anything(), // cancelPrice — listener 가 결정 (null = 전액, int = 부분)
$this->anything(),
$this->anything(),
)
->willReturn(self::CANCEL_SUCCESS);
$this->app->instance(KgInicisApiService::class, $mock);
$this->mockCouponRestore();
$data = $this->createKgInicisOrderWithCoupon(optionCount: 1, unitPrice: 30000, couponDiscount: 3000);
$order = $data['order'];
$this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
])->assertOk()->assertJsonPath('success', true);
}
// ====================================================
// 4. 쿠폰 적용 + 부분취소
// ====================================================
public function test_partial_cancel_with_coupon_succeeds_when_min_amount_still_met(): void
{
$this->stubKgInicisCancelSuccess();
$this->mockCouponRestore();
$data = $this->createKgInicisOrderWithCoupon(
optionCount: 2, unitPrice: 30000, couponDiscount: 3000, minOrderAmount: 20000
);
$order = $data['order'];
$options = $data['options'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'partial',
'items' => [['order_option_id' => $options[0]->id, 'cancel_quantity' => 1]],
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertOk()->assertJsonPath('success', true);
// 부분취소는 별도 주문 상태를 두지 않고, 옵션 상태(일부 CANCELLED)로 부분취소를 판정한다.
$this->assertTrue($order->fresh(['options'])->isPartiallyCancelled(), '부분취소 후 부분취소 상태로 판정되어야 합니다');
}
public function test_partial_cancel_rejected_when_coupon_condition_no_longer_met(): void
{
$this->expectKgInicisCancelNever();
$data = $this->createKgInicisOrderWithCoupon(
optionCount: 2, unitPrice: 20000, couponDiscount: 22000, minOrderAmount: 30000
);
$order = $data['order'];
$options = $data['options'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'partial',
'items' => [['order_option_id' => $options[0]->id, 'cancel_quantity' => 1]],
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertStatus(422)->assertJsonPath('success', false);
}
// ====================================================
// 5. KG 이니시스 API 오류
// ====================================================
public function test_cancel_fails_and_rolls_back_when_kginicis_api_throws(): void
{
$this->stubKgInicisCancelFailure('KG 이니시스 취소 실패: 거래가 만료되었습니다');
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => true,
]);
$response->assertStatus(422)->assertJsonPath('success', false);
$order->refresh();
$this->assertNotEquals(OrderStatusEnum::CANCELLED, $order->order_status, 'PG 환불 실패 시 주문 상태 변경이 롤백되어야 합니다');
}
// ====================================================
// 6. non-kginicis PG 주문에 대한 무간섭 검증
// ====================================================
public function test_listener_skips_non_kginicis_payment(): void
{
$this->expectKgInicisCancelNever();
$data = $this->createKgInicisOrderWithShipping(optionCount: 1, unitPrice: 20000, shippingFee: 3000);
$order = $data['order'];
$payment = $data['payment'];
$payment->update(['pg_provider' => 'other_pg']);
$response = $this->actingAs($this->adminUser)
->postJson("/api/modules/sirsoft-ecommerce/admin/orders/{$order->order_number}/cancel", [
'type' => 'full',
'reason' => 'changed_mind',
'cancel_pg' => false,
]);
$response->assertOk()->assertJsonPath('success', true);
$order->refresh();
$this->assertEquals(OrderStatusEnum::CANCELLED, $order->order_status);
}
}