Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/Unit/Console/CancelPendingPaymentOrdersCommandTest.php
T
2026-07-01 10:30:32 +09:00

290 lines
10 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Console;
use App\Contracts\Extension\ModuleInterface;
use App\Contracts\Extension\ModuleManagerInterface;
use App\Contracts\Extension\ModuleSettingsInterface;
use App\Models\User;
use App\Services\ModuleSettingsService;
use Carbon\Carbon;
use Mockery;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Models\Order;
use Modules\Sirsoft\Ecommerce\Models\OrderPayment;
use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* ModuleInterface + ModuleSettingsInterface 결합 스텁
* (두 인터페이스가 getSettingsDefaultsPath()를 공유하므로 intersection mock 불가 → abstract class 사용)
*/
abstract class CancelPendingPaymentOrdersModuleStub implements ModuleInterface, ModuleSettingsInterface {}
/**
* 입금 기한 만료 주문 자동 취소 커맨드 테스트
*/
class CancelPendingPaymentOrdersCommandTest extends ModuleTestCase
{
/**
* 테스트용 모듈 설정값
*/
private array $moduleSettings = [];
protected function setUp(): void
{
parent::setUp();
// 자동 취소 기능 활성화 (module_setting() mock)
$this->moduleSettings = [
'order_settings.auto_cancel_expired' => true,
];
$this->mockModuleSetting();
}
/**
* module_setting() 헬퍼가 사용하는 ModuleManagerInterface를 mock합니다.
*/
private function mockModuleSetting(): void
{
$mockModule = $this->createMock(CancelPendingPaymentOrdersModuleStub::class);
$mockModule->method('getSetting')
->willReturnCallback(function (string $key, mixed $default = null) {
return array_key_exists($key, $this->moduleSettings)
? $this->moduleSettings[$key]
: $default;
});
$mockModuleManager = $this->createMock(ModuleManagerInterface::class);
$mockModuleManager->method('getModule')
->with('sirsoft-ecommerce')
->willReturn($mockModule);
$this->app->instance(ModuleManagerInterface::class, $mockModuleManager);
// ModuleSettingsService 는 ModuleManagerInterface 를 생성 시점에 주입받으므로
// instance 교체 후 기존 service 인스턴스를 forget 해 다음 resolve 에서 새 mock 사용
$this->app->forgetInstance(ModuleSettingsService::class);
// sirsoft-ecommerce 는 전용 EcommerceSettingsService 가 자동 discover 되어
// ModuleSettingsService::get 에서 먼저 위임됨. 이 서비스를 모듈 설정 mock 으로 교체.
$mockEcommerceSettings = Mockery::mock(EcommerceSettingsService::class)->makePartial();
$mockEcommerceSettings->shouldReceive('getSetting')
->andReturnUsing(function (string $key, mixed $default = null) {
return array_key_exists($key, $this->moduleSettings)
? $this->moduleSettings[$key]
: $default;
});
$this->app->instance(EcommerceSettingsService::class, $mockEcommerceSettings);
}
public function test_command_exists(): void
{
$this->artisan('sirsoft-ecommerce:cancel-pending-orders --dry-run')
->assertSuccessful();
}
public function test_cancels_expired_vbank_order(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
// 만료된 가상계좌 결제
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->subDay(), // 1일 전 만료
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::CANCELLED, $order->order_status);
}
public function test_cancels_expired_dbank_manual_deposit_order(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
// 만료된 무통장입금(수동 입금확인) 결제 (DBANK 메서드 + deposit_due_at 사용)
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::DBANK,
'deposit_due_at' => Carbon::now()->subDay(), // 1일 전 만료
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::CANCELLED, $order->order_status);
}
/**
* 계좌이체(BANK)는 입금 기한 만료 자동취소 대상이 아니다.
*
* 자동취소 대상은 가상계좌(VBANK)와 무통장입금(DBANK)뿐이다.
* 과거 쿼리가 DBANK 대신 BANK 를 매칭해 무통장입금 주문이
* 자동취소에서 누락되던 회귀를 차단한다 (주문 442).
*/
public function test_does_not_cancel_expired_bank_transfer_order(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
// 계좌이체(BANK)는 입금 기한 만료 자동취소 대상이 아님
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::BANK,
'deposit_due_at' => Carbon::now()->subDay(),
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $order->order_status);
}
public function test_does_not_cancel_non_expired_order(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
// 아직 만료되지 않은 가상계좌
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->addDays(2), // 2일 후 만료
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $order->order_status);
}
public function test_does_not_cancel_paid_order(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE, // 이미 결제됨
]);
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->subDay(),
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $order->order_status);
}
public function test_dry_run_does_not_cancel(): void
{
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->subDay(),
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders --dry-run')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $order->order_status);
}
public function test_respects_limit_option(): void
{
$user = User::factory()->create();
// 5개 만료 주문 생성
for ($i = 0; $i < 5; $i++) {
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->subDay(),
]);
}
// 2개만 처리
$this->artisan('sirsoft-ecommerce:cancel-pending-orders --limit=2')
->assertSuccessful();
$cancelledCount = Order::where('order_status', OrderStatusEnum::CANCELLED)->count();
$this->assertEquals(2, $cancelledCount);
}
public function test_disabled_when_config_is_false(): void
{
$this->moduleSettings['order_settings.auto_cancel_expired'] = false;
$this->mockModuleSetting();
$user = User::factory()->create();
$order = Order::factory()->create([
'user_id' => $user->id,
'order_status' => OrderStatusEnum::PENDING_PAYMENT,
]);
OrderPayment::factory()->create([
'order_id' => $order->id,
'payment_method' => PaymentMethodEnum::VBANK,
'vbank_due_at' => Carbon::now()->subDay(),
]);
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->assertSuccessful();
$order->refresh();
$this->assertEquals(OrderStatusEnum::PENDING_PAYMENT, $order->order_status);
}
public function test_outputs_no_orders_message_when_empty(): void
{
$this->artisan('sirsoft-ecommerce:cancel-pending-orders')
->expectsOutput('처리할 만료 주문이 없습니다.')
->assertSuccessful();
}
}